""" Tests for `attr._make`. """ from __future__ import absolute_import, division, print_function import pytest from . import simple_attr from attr import _config from attr._compat import PY3 from attr._make import ( Attribute, NOTHING, _CountingAttr, _transform_attrs, attr, attributes, fields, make_class, validate, ) class TestCountingAttr(object): """ Tests for `attr`. """ def test_returns_Attr(self): """ Returns an instance of _CountingAttr. """ a = attr() assert isinstance(a, _CountingAttr) def make_tc(): class TransformC(object): z = attr() y = attr() x = attr() a = 42 return TransformC class TestTransformAttrs(object): """ Tests for `_transform_attrs`. """ def test_normal(self): """ Transforms every `_CountingAttr` and leaves others (a) be. """ C = make_tc() _transform_attrs(C, None) assert ["z", "y", "x"] == [a.name for a in C.__attrs_attrs__] def test_empty(self): """ No attributes works as expected. """ @attributes class C(object): pass _transform_attrs(C, None) assert () == C.__attrs_attrs__ @pytest.mark.parametrize("attribute", [ "z", "y", "x", ]) def test_transforms_to_attribute(self, attribute): """ All `_CountingAttr`s are transformed into `Attribute`s. """ C = make_tc() _transform_attrs(C, None) assert isinstance(getattr(C, attribute), Attribute) def test_conflicting_defaults(self): """ Raises `ValueError` if attributes with defaults are followed by mandatory attributes. """ class C(object): x = attr(default=None) y = attr() with pytest.raises(ValueError) as e: _transform_attrs(C, None) assert ( "No mandatory attributes allowed after an attribute with a " "default value or factory. Attribute in question: Attribute" "(name='y', default=NOTHING, validator=None, repr=True, " "cmp=True, hash=True, init=True, convert=None)", ) == e.value.args def test_these(self): """ If these is passed, use it and ignore body. """ class C(object): y = attr() _transform_attrs(C, {"x": attr()}) assert ( simple_attr("x"), ) == C.__attrs_attrs__ assert isinstance(C.y, _CountingAttr) def test_recurse(self): """ Collect attributes from all sub-classes. """ class A(object): a = None class B(A): b = attr() _transform_attrs(B, None) class C(B): c = attr() _transform_attrs(C, None) class D(C): d = attr() _transform_attrs(D, None) class E(D): e = attr() _transform_attrs(E, None) assert ( simple_attr("b"), simple_attr("c"), simple_attr("d"), simple_attr("e"), ) == E.__attrs_attrs__ class TestAttributes(object): """ Tests for the `attributes` class decorator. """ @pytest.mark.skipif(PY3, reason="No old-style classes in Py3") def test_catches_old_style(self): """ Raises TypeError on old-style classes. """ with pytest.raises(TypeError) as e: @attributes class C: pass assert ("attrs only works with new-style classes.",) == e.value.args def test_sets_attrs(self): """ Sets the `__attrs_attrs__` class attribute with a list of `Attribute`s. """ @attributes class C(object): x = attr() assert "x" == C.__attrs_attrs__[0].name assert all(isinstance(a, Attribute) for a in C.__attrs_attrs__) def test_empty(self): """ No attributes, no problems. """ @attributes class C3(object): pass assert "C3()" == repr(C3()) assert C3() == C3() @pytest.mark.parametrize("method_name", [ "__repr__", "__eq__", "__hash__", "__init__", ]) def test_adds_all_by_default(self, method_name): """ If no further arguments are supplied, all add_XXX functions are applied. """ # Set the method name to a sentinel and check whether it has been # overwritten afterwards. sentinel = object() class C1(object): x = attr() setattr(C1, method_name, sentinel) C1 = attributes(C1) class C2(object): x = attr() setattr(C2, method_name, sentinel) C2 = attributes(C2) assert sentinel != getattr(C1, method_name) assert sentinel != getattr(C2, method_name) @pytest.mark.parametrize("arg_name, method_name", [ ("repr", "__repr__"), ("cmp", "__eq__"), ("hash", "__hash__"), ("init", "__init__"), ]) def test_respects_add_arguments(self, arg_name, method_name): """ If a certain `add_XXX` is `True`, XXX is not added to the class. """ # Set the method name to a sentinel and check whether it has been # overwritten afterwards. sentinel = object() am_args = { "repr": True, "cmp": True, "hash": True, "init": True } am_args[arg_name] = False class C(object): x = attr() setattr(C, method_name, sentinel) C = attributes(**am_args)(C) assert sentinel == getattr(C, method_name) @pytest.mark.skipif(not PY3, reason="__qualname__ is PY3-only.") def test_repr_qualname(self): """ On Python 3, the name in repr is the __qualname__. """ @attributes class C(object): @attributes class D(object): pass assert "C.D()" == repr(C.D()) assert "GC.D()" == repr(GC.D()) def test_repr_fake_qualname(self): """ Setting repr_ns overrides a potentially guessed namespace. """ @attributes class C(object): @attributes(repr_ns="C") class D(object): pass assert "C.D()" == repr(C.D()) @attributes class GC(object): @attributes class D(object): pass class TestAttribute(object): """ Tests for `Attribute`. """ def test_missing_argument(self): """ Raises `TypeError` if an Argument is missing. """ with pytest.raises(TypeError) as e: Attribute(default=NOTHING, validator=None) assert ("Missing argument 'name'.",) == e.value.args def test_too_many_arguments(self): """ Raises `TypeError` if extra arguments are passed. """ with pytest.raises(TypeError) as e: Attribute(name="foo", default=NOTHING, factory=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True, convert=None) assert ("Too many arguments.",) == e.value.args class TestMakeClass(object): """ Tests for `make_class`. """ @pytest.mark.parametrize("ls", [ list, tuple ]) def test_simple(self, ls): """ Passing a list of strings creates attributes with default args. """ C1 = make_class("C1", ls(["a", "b"])) @attributes class C2(object): a = attr() b = attr() assert C1.__attrs_attrs__ == C2.__attrs_attrs__ def test_dict(self): """ Passing a dict of name: _CountingAttr creates an equivalent class. """ C1 = make_class("C1", {"a": attr(default=42), "b": attr(default=None)}) @attributes class C2(object): a = attr(default=42) b = attr(default=None) assert C1.__attrs_attrs__ == C2.__attrs_attrs__ def test_attr_args(self): """ attributes_arguments are passed to attributes """ C = make_class("C", ["x"], repr=False) assert repr(C(1)).startswith("