# SPDX-License-Identifier: MIT """ Tests for `attr._make`. """ import copy import functools import gc import inspect import itertools import sys from operator import attrgetter import pytest from hypothesis import assume, given from hypothesis.strategies import booleans, integers, lists, sampled_from, text import attr from attr import _config from attr._compat import PY310 from attr._make import ( Attribute, Factory, _AndValidator, _Attributes, _ClassBuilder, _CountingAttr, _determine_attrib_eq_order, _determine_attrs_eq_order, _determine_whether_to_implement, _transform_attrs, and_, fields, fields_dict, make_class, validate, ) from attr.exceptions import DefaultAlreadySetError, NotAnAttrsClassError from .strategies import ( gen_attr_names, list_of_attrs, optional_bool, 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: """ 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 def make_tc(): class TransformC: z = attr.ib() y = attr.ib() x = attr.ib() a = 42 return TransformC class TestTransformAttrs: """ Tests for `_transform_attrs`. """ def test_no_modifications(self): """ Does not attach __attrs_attrs__ to the class. """ C = make_tc() _transform_attrs(C, None, False, False, True, None) 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, False, True, None) assert ["z", "y", "x"] == [a.name for a in attrs] def test_empty(self): """ No attributes works as expected. """ @attr.s class C: pass assert _Attributes(((), [], {})) == _transform_attrs( C, None, False, False, True, None ) def test_transforms_to_attribute(self): """ All `_CountingAttr`s are transformed into `Attribute`s. """ C = make_tc() attrs, base_attrs, _ = _transform_attrs( C, None, False, False, True, None ) assert [] == base_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: x = attr.ib(default=None) y = attr.ib() with pytest.raises(ValueError) as e: _transform_attrs(C, None, False, False, True, 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, " "eq=True, eq_key=None, order=True, order_key=None, " "hash=None, init=True, " "metadata=mappingproxy({}), type=None, converter=None, " "kw_only=False, inherited=False, on_setattr=None)", ) == e.value.args def test_kw_only(self): """ Converts all attributes, including base class' attributes, if `kw_only` is provided. Therefore, `kw_only` allows attributes with defaults to precede mandatory attributes. Updates in the subclass *don't* affect the base class attributes. """ @attr.s class B: b = attr.ib() for b_a in B.__attrs_attrs__: assert b_a.kw_only is False class C(B): x = attr.ib(default=None) y = attr.ib() attrs, base_attrs, _ = _transform_attrs( C, None, False, True, True, None ) assert len(attrs) == 3 assert len(base_attrs) == 1 for a in attrs: assert a.kw_only is True for b_a in B.__attrs_attrs__: assert b_a.kw_only is False def test_these(self): """ If these is passed, use it and ignore body and base classes. """ class Base: z = attr.ib() class C(Base): y = attr.ib() attrs, base_attrs, _ = _transform_attrs( C, {"x": attr.ib()}, False, False, True, None ) assert [] == base_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: 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=dict([("a", a), ("b", b)])) class C: pass assert "C(a=1, b=2)" == repr(C()) def test_multiple_inheritance_old(self): """ Old multiple inheritance attribute collection behavior is retained. See #285 """ @attr.s class A: 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()) def test_overwrite_proper_mro(self): """ The proper MRO path works single overwrites too. """ @attr.s(collect_by_mro=True) class C: x = attr.ib(default=1) @attr.s(collect_by_mro=True) class D(C): x = attr.ib(default=2) assert "D(x=2)" == repr(D()) def test_multiple_inheritance_proper_mro(self): """ Attributes are collected according to the MRO. See #428 """ @attr.s class A: 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(collect_by_mro=True) class E(C, D): e1 = attr.ib(default="e1") e2 = attr.ib(default="e2") assert ( "E(a1='a1', a2='a2', d1='d1', d2='d2', b1='b1', b2='b2', c1='c1', " "c2='c2', e1='e1', e2='e2')" ) == repr(E()) def test_mro(self): """ Attributes and methods are looked up the same way. See #428 """ @attr.s(collect_by_mro=True) class A: x = attr.ib(10) def xx(self): return 10 @attr.s(collect_by_mro=True) class B(A): y = attr.ib(20) @attr.s(collect_by_mro=True) class C(A): x = attr.ib(50) def xx(self): return 50 @attr.s(collect_by_mro=True) class D(B, C): pass d = D() assert d.x == d.xx() def test_inherited(self): """ Inherited Attributes have `.inherited` True, otherwise False. """ @attr.s class A: a = attr.ib() @attr.s class B(A): b = attr.ib() @attr.s class C(B): a = attr.ib() c = attr.ib() f = attr.fields assert False is f(A).a.inherited assert True is f(B).a.inherited assert False is f(B).b.inherited assert False is f(C).a.inherited assert True is f(C).b.inherited assert False is f(C).c.inherited class TestAttributes: """ Tests for the `attrs`/`attr.s` class decorator. """ def test_sets_attrs(self): """ Sets the `__attrs_attrs__` class attribute with a list of `Attribute`s. """ @attr.s class C: 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: 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: 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__"), ("eq", "__eq__"), ("order", "__le__"), ("hash", "__hash__"), ("init", "__init__"), ], ) def test_respects_add_arguments(self, arg_name, method_name): """ If a certain `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, "eq": True, "order": True, "hash": True, "init": True, } am_args[arg_name] = False if arg_name == "eq": am_args["order"] = False class C: x = attr.ib() setattr(C, method_name, sentinel) C = attr.s(**am_args)(C) assert sentinel == getattr(C, method_name) @pytest.mark.parametrize("init", [True, False]) def test_respects_init_attrs_init(self, init): """ If init=False, adds __attrs_init__ to the class. Otherwise, it does not. """ class C: x = attr.ib() C = attr.s(init=init)(C) assert hasattr(C, "__attrs_init__") != init @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: @attr.s(slots=slots_inner) class D: 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: @attr.s(repr_ns="C", slots=slots_inner) class D: pass assert "C.D()" == repr(C.D()) @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: @attr.s(slots=slots_inner) class D: pass assert C.D.__name__ == "D" assert C.D.__qualname__ == C.__qualname__ + ".D" @pytest.mark.parametrize("with_validation", [True, False]) def test_pre_init(self, with_validation, monkeypatch): """ Verify that __attrs_pre_init__ gets called if defined. """ monkeypatch.setattr(_config, "_run_validators", with_validation) @attr.s class C: def __attrs_pre_init__(self2): self2.z = 30 c = C() assert 30 == getattr(c, "z", None) @pytest.mark.parametrize("with_validation", [True, False]) 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: 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) @pytest.mark.parametrize("with_validation", [True, False]) def test_pre_post_init_order(self, with_validation, monkeypatch): """ Verify that __attrs_post_init__ gets called if defined. """ monkeypatch.setattr(_config, "_run_validators", with_validation) @attr.s class C: x = attr.ib() def __attrs_pre_init__(self2): self2.z = 30 def __attrs_post_init__(self2): self2.z += self2.x c = C(x=10) assert 40 == getattr(c, "z", None) def test_types(self): """ Sets the `Attribute.type` attr from type argument. """ @attr.s class C: 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 def test_clean_class(self, slots): """ Attribute definitions do not appear on the class body after @attr.s. """ @attr.s(slots=slots) class C: 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: 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: 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: x = attr.ib(factory=Factory(list)) def test_inherited_does_not_affect_hashing_and_equality(self): """ Whether or not an Attribute has been inherited doesn't affect how it's hashed and compared. """ @attr.s class BaseClass: x = attr.ib() @attr.s class SubClass(BaseClass): pass ba = attr.fields(BaseClass)[0] sa = attr.fields(SubClass)[0] assert ba == sa assert hash(ba) == hash(sa) class TestKeywordOnlyAttributes: """ Tests for keyword-only attributes. """ def test_adds_keyword_only_arguments(self): """ Attributes can be added as keyword-only. """ @attr.s class C: a = attr.ib() b = attr.ib(default=2, kw_only=True) c = attr.ib(kw_only=True) d = attr.ib(default=attr.Factory(lambda: 4), kw_only=True) c = C(1, c=3) assert c.a == 1 assert c.b == 2 assert c.c == 3 assert c.d == 4 def test_ignores_kw_only_when_init_is_false(self): """ Specifying ``kw_only=True`` when ``init=False`` is essentially a no-op. """ @attr.s class C: x = attr.ib(init=False, default=0, kw_only=True) y = attr.ib() c = C(1) assert c.x == 0 assert c.y == 1 def test_keyword_only_attributes_presence(self): """ Raises `TypeError` when keyword-only arguments are not specified. """ @attr.s class C: x = attr.ib(kw_only=True) with pytest.raises(TypeError) as e: C() assert ( "missing 1 required keyword-only argument: 'x'" ) in e.value.args[0] def test_keyword_only_attributes_unexpected(self): """ Raises `TypeError` when unexpected keyword argument passed. """ @attr.s class C: x = attr.ib(kw_only=True) with pytest.raises(TypeError) as e: C(x=5, y=10) assert "got an unexpected keyword argument 'y'" in e.value.args[0] def test_keyword_only_attributes_can_come_in_any_order(self): """ Mandatory vs non-mandatory attr order only matters when they are part of the __init__ signature and when they aren't kw_only (which are moved to the end and can be mandatory or non-mandatory in any order, as they will be specified as keyword args anyway). """ @attr.s class C: a = attr.ib(kw_only=True) b = attr.ib(kw_only=True, default="b") c = attr.ib(kw_only=True) d = attr.ib() e = attr.ib(default="e") f = attr.ib(kw_only=True) g = attr.ib(kw_only=True, default="g") h = attr.ib(kw_only=True) i = attr.ib(init=False) c = C("d", a="a", c="c", f="f", h="h") assert c.a == "a" assert c.b == "b" assert c.c == "c" assert c.d == "d" assert c.e == "e" assert c.f == "f" assert c.g == "g" assert c.h == "h" def test_keyword_only_attributes_allow_subclassing(self): """ Subclass can define keyword-only attributed without defaults, when the base class has attributes with defaults. """ @attr.s class Base: x = attr.ib(default=0) @attr.s class C(Base): y = attr.ib(kw_only=True) c = C(y=1) assert c.x == 0 assert c.y == 1 def test_keyword_only_class_level(self): """ `kw_only` can be provided at the attr.s level, converting all attributes to `kw_only.` """ @attr.s(kw_only=True) class C: x = attr.ib() y = attr.ib(kw_only=True) with pytest.raises(TypeError): C(0, y=1) c = C(x=0, y=1) assert c.x == 0 assert c.y == 1 def test_keyword_only_class_level_subclassing(self): """ Subclass `kw_only` propagates to attrs inherited from the base, allowing non-default following default. """ @attr.s class Base: x = attr.ib(default=0) @attr.s(kw_only=True) class C(Base): y = attr.ib() with pytest.raises(TypeError): C(1) c = C(x=0, y=1) assert c.x == 0 assert c.y == 1 def test_init_false_attribute_after_keyword_attribute(self): """ A positional attribute cannot follow a `kw_only` attribute, but an `init=False` attribute can because it won't appear in `__init__` """ @attr.s class KwArgBeforeInitFalse: kwarg = attr.ib(kw_only=True) non_init_function_default = attr.ib(init=False) non_init_keyword_default = attr.ib( init=False, default="default-by-keyword" ) @non_init_function_default.default def _init_to_init(self): return self.kwarg + "b" c = KwArgBeforeInitFalse(kwarg="a") assert c.kwarg == "a" assert c.non_init_function_default == "ab" assert c.non_init_keyword_default == "default-by-keyword" def test_init_false_attribute_after_keyword_attribute_with_inheritance( self, ): """ A positional attribute cannot follow a `kw_only` attribute, but an `init=False` attribute can because it won't appear in `__init__`. This test checks that we allow this even when the `kw_only` attribute appears in a parent class """ @attr.s class KwArgBeforeInitFalseParent: kwarg = attr.ib(kw_only=True) @attr.s class KwArgBeforeInitFalseChild(KwArgBeforeInitFalseParent): non_init_function_default = attr.ib(init=False) non_init_keyword_default = attr.ib( init=False, default="default-by-keyword" ) @non_init_function_default.default def _init_to_init(self): return self.kwarg + "b" c = KwArgBeforeInitFalseChild(kwarg="a") assert c.kwarg == "a" assert c.non_init_function_default == "ab" assert c.non_init_keyword_default == "default-by-keyword" @attr.s class GC: @attr.s class D: pass class TestMakeClass: """ 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: 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: 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: """ 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: pass b = _ClassBuilder( C, None, True, True, False, False, False, False, False, False, True, None, False, None, ) assert "<_ClassBuilder(cls=C)>" == repr(b) def test_returns_self(self): """ All methods return the builder for chaining. """ class C: x = attr.ib() b = _ClassBuilder( C, None, True, True, False, False, False, False, False, False, True, None, False, None, ) cls = ( b.add_eq() .add_order() .add_hash() .add_init() .add_attrs_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: def organic(self): pass @attr.s(hash=True, str=True) class D: pass meth_C = getattr(C, meth_name) meth_D = getattr(D, meth_name) assert meth_name == meth_C.__name__ == meth_D.__name__ assert C.organic.__module__ == meth_C.__module__ == meth_D.__module__ # This is assertion that would fail if a single __ne__ instance # was reused across multiple _make_eq calls. organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0] assert organic_prefix + "." + meth_name == meth_C.__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: pass b = _ClassBuilder( C, these=None, slots=False, frozen=False, weakref_slot=True, getstate_setstate=False, auto_attribs=False, is_exc=False, kw_only=False, cache_hash=False, collect_by_mro=True, on_setattr=None, has_custom_setattr=False, field_transformer=None, ) 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 slotted classes. """ @attr.s(slots=True) class C: __weakref__ = attr.ib( init=False, hash=False, repr=False, eq=False, order=False ) assert C() == copy.deepcopy(C()) def test_no_references_to_original(self): """ When subclassing a slotted class, there are no stray references to the original class. """ @attr.s(slots=True) class C: pass @attr.s(slots=True) class C2(C): pass # The original C2 is in a reference cycle, so force a collect: gc.collect() assert [C2] == C.__subclasses__() def _get_copy_kwargs(include_slots=True): """ Generate a list of compatible attr.s arguments for the `copy` tests. """ options = ["frozen", "hash", "cache_hash"] if include_slots: options.extend(["slots", "weakref_slot"]) out_kwargs = [] for args in itertools.product([True, False], repeat=len(options)): kwargs = dict(zip(options, args)) kwargs["hash"] = kwargs["hash"] or None if kwargs["cache_hash"] and not ( kwargs["frozen"] or kwargs["hash"] ): continue out_kwargs.append(kwargs) return out_kwargs @pytest.mark.parametrize("kwargs", _get_copy_kwargs()) def test_copy(self, kwargs): """ Ensure that an attrs class can be copied successfully. """ @attr.s(eq=True, **kwargs) class C: x = attr.ib() a = C(1) b = copy.deepcopy(a) assert a == b @pytest.mark.parametrize("kwargs", _get_copy_kwargs(include_slots=False)) def test_copy_custom_setstate(self, kwargs): """ Ensure that non-slots classes respect a custom __setstate__. """ @attr.s(eq=True, **kwargs) class C: x = attr.ib() def __getstate__(self): return self.__dict__ def __setstate__(self, state): state["x"] *= 5 self.__dict__.update(state) expected = C(25) actual = copy.copy(C(5)) assert actual == expected class TestMakeOrder: """ Tests for _make_order(). """ def test_subclasses_cannot_be_compared(self): """ Calling comparison methods on subclasses raises a TypeError. We use the actual operation so we get an error raised on Python 3. """ @attr.s class A: a = attr.ib() @attr.s class B(A): pass a = A(42) b = B(42) assert a <= a assert a >= a assert not a < a assert not a > a assert ( NotImplemented == a.__lt__(b) == a.__le__(b) == a.__gt__(b) == a.__ge__(b) ) with pytest.raises(TypeError): a <= b with pytest.raises(TypeError): a >= b with pytest.raises(TypeError): a < b with pytest.raises(TypeError): a > b class TestDetermineAttrsEqOrder: def test_default(self): """ If all are set to None, set both eq and order to the passed default. """ assert (42, 42) == _determine_attrs_eq_order(None, None, None, 42) @pytest.mark.parametrize("eq", [True, False]) def test_order_mirrors_eq_by_default(self, eq): """ If order is None, it mirrors eq. """ assert (eq, eq) == _determine_attrs_eq_order(None, eq, None, True) def test_order_without_eq(self): """ eq=False, order=True raises a meaningful ValueError. """ with pytest.raises( ValueError, match="`order` can only be True if `eq` is True too." ): _determine_attrs_eq_order(None, False, True, True) @given(cmp=booleans(), eq=optional_bool, order=optional_bool) def test_mix(self, cmp, eq, order): """ If cmp is not None, eq and order must be None and vice versa. """ assume(eq is not None or order is not None) with pytest.raises( ValueError, match="Don't mix `cmp` with `eq' and `order`." ): _determine_attrs_eq_order(cmp, eq, order, True) class TestDetermineAttribEqOrder: def test_default(self): """ If all are set to None, set both eq and order to the passed default. """ assert (42, None, 42, None) == _determine_attrib_eq_order( None, None, None, 42 ) def test_eq_callable_order_boolean(self): """ eq=callable or order=callable need to transformed into eq/eq_key or order/order_key. """ assert (True, str.lower, False, None) == _determine_attrib_eq_order( None, str.lower, False, True ) def test_eq_callable_order_callable(self): """ eq=callable or order=callable need to transformed into eq/eq_key or order/order_key. """ assert (True, str.lower, True, abs) == _determine_attrib_eq_order( None, str.lower, abs, True ) def test_eq_boolean_order_callable(self): """ eq=callable or order=callable need to transformed into eq/eq_key or order/order_key. """ assert (True, None, True, str.lower) == _determine_attrib_eq_order( None, True, str.lower, True ) @pytest.mark.parametrize("eq", [True, False]) def test_order_mirrors_eq_by_default(self, eq): """ If order is None, it mirrors eq. """ assert (eq, None, eq, None) == _determine_attrib_eq_order( None, eq, None, True ) def test_order_without_eq(self): """ eq=False, order=True raises a meaningful ValueError. """ with pytest.raises( ValueError, match="`order` can only be True if `eq` is True too." ): _determine_attrib_eq_order(None, False, True, True) @given(cmp=booleans(), eq=optional_bool, order=optional_bool) def test_mix(self, cmp, eq, order): """ If cmp is not None, eq and order must be None and vice versa. """ assume(eq is not None or order is not None) with pytest.raises( ValueError, match="Don't mix `cmp` with `eq' and `order`." ): _determine_attrib_eq_order(cmp, eq, order, True) class TestDocs: @pytest.mark.parametrize( "meth_name", [ "__init__", "__repr__", "__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__", ], ) def test_docs(self, meth_name): """ Tests the presence and correctness of the documentation for the generated methods """ @attr.s class A: pass if hasattr(A, "__qualname__"): method = getattr(A, meth_name) expected = f"Method generated by attrs for class {A.__qualname__}." assert expected == method.__doc__ class BareC: pass class BareSlottedC: __slots__ = () class TestAutoDetect: @pytest.mark.parametrize("C", (BareC, BareSlottedC)) def test_determine_detects_non_presence_correctly(self, C): """ On an empty class, nothing should be detected. """ assert True is _determine_whether_to_implement( C, None, True, ("__init__",) ) assert True is _determine_whether_to_implement( C, None, True, ("__repr__",) ) assert True is _determine_whether_to_implement( C, None, True, ("__eq__", "__ne__") ) assert True is _determine_whether_to_implement( C, None, True, ("__le__", "__lt__", "__ge__", "__gt__") ) def test_make_all_by_default(self, slots, frozen): """ If nothing is there to be detected, imply init=True, repr=True, hash=None, eq=True, order=True. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() i = C(1) o = object() assert i.__init__ is not o.__init__ assert i.__repr__ is not o.__repr__ assert i.__eq__ is not o.__eq__ assert i.__ne__ is not o.__ne__ assert i.__le__ is not o.__le__ assert i.__lt__ is not o.__lt__ assert i.__ge__ is not o.__ge__ assert i.__gt__ is not o.__gt__ def test_detect_auto_init(self, slots, frozen): """ If auto_detect=True and an __init__ exists, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) class CI: x = attr.ib() def __init__(self): object.__setattr__(self, "x", 42) assert 42 == CI().x def test_detect_auto_repr(self, slots, frozen): """ If auto_detect=True and an __repr__ exists, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() def __repr__(self): return "hi" assert "hi" == repr(C(42)) def test_hash_uses_eq(self, slots, frozen): """ If eq is passed in, then __hash__ should use the eq callable to generate the hash code. """ @attr.s(slots=slots, frozen=frozen, hash=True) class C: x = attr.ib(eq=str) @attr.s(slots=slots, frozen=frozen, hash=True) class D: x = attr.ib() # These hashes should be the same because 1 is turned into # string before hashing. assert hash(C("1")) == hash(C(1)) assert hash(D("1")) != hash(D(1)) def test_detect_auto_hash(self, slots, frozen): """ If auto_detect=True and an __hash__ exists, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() def __hash__(self): return 0xC0FFEE assert 0xC0FFEE == hash(C(42)) def test_detect_auto_eq(self, slots, frozen): """ If auto_detect=True and an __eq__ or an __ne__, exist, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() def __eq__(self, o): raise ValueError("worked") with pytest.raises(ValueError, match="worked"): C(1) == C(1) @attr.s(auto_detect=True, slots=slots, frozen=frozen) class D: x = attr.ib() def __ne__(self, o): raise ValueError("worked") with pytest.raises(ValueError, match="worked"): D(1) != D(1) def test_detect_auto_order(self, slots, frozen): """ If auto_detect=True and an __ge__, __gt__, __le__, or and __lt__ exist, don't write one. It's surprisingly difficult to test this programmatically, so we do it by hand. """ def assert_not_set(cls, ex, meth_name): __tracebackhide__ = True a = getattr(cls, meth_name) if meth_name == ex: assert a == 42 else: assert a is getattr(object, meth_name) def assert_none_set(cls, ex): __tracebackhide__ = True for m in ("le", "lt", "ge", "gt"): assert_not_set(cls, ex, "__" + m + "__") @attr.s(auto_detect=True, slots=slots, frozen=frozen) class LE: __le__ = 42 @attr.s(auto_detect=True, slots=slots, frozen=frozen) class LT: __lt__ = 42 @attr.s(auto_detect=True, slots=slots, frozen=frozen) class GE: __ge__ = 42 @attr.s(auto_detect=True, slots=slots, frozen=frozen) class GT: __gt__ = 42 assert_none_set(LE, "__le__") assert_none_set(LT, "__lt__") assert_none_set(GE, "__ge__") assert_none_set(GT, "__gt__") def test_override_init(self, slots, frozen): """ If init=True is passed, ignore __init__. """ @attr.s(init=True, auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() def __init__(self): pytest.fail("should not be called") assert C(1) == C(1) def test_override_repr(self, slots, frozen): """ If repr=True is passed, ignore __repr__. """ @attr.s(repr=True, auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() def __repr__(self): pytest.fail("should not be called") assert "C(x=1)" == repr(C(1)) def test_override_hash(self, slots, frozen): """ If hash=True is passed, ignore __hash__. """ @attr.s(hash=True, auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() def __hash__(self): pytest.fail("should not be called") assert hash(C(1)) def test_override_eq(self, slots, frozen): """ If eq=True is passed, ignore __eq__ and __ne__. """ @attr.s(eq=True, auto_detect=True, slots=slots, frozen=frozen) class C: x = attr.ib() def __eq__(self, o): pytest.fail("should not be called") def __ne__(self, o): pytest.fail("should not be called") assert C(1) == C(1) @pytest.mark.parametrize( "eq,order,cmp", [ (True, None, None), (True, True, None), (None, True, None), (None, None, True), ], ) def test_override_order(self, slots, frozen, eq, order, cmp): """ If order=True is passed, ignore __le__, __lt__, __gt__, __ge__. eq=True and cmp=True both imply order=True so test it too. """ def meth(self, o): pytest.fail("should not be called") @attr.s( cmp=cmp, order=order, eq=eq, auto_detect=True, slots=slots, frozen=frozen, ) class C: x = attr.ib() __le__ = __lt__ = __gt__ = __ge__ = meth assert C(1) < C(2) assert C(1) <= C(2) assert C(2) > C(1) assert C(2) >= C(1) @pytest.mark.parametrize("first", [True, False]) def test_total_ordering(self, slots, first): """ functools.total_ordering works as expected if an order method and an eq method are detected. Ensure the order doesn't matter. """ class C: x = attr.ib() own_eq_called = attr.ib(default=False) own_le_called = attr.ib(default=False) def __eq__(self, o): self.own_eq_called = True return self.x == o.x def __le__(self, o): self.own_le_called = True return self.x <= o.x if first: C = functools.total_ordering( attr.s(auto_detect=True, slots=slots)(C) ) else: C = attr.s(auto_detect=True, slots=slots)( functools.total_ordering(C) ) c1, c2 = C(1), C(2) assert c1 < c2 assert c1.own_le_called c1, c2 = C(1), C(2) assert c2 > c1 assert c2.own_le_called c1, c2 = C(1), C(2) assert c2 != c1 assert c1 == c1 assert c1.own_eq_called def test_detects_setstate_getstate(self, slots): """ __getstate__ and __setstate__ are not overwritten if either is present. """ @attr.s(slots=slots, auto_detect=True) class C: def __getstate__(self): return ("hi",) assert getattr(object, "__setstate__", None) is getattr( C, "__setstate__", None ) @attr.s(slots=slots, auto_detect=True) class C: called = attr.ib(False) def __setstate__(self, state): self.called = True i = C() assert False is i.called i.__setstate__(()) assert True is i.called assert getattr(object, "__getstate__", None) is getattr( C, "__getstate__", None ) @pytest.mark.skipif(PY310, reason="Pre-3.10 only.") def test_match_args_pre_310(self): """ __match_args__ is not created on Python versions older than 3.10. """ @attr.s class C: a = attr.ib() assert None is getattr(C, "__match_args__", None) @pytest.mark.skipif(not PY310, reason="Structural pattern matching is 3.10+") class TestMatchArgs: """ Tests for match_args and __match_args__ generation. """ def test_match_args(self): """ __match_args__ is created by default on Python 3.10. """ @attr.define class C: a = attr.field() assert ("a",) == C.__match_args__ def test_explicit_match_args(self): """ A custom __match_args__ set is not overwritten. """ ma = () @attr.define class C: a = attr.field() __match_args__ = ma assert C(42).__match_args__ is ma @pytest.mark.parametrize("match_args", [True, False]) def test_match_args_attr_set(self, match_args): """ __match_args__ is set depending on match_args. """ @attr.define(match_args=match_args) class C: a = attr.field() if match_args: assert hasattr(C, "__match_args__") else: assert not hasattr(C, "__match_args__") def test_match_args_kw_only(self): """ kw_only classes don't generate __match_args__. kw_only fields are not included in __match_args__. """ @attr.define class C: a = attr.field(kw_only=True) b = attr.field() assert C.__match_args__ == ("b",) @attr.define(kw_only=True) class C: a = attr.field() b = attr.field() assert C.__match_args__ == () def test_match_args_argument(self): """ match_args being False with inheritance. """ @attr.define(match_args=False) class X: a = attr.field() assert "__match_args__" not in X.__dict__ @attr.define(match_args=False) class Y: a = attr.field() __match_args__ = ("b",) assert Y.__match_args__ == ("b",) @attr.define(match_args=False) class Z(Y): z = attr.field() assert Z.__match_args__ == ("b",) @attr.define class A: a = attr.field() z = attr.field() @attr.define(match_args=False) class B(A): b = attr.field() assert B.__match_args__ == ("a", "z") def test_make_class(self): """ match_args generation with make_class. """ C1 = make_class("C1", ["a", "b"]) assert ("a", "b") == C1.__match_args__ C1 = make_class("C1", ["a", "b"], match_args=False) assert not hasattr(C1, "__match_args__") C1 = make_class("C1", ["a", "b"], kw_only=True) assert () == C1.__match_args__ C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()}) assert ("b",) == C1.__match_args__