# SPDX-License-Identifier: MIT """ Tests for `attr._make`. """ import copy import functools import gc import inspect import itertools import sys from operator import attrgetter from typing import Generic, TypeVar 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 PY_3_10_PLUS, PY_3_14_PLUS 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)) @pytest.fixture(name="with_and_without_validation", params=[True, False]) def _with_and_without_validation(request): """ Run tests with and without validation enabled. """ attr.validators.set_disabled(request.param) try: yield finally: attr.validators.set_disabled(False) 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, alias=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={"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__"), ("unsafe_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, "unsafe_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): """ 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. """ with pytest.deprecated_call(match="The `repr_ns` argument"): @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): """ __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.usefixtures("with_and_without_validation") def test_pre_init(self): """ Verify that __attrs_pre_init__ gets called if defined. """ @attr.s class C: def __attrs_pre_init__(self2): self2.z = 30 c = C() assert 30 == getattr(c, "z", None) @pytest.mark.usefixtures("with_and_without_validation") def test_pre_init_args(self): """ Verify that __attrs_pre_init__ gets called with extra args if defined. """ @attr.s class C: x = attr.ib() def __attrs_pre_init__(self2, x): self2.z = x + 1 c = C(x=10) assert 11 == getattr(c, "z", None) @pytest.mark.usefixtures("with_and_without_validation") def test_pre_init_kwargs(self): """ Verify that __attrs_pre_init__ gets called with extra args and kwargs if defined. """ @attr.s class C: x = attr.ib() y = attr.field(kw_only=True) def __attrs_pre_init__(self2, x, y): self2.z = x + y + 1 c = C(10, y=11) assert 22 == getattr(c, "z", None) @pytest.mark.usefixtures("with_and_without_validation") def test_pre_init_kwargs_only(self): """ Verify that __attrs_pre_init__ gets called with extra kwargs only if defined. """ @attr.s class C: y = attr.field(kw_only=True) def __attrs_pre_init__(self2, y): self2.z = y + 1 c = C(y=11) assert 12 == getattr(c, "z", None) @pytest.mark.usefixtures("with_and_without_validation") def test_pre_init_kw_only_work_with_defaults(self): """ Default values together with kw_only don't break __attrs__pre_init__. """ val = None @attr.define class KWOnlyAndDefault: kw_and_default: int = attr.field(kw_only=True, default=3) def __attrs_pre_init__(self, *, kw_and_default): nonlocal val val = kw_and_default inst = KWOnlyAndDefault() assert 3 == val == inst.kw_and_default @pytest.mark.usefixtures("with_and_without_validation") def test_post_init(self): """ Verify that __attrs_post_init__ gets called if defined. """ @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.usefixtures("with_and_without_validation") def test_pre_post_init_order(self): """ Verify that __attrs_post_init__ gets called if defined. """ @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(unsafe_hash=True, str=True) class C: def organic(self): pass @attr.s(unsafe_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__() @pytest.mark.xfail(PY_3_14_PLUS, reason="Currently broken on nightly.") def test_no_references_to_original_when_using_cached_property(self): """ When subclassing a slotted class and using cached property, there are no stray references to the original class. """ @attr.s(slots=True) class C: pass @attr.s(slots=True) class C2(C): @functools.cached_property def value(self) -> int: return 0 # 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", "unsafe_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["unsafe_hash"] = kwargs["unsafe_hash"] or None if kwargs["cache_hash"] and not ( kwargs["frozen"] or kwargs["unsafe_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 TestInitAlias: """ Tests for Attribute alias handling. """ def test_default_and_specify(self): """ alias is present on the Attributes returned from attr.fields. If left unspecified, it defaults to standard private-attribute handling. If specified, it passes through the explicit alias. """ # alias is None by default on _CountingAttr default_counting = attr.ib() assert default_counting.alias is None override_counting = attr.ib(alias="specified") assert override_counting.alias == "specified" @attr.s class Cases: public_default = attr.ib() _private_default = attr.ib() __dunder_default__ = attr.ib() public_override = attr.ib(alias="public") _private_override = attr.ib(alias="_private") __dunder_override__ = attr.ib(alias="__dunder__") cases = attr.fields_dict(Cases) # Default applies private-name mangling logic assert cases["public_default"].name == "public_default" assert cases["public_default"].alias == "public_default" assert cases["_private_default"].name == "_private_default" assert cases["_private_default"].alias == "private_default" assert cases["__dunder_default__"].name == "__dunder_default__" assert cases["__dunder_default__"].alias == "dunder_default__" # Override is passed through assert cases["public_override"].name == "public_override" assert cases["public_override"].alias == "public" assert cases["_private_override"].name == "_private_override" assert cases["_private_override"].alias == "_private" assert cases["__dunder_override__"].name == "__dunder_override__" assert cases["__dunder_override__"].alias == "__dunder__" # And aliases are applied to the __init__ signature example = Cases( public_default=1, private_default=2, dunder_default__=3, public=4, _private=5, __dunder__=6, ) assert example.public_default == 1 assert example._private_default == 2 assert example.__dunder_default__ == 3 assert example.public_override == 4 assert example._private_override == 5 assert example.__dunder_override__ == 6 def test_evolve(self): """ attr.evolve uses Attribute.alias to determine parameter names. """ @attr.s class EvolveCase: _override = attr.ib(alias="_override") __mangled = attr.ib() __dunder__ = attr.ib() org = EvolveCase(1, 2, 3) # Previous behavior of evolve as broken for double-underscore # passthrough, and would raise here due to mis-mapping the __dunder__ # alias assert attr.evolve(org) == org # evolve uses the alias to match __init__ signature assert attr.evolve( org, _override=0, ) == EvolveCase(0, 2, 3) # and properly passes through dunders and mangles assert attr.evolve( org, EvolveCase__mangled=4, dunder__=5, ) == EvolveCase(1, 4, 5) 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. """ @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, unsafe_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, unsafe_hash=True) class C: x = attr.ib(eq=str) @attr.s(slots=slots, frozen=frozen, unsafe_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 unsafe_hash=True is passed, ignore __hash__. """ @attr.s(unsafe_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(PY_3_10_PLUS, 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 PY_3_10_PLUS, 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__