""" Tests for `attr._make`. """ from __future__ import absolute_import, division, print_function import copy import inspect import itertools import sys from operator import attrgetter import pytest from hypothesis import given from hypothesis.strategies import booleans, integers, lists, sampled_from, text import attr from attr import _config from attr._compat import PY2, ordered_dict from attr._make import ( Attribute, Factory, _AndValidator, _Attributes, _ClassBuilder, _CountingAttr, _transform_attrs, and_, fields, fields_dict, make_class, validate, ) from attr.exceptions import DefaultAlreadySetError, NotAnAttrsClassError from .strategies import ( gen_attr_names, list_of_attrs, simple_attrs, simple_attrs_with_metadata, simple_attrs_without_metadata, simple_classes, ) from .utils import simple_attr attrs_st = simple_attrs.map(lambda c: Attribute.from_counting_attr("name", c)) class TestCountingAttr(object): """ Tests for `attr`. """ def test_returns_Attr(self): """ Returns an instance of _CountingAttr. """ a = attr.ib() assert isinstance(a, _CountingAttr) def test_validators_lists_to_wrapped_tuples(self): """ If a list is passed as validator, it's just converted to a tuple. """ def v1(_, __): pass def v2(_, __): pass a = attr.ib(validator=[v1, v2]) assert _AndValidator((v1, v2)) == a._validator def test_validator_decorator_single(self): """ If _CountingAttr.validator is used as a decorator and there is no decorator set, the decorated method is used as the validator. """ a = attr.ib() @a.validator def v(): pass assert v == a._validator @pytest.mark.parametrize( "wrap", [lambda v: v, lambda v: [v], lambda v: and_(v)] ) def test_validator_decorator(self, wrap): """ If _CountingAttr.validator is used as a decorator and there is already a decorator set, the decorators are composed using `and_`. """ def v(_, __): pass a = attr.ib(validator=wrap(v)) @a.validator def v2(self, _, __): pass assert _AndValidator((v, v2)) == a._validator def test_default_decorator_already_set(self): """ Raise DefaultAlreadySetError if the decorator is used after a default has been set. """ a = attr.ib(default=42) with pytest.raises(DefaultAlreadySetError): @a.default def f(self): pass def test_default_decorator_sets(self): """ Decorator wraps the method in a Factory with pass_self=True and sets the default. """ a = attr.ib() @a.default def f(self): pass assert Factory(f, True) == a._default class TestAttribute(object): """ Tests for `attr.Attribute`. """ def test_deprecated_convert_argument(self): """ Using *convert* raises a DeprecationWarning and sets the converter field. """ def conv(v): return v with pytest.warns(DeprecationWarning) as wi: a = Attribute( "a", True, True, True, True, True, True, convert=conv ) w = wi.pop() assert conv == a.converter assert ( "The `convert` argument is deprecated in favor of `converter`. " "It will be removed after 2019/01.", ) == w.message.args assert __file__ == w.filename def test_deprecated_convert_attribute(self): """ If Attribute.convert is accessed, a DeprecationWarning is raised. """ def conv(v): return v a = simple_attr("a", converter=conv) with pytest.warns(DeprecationWarning) as wi: convert = a.convert w = wi.pop() assert conv is convert is a.converter assert ( "The `convert` attribute is deprecated in favor of `converter`. " "It will be removed after 2019/01.", ) == w.message.args assert __file__ == w.filename def test_convert_converter(self): """ A TypeError is raised if both *convert* and *converter* are passed. """ with pytest.raises(RuntimeError) as ei: Attribute( "a", True, True, True, True, True, True, convert=lambda v: v, converter=lambda v: v, ) assert ( "Can't pass both `convert` and `converter`. " "Please use `converter` only.", ) == ei.value.args def make_tc(): class TransformC(object): z = attr.ib() y = attr.ib() x = attr.ib() a = 42 return TransformC class TestTransformAttrs(object): """ Tests for `_transform_attrs`. """ def test_no_modifications(self): """ Doesn't attach __attrs_attrs__ to the class anymore. """ C = make_tc() _transform_attrs(C, None, False) assert None is getattr(C, "__attrs_attrs__", None) def test_normal(self): """ Transforms every `_CountingAttr` and leaves others (a) be. """ C = make_tc() attrs, _, _ = _transform_attrs(C, None, False) assert ["z", "y", "x"] == [a.name for a in attrs] def test_empty(self): """ No attributes works as expected. """ @attr.s class C(object): pass assert _Attributes(((), [], {})) == _transform_attrs(C, None, False) def test_transforms_to_attribute(self): """ All `_CountingAttr`s are transformed into `Attribute`s. """ C = make_tc() attrs, super_attrs, _ = _transform_attrs(C, None, False) assert [] == super_attrs assert 3 == len(attrs) assert all(isinstance(a, Attribute) for a in attrs) def test_conflicting_defaults(self): """ Raises `ValueError` if attributes with defaults are followed by mandatory attributes. """ class C(object): x = attr.ib(default=None) y = attr.ib() with pytest.raises(ValueError) as e: _transform_attrs(C, None, False) 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=None, init=True, metadata=mappingproxy({}), " "type=None, converter=None)", ) == e.value.args def test_these(self): """ If these is passed, use it and ignore body and super classes. """ class Base(object): z = attr.ib() class C(Base): y = attr.ib() attrs, super_attrs, _ = _transform_attrs(C, {"x": attr.ib()}, False) assert [] == super_attrs assert (simple_attr("x"),) == attrs def test_these_leave_body(self): """ If these is passed, no attributes are removed from the body. """ @attr.s(init=False, these={"x": attr.ib()}) class C(object): x = 5 assert 5 == C().x assert "C(x=5)" == repr(C()) def test_these_ordered(self): """ If these is passed ordered attrs, their order respect instead of the counter. """ b = attr.ib(default=2) a = attr.ib(default=1) @attr.s(these=ordered_dict([("a", a), ("b", b)])) class C(object): pass assert "C(a=1, b=2)" == repr(C()) def test_multiple_inheritance(self): """ Order of attributes doesn't get mixed up by multiple inheritance. See #285 """ @attr.s class A(object): a1 = attr.ib(default="a1") a2 = attr.ib(default="a2") @attr.s class B(A): b1 = attr.ib(default="b1") b2 = attr.ib(default="b2") @attr.s class C(B, A): c1 = attr.ib(default="c1") c2 = attr.ib(default="c2") @attr.s class D(A): d1 = attr.ib(default="d1") d2 = attr.ib(default="d2") @attr.s class E(C, D): e1 = attr.ib(default="e1") e2 = attr.ib(default="e2") assert ( "E(a1='a1', a2='a2', b1='b1', b2='b2', c1='c1', c2='c2', d1='d1', " "d2='d2', e1='e1', e2='e2')" ) == repr(E()) class TestAttributes(object): """ Tests for the `attrs`/`attr.s` class decorator. """ @pytest.mark.skipif(not PY2, 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: @attr.s 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. """ @attr.s class C(object): x = attr.ib() 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. """ @attr.s class C3(object): pass assert "C3()" == repr(C3()) assert C3() == C3() @given(attr=attrs_st, attr_name=sampled_from(Attribute.__slots__)) def test_immutable(self, attr, attr_name): """ Attribute instances are immutable. """ with pytest.raises(AttributeError): setattr(attr, attr_name, 1) @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 except add_hash are applied. __hash__ is set to None. """ # Set the method name to a sentinel and check whether it has been # overwritten afterwards. sentinel = object() class C(object): x = attr.ib() setattr(C, method_name, sentinel) C = attr.s(C) meth = getattr(C, method_name) assert sentinel != meth if method_name == "__hash__": assert meth is None @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 `False`, `__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.ib() setattr(C, method_name, sentinel) C = attr.s(**am_args)(C) assert sentinel == getattr(C, method_name) @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.") @given(slots_outer=booleans(), slots_inner=booleans()) def test_repr_qualname(self, slots_outer, slots_inner): """ On Python 3, the name in repr is the __qualname__. """ @attr.s(slots=slots_outer) class C(object): @attr.s(slots=slots_inner) class D(object): pass assert "C.D()" == repr(C.D()) assert "GC.D()" == repr(GC.D()) @given(slots_outer=booleans(), slots_inner=booleans()) def test_repr_fake_qualname(self, slots_outer, slots_inner): """ Setting repr_ns overrides a potentially guessed namespace. """ @attr.s(slots=slots_outer) class C(object): @attr.s(repr_ns="C", slots=slots_inner) class D(object): pass assert "C.D()" == repr(C.D()) @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.") @given(slots_outer=booleans(), slots_inner=booleans()) def test_name_not_overridden(self, slots_outer, slots_inner): """ On Python 3, __name__ is different from __qualname__. """ @attr.s(slots=slots_outer) class C(object): @attr.s(slots=slots_inner) class D(object): pass assert C.D.__name__ == "D" assert C.D.__qualname__ == C.__qualname__ + ".D" @given(with_validation=booleans()) def test_post_init(self, with_validation, monkeypatch): """ Verify that __attrs_post_init__ gets called if defined. """ monkeypatch.setattr(_config, "_run_validators", with_validation) @attr.s class C(object): x = attr.ib() y = attr.ib() def __attrs_post_init__(self2): self2.z = self2.x + self2.y c = C(x=10, y=20) assert 30 == getattr(c, "z", None) def test_types(self): """ Sets the `Attribute.type` attr from type argument. """ @attr.s class C(object): x = attr.ib(type=int) y = attr.ib(type=str) z = attr.ib() assert int is fields(C).x.type assert str is fields(C).y.type assert None is fields(C).z.type @pytest.mark.parametrize("slots", [True, False]) def test_clean_class(self, slots): """ Attribute definitions do not appear on the class body after @attr.s. """ @attr.s(slots=slots) class C(object): x = attr.ib() x = getattr(C, "x", None) assert not isinstance(x, _CountingAttr) def test_factory_sugar(self): """ Passing factory=f is syntactic sugar for passing default=Factory(f). """ @attr.s class C(object): x = attr.ib(factory=list) assert Factory(list) == attr.fields(C).x.default def test_sugar_factory_mutex(self): """ Passing both default and factory raises ValueError. """ with pytest.raises(ValueError, match="mutually exclusive"): @attr.s class C(object): x = attr.ib(factory=list, default=Factory(list)) def test_sugar_callable(self): """ Factory has to be a callable to prevent people from passing Factory into it. """ with pytest.raises(ValueError, match="must be a callable"): @attr.s class C(object): x = attr.ib(factory=Factory(list)) @attr.s class GC(object): @attr.s class D(object): pass 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"])) @attr.s class C2(object): a = attr.ib() b = attr.ib() 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.ib(default=42), "b": attr.ib(default=None)} ) @attr.s class C2(object): a = attr.ib(default=42) b = attr.ib(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(" 0 for cls_a, raw_a in zip(fields(C), list_of_attrs): assert cls_a.metadata != {} assert cls_a.metadata == raw_a.metadata def test_metadata(self): """ If metadata that is not None is passed, it is used. This is necessary for coverage because the previous test is hypothesis-based. """ md = {} a = attr.ib(metadata=md) assert md is a.metadata class TestClassBuilder(object): """ Tests for `_ClassBuilder`. """ def test_repr_str(self): """ Trying to add a `__str__` without having a `__repr__` raises a ValueError. """ with pytest.raises(ValueError) as ei: make_class("C", {}, repr=False, str=True) assert ( "__str__ can only be generated if a __repr__ exists.", ) == ei.value.args def test_repr(self): """ repr of builder itself makes sense. """ class C(object): pass b = _ClassBuilder(C, None, True, True, False) assert "<_ClassBuilder(cls=C)>" == repr(b) def test_returns_self(self): """ All methods return the builder for chaining. """ class C(object): x = attr.ib() b = _ClassBuilder(C, None, True, True, False) cls = ( b.add_cmp() .add_hash() .add_init() .add_repr("ns") .add_str() .build_class() ) assert "ns.C(x=1)" == repr(cls(1)) @pytest.mark.parametrize( "meth_name", [ "__init__", "__hash__", "__repr__", "__str__", "__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__", ], ) def test_attaches_meta_dunders(self, meth_name): """ Generated methods have correct __module__, __name__, and __qualname__ attributes. """ @attr.s(hash=True, str=True) class C(object): def organic(self): pass meth = getattr(C, meth_name) assert meth_name == meth.__name__ assert C.organic.__module__ == meth.__module__ if not PY2: organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0] assert organic_prefix + "." + meth_name == meth.__qualname__ def test_handles_missing_meta_on_class(self): """ If the class hasn't a __module__ or __qualname__, the method hasn't either. """ class C(object): pass b = _ClassBuilder( C, these=None, slots=False, frozen=False, auto_attribs=False ) b._cls = {} # no __module__; no __qualname__ def fake_meth(self): pass fake_meth.__module__ = "42" fake_meth.__qualname__ = "23" rv = b._add_method_dunders(fake_meth) assert "42" == rv.__module__ == fake_meth.__module__ assert "23" == rv.__qualname__ == fake_meth.__qualname__ def test_weakref_setstate(self): """ __weakref__ is not set on in setstate because it's not writable in slots classes. """ @attr.s(slots=True) class C(object): __weakref__ = attr.ib( init=False, hash=False, repr=False, cmp=False ) assert C() == copy.deepcopy(C())