| # SPDX-License-Identifier: MIT | |
| """ | |
| Tests for `attr._make`. | |
| """ | |
| import copy | |
| import functools | |
| import gc | |
| import inspect | |
| import itertools | |
| import pickle | |
| import sys | |
| import types | |
| import unicodedata | |
| 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 | |
| import attrs | |
| from attr import _config | |
| from attr._make import ( | |
| Attribute, | |
| ClassProps, | |
| 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, False) | |
| ) | |
| @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], and_], | |
| ids=["single", "list", "and"], | |
| ) | |
| 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_converter_decorator_single(self): | |
| """ | |
| If _CountingAttr.converter is used as a decorator and there is no | |
| decorator set, the decorated method is used as the converter. | |
| """ | |
| a = attr.ib() | |
| @a.converter | |
| def v(self, value, field): | |
| pass | |
| assert isinstance(a._converter, attr.Converter) | |
| assert a._converter.takes_self | |
| assert a._converter.takes_field | |
| @pytest.mark.parametrize( | |
| "wrap", | |
| [lambda v: v, lambda v: [v], attr.converters.pipe], | |
| ids=["single", "list", "pipe"], | |
| ) | |
| def test_converter_decorator(self, wrap): | |
| """ | |
| If _CountingAttr.converter is used as a decorator and there is already | |
| a decorator set, the decorators are composed using `pipe`. | |
| """ | |
| def v(_): | |
| pass | |
| a = attr.ib(converter=wrap(v)) | |
| @a.converter | |
| def v2(self, value, field): | |
| pass | |
| assert isinstance(a._converter, attr.Converter) | |
| assert a._converter.takes_self | |
| assert a._converter.takes_field | |
| 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, ClassProps.KeywordOnly.NO, 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, ClassProps.KeywordOnly.NO, 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, ClassProps.KeywordOnly.NO, 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, ClassProps.KeywordOnly.NO, 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, ClassProps.KeywordOnly.NO, 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='y')" | |
| ), | |
| ) == e.value.args | |
| def test_kw_only(self): | |
| """ | |
| Converts all attributes in the current class if `kw_only` is provided. | |
| Base class attributes are **not** affected. | |
| """ | |
| @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, ClassProps.KeywordOnly.YES, True, None | |
| ) | |
| assert len(attrs) == 3 | |
| assert len(base_attrs) == 1 | |
| for a in attrs: | |
| if a.name == "b": | |
| assert a.kw_only is False | |
| else: | |
| assert a.kw_only is True | |
| attrs, base_attrs, _ = _transform_attrs( | |
| C, | |
| None, | |
| False, | |
| ClassProps.KeywordOnly.FORCE, | |
| 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, ClassProps.KeywordOnly.NO, 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_sets_attrs_props(self): | |
| """ | |
| Sets the `__attrs_props__` class attribute with the effective decorator | |
| properties. | |
| """ | |
| @attr.s( | |
| slots=True, | |
| frozen=True, | |
| repr=True, | |
| eq=True, | |
| order=True, | |
| unsafe_hash=True, | |
| init=True, | |
| match_args=False, | |
| kw_only=True, | |
| auto_attribs=True, | |
| cache_hash=True, | |
| str=True, | |
| ) | |
| class C: | |
| x: int = attr.ib() | |
| assert ClassProps( | |
| is_exception=False, | |
| is_slotted=True, | |
| is_frozen=True, | |
| added_init=True, | |
| added_repr=True, | |
| added_eq=True, | |
| added_ordering=True, | |
| hashability=ClassProps.Hashability.HASHABLE_CACHED, | |
| added_match_args=False, | |
| kw_only=ClassProps.KeywordOnly.FORCE, | |
| has_weakref_slot=True, | |
| collected_fields_by_mro=False, | |
| added_str=True, | |
| added_pickling=True, | |
| on_setattr_hook=None, | |
| field_transformer=None, | |
| ) == attrs.inspect(C) | |
| def test_sets_attrs_props_defaults(self): | |
| """ | |
| Default values are derived in `__attrs_props__` when not specified in | |
| the decorator. | |
| """ | |
| @attr.s | |
| class CDef: | |
| x = attr.ib() | |
| assert ClassProps( | |
| is_exception=False, | |
| is_slotted=False, | |
| is_frozen=False, | |
| added_init=True, | |
| added_repr=True, | |
| added_eq=True, | |
| added_ordering=True, | |
| hashability=ClassProps.Hashability.UNHASHABLE, | |
| added_match_args=True, | |
| kw_only=ClassProps.KeywordOnly.NO, | |
| has_weakref_slot=True, | |
| collected_fields_by_mro=False, | |
| added_str=False, | |
| added_pickling=False, | |
| on_setattr_hook=None, | |
| field_transformer=None, | |
| ) == attrs.inspect(CDef) | |
| 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_pre_init_with_mixture_of_defaults_and_kw_only(self): | |
| """ | |
| Attrs should properly handle a mixture of positional, positional with | |
| default, keyword-only, and keyword-only with default attributes when | |
| passing values to __attrs_pre_init__. | |
| """ | |
| g_val1 = None | |
| g_val2 = None | |
| g_val3 = None | |
| g_val4 = None | |
| g_val5 = None | |
| g_val6 = None | |
| @attr.define | |
| class MixtureClass: | |
| val1: int | |
| val2: int = 100 | |
| val3: int = attr.field(factory=int) | |
| val4: int = attr.field(kw_only=True) | |
| val5: int = attr.field(default=100, kw_only=True) | |
| val6: int = attr.field(factory=int, kw_only=True) | |
| def __attrs_pre_init__(self, val1, val2, val3, val4, val5, val6): | |
| nonlocal g_val1, g_val2, g_val3, g_val4, g_val5, g_val6 | |
| g_val1 = val1 | |
| g_val2 = val2 | |
| g_val3 = val3 | |
| g_val4 = val4 | |
| g_val5 = val5 | |
| g_val6 = val6 | |
| inst = MixtureClass( | |
| val1=200, val2=200, val3=200, val4=200, val5=200, val6=200 | |
| ) | |
| assert 200 == g_val1 == inst.val1 | |
| assert 200 == g_val2 == inst.val2 | |
| assert 200 == g_val3 == inst.val3 | |
| assert 200 == g_val4 == inst.val4 | |
| assert 200 == g_val5 == inst.val5 | |
| assert 200 == g_val6 == inst.val6 | |
| @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): | |
| """ | |
| When `force_kw_only=True`, 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) | |
| with pytest.raises(TypeError): | |
| C(0, y=1) | |
| c = C(x=0, y=1) | |
| assert c.x == 0 | |
| assert c.y == 1 | |
| @attr.s(kw_only=True, force_kw_only=False) | |
| class C(Base): | |
| y = attr.ib() | |
| c = C(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" | |
| def test_attr_level_kw_only_and_class_level_kw_only(self): | |
| """ | |
| If a field explicitly sets 'kw_only=False', it should not be made | |
| keyword-only even if the class sets 'kw_only=True'. | |
| """ | |
| @attr.s(kw_only=True) | |
| class OldClassOldBehavior: | |
| yes = attr.ib() | |
| no = attr.ib(kw_only=False) | |
| @attr.s(kw_only=True, force_kw_only=False) | |
| class OldClassNewBehavior: | |
| yes = attr.ib() | |
| no = attr.ib(kw_only=False) | |
| @attr.define(kw_only=True, force_kw_only=True) | |
| class NewClassOldBehavior: | |
| yes = attr.field() | |
| no = attr.field(kw_only=False) | |
| @attr.define(kw_only=True) | |
| class NewClassNewBehavior: | |
| yes = attr.field() | |
| no = attr.field(kw_only=False) | |
| for cls in [OldClassNewBehavior, NewClassNewBehavior]: | |
| fs = fields_dict(cls) | |
| assert fs["yes"].kw_only is True | |
| assert fs["no"].kw_only is False | |
| for cls in [OldClassOldBehavior, NewClassOldBehavior]: | |
| fs = fields_dict(cls) | |
| assert fs["yes"].kw_only is True | |
| assert fs["no"].kw_only is True | |
| def test_kw_only_inheritance(self): | |
| """ | |
| Comprehensive test about how `kw_only` works when there's multiple | |
| levels of inheritance with different `kw_only` settings. | |
| """ | |
| @attr.define() | |
| class A: | |
| a = attr.field() | |
| a_t = attr.field(kw_only=True) | |
| a_f = attr.field(kw_only=False) | |
| @attr.define(kw_only=True) | |
| class B(A): | |
| b = attr.field() | |
| b_t = attr.field(kw_only=True) | |
| b_f = attr.field(kw_only=False) | |
| @attr.define(kw_only=False) | |
| class C(B): | |
| c = attr.field() | |
| c_t = attr.field(kw_only=True) | |
| c_f = attr.field(kw_only=False) | |
| fs = fields_dict(A) | |
| assert fs["a"].kw_only is False | |
| assert fs["a_t"].kw_only is True | |
| assert fs["a_f"].kw_only is False | |
| fs = fields_dict(B) | |
| assert fs["a"].kw_only is False | |
| assert fs["a_t"].kw_only is True | |
| assert fs["a_f"].kw_only is False | |
| assert fs["b"].kw_only is True | |
| assert fs["b_t"].kw_only is True | |
| assert fs["b_f"].kw_only is False | |
| fs = fields_dict(C) | |
| assert fs["a"].kw_only is False | |
| assert fs["a_t"].kw_only is True | |
| assert fs["a_f"].kw_only is False | |
| assert fs["b"].kw_only is True | |
| assert fs["b_t"].kw_only is True | |
| assert fs["b_f"].kw_only is False | |
| assert fs["c"].kw_only is False | |
| assert fs["c_t"].kw_only is True | |
| assert fs["c_f"].kw_only is False | |
| def test_kw_only_inheritance_force_kw_only(self): | |
| """ | |
| Similar to above, but when `force_kw_only` (pre-25.3.0 behavior). | |
| """ | |
| @attr.define(force_kw_only=True) | |
| class A: | |
| a = attr.field() | |
| a_t = attr.field(kw_only=True) | |
| a_f = attr.field(kw_only=False) | |
| @attr.define(kw_only=True, force_kw_only=True) | |
| class B(A): | |
| b = attr.field() | |
| b_t = attr.field(kw_only=True) | |
| b_f = attr.field(kw_only=False) | |
| @attr.define(kw_only=False, force_kw_only=True) | |
| class C(B): | |
| c = attr.field() | |
| c_t = attr.field(kw_only=True) | |
| c_f = attr.field(kw_only=False) | |
| fs = fields_dict(A) | |
| assert fs["a"].kw_only is False | |
| assert fs["a_t"].kw_only is True | |
| assert fs["a_f"].kw_only is False | |
| fs = fields_dict(B) | |
| assert fs["a"].kw_only is True | |
| assert fs["a_t"].kw_only is True | |
| assert fs["a_f"].kw_only is True | |
| assert fs["b"].kw_only is True | |
| assert fs["b_t"].kw_only is True | |
| assert fs["b_f"].kw_only is True | |
| fs = fields_dict(C) | |
| assert fs["a"].kw_only is False | |
| assert fs["a_t"].kw_only is True | |
| assert fs["a_f"].kw_only is False | |
| assert fs["b"].kw_only is True | |
| assert fs["b_t"].kw_only is True | |
| assert fs["b_f"].kw_only is True | |
| assert fs["c"].kw_only is False | |
| assert fs["c_t"].kw_only is True | |
| assert fs["c_f"].kw_only is False | |
| @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("<tests.test_make.C object at 0x") | |
| def test_normalized_unicode_attr_args(self): | |
| """ | |
| Unicode identifiers are valid in Python. | |
| """ | |
| clsname = "ü" | |
| assert clsname == unicodedata.normalize("NFKC", clsname) | |
| attrname = "ß" | |
| assert attrname == unicodedata.normalize("NFKC", attrname) | |
| C = make_class(clsname, [attrname], repr=False) | |
| assert repr(C(1)).startswith("<tests.test_make.ü object at 0x") | |
| kwargs = {"ß": 1} | |
| c = C(**kwargs) | |
| assert 1 == c.ß | |
| def test_unnormalized_unicode_attr_args(self): | |
| """ | |
| Unicode identifiers are normalized to NFKC form in Python. | |
| """ | |
| clsname = "Ŀ" | |
| assert clsname != unicodedata.normalize("NFKC", clsname) | |
| attrname = "ㅁ" | |
| assert attrname != unicodedata.normalize("NFKC", attrname) | |
| C = make_class(clsname, [attrname], repr=False) | |
| assert repr(C(1)).startswith("<tests.test_make.L· object at 0x") | |
| kwargs = {unicodedata.normalize("NFKC", attrname): 1} | |
| c = C(**kwargs) | |
| assert 1 == c.ㅁ | |
| def test_catches_wrong_attrs_type(self): | |
| """ | |
| Raise `TypeError` if an invalid type for attrs is passed. | |
| """ | |
| with pytest.raises(TypeError) as e: | |
| make_class("C", object()) | |
| assert ("attrs argument must be a dict or a list.",) == e.value.args | |
| def test_bases(self): | |
| """ | |
| Parameter bases default to (object,) and subclasses correctly | |
| """ | |
| class D: | |
| pass | |
| cls = make_class("C", {}) | |
| assert cls.__mro__[-1] is object | |
| cls = make_class("C", {}, bases=(D,)) | |
| assert D in cls.__mro__ | |
| assert isinstance(cls(), D) | |
| def test_additional_class_body(self): | |
| """ | |
| Additional class_body is added to newly created class. | |
| """ | |
| def echo_func(cls, *args): | |
| return args | |
| cls = make_class("C", {}, class_body={"echo": classmethod(echo_func)}) | |
| assert ("a", "b") == cls.echo("a", "b") | |
| def test_clean_class(self, slots): | |
| """ | |
| Attribute definitions do not appear on the class body. | |
| """ | |
| C = make_class("C", ["x"], slots=slots) | |
| x = getattr(C, "x", None) | |
| assert not isinstance(x, _CountingAttr) | |
| def test_missing_sys_getframe(self, monkeypatch): | |
| """ | |
| `make_class()` does not fail when `sys._getframe()` is not available. | |
| """ | |
| monkeypatch.delattr(sys, "_getframe") | |
| C = make_class("C", ["x"]) | |
| assert 1 == len(C.__attrs_attrs__) | |
| def test_make_class_ordered(self): | |
| """ | |
| If `make_class()` is passed ordered attrs, their order is respected | |
| instead of the counter. | |
| """ | |
| b = attr.ib(default=2) | |
| a = attr.ib(default=1) | |
| C = attr.make_class("C", {"a": a, "b": b}) | |
| assert "C(a=1, b=2)" == repr(C()) | |
| def test_generic_dynamic_class(self): | |
| """ | |
| make_class can create generic dynamic classes. | |
| https://github.com/python-attrs/attrs/issues/756 | |
| https://bugs.python.org/issue33188 | |
| """ | |
| from types import new_class | |
| from typing import Generic, TypeVar | |
| MyTypeVar = TypeVar("MyTypeVar") | |
| MyParent = new_class("MyParent", (Generic[MyTypeVar],), {}) | |
| attr.make_class("test", {"id": attr.ib(type=str)}, (MyParent[int],)) | |
| def test_annotations(self): | |
| """ | |
| make_class fills the __annotations__ dict for attributes with a known | |
| type. | |
| """ | |
| a = attr.ib(type=bool) | |
| b = attr.ib( | |
| type=None | |
| ) # Won't be added to ann. b/c of unfavorable default | |
| c = attr.ib() | |
| C = attr.make_class("C", {"a": a, "b": b, "c": c}) | |
| C = attr.resolve_types(C) | |
| assert {"a": bool} == C.__annotations__ | |
| def test_annotations_resolve(self): | |
| """ | |
| resolve_types() resolves the annotations added by make_class(). | |
| """ | |
| a = attr.ib(type="bool") | |
| C = attr.make_class("C", {"a": a}) | |
| C = attr.resolve_types(C) | |
| assert attr.fields(C).a.type is bool | |
| assert {"a": "bool"} == C.__annotations__ | |
| class TestFields: | |
| """ | |
| Tests for `fields`. | |
| """ | |
| @given(simple_classes()) | |
| def test_instance(self, C): | |
| """ | |
| Returns the class fields for *attrs* instances too. | |
| """ | |
| assert fields(C()) is fields(C) | |
| def test_handler_non_attrs_instance(self): | |
| """ | |
| Raises `TypeError` on non-*attrs* instances. | |
| """ | |
| with pytest.raises( | |
| TypeError, | |
| match=r"Passed object must be a class or attrs instance\.", | |
| ): | |
| fields(object()) | |
| def test_handler_non_attrs_class(self): | |
| """ | |
| Raises `ValueError` if passed a non-*attrs* instance. | |
| """ | |
| with pytest.raises(NotAnAttrsClassError) as e: | |
| fields(object) | |
| assert ( | |
| f"{object!r} is not an attrs-decorated class." | |
| ) == e.value.args[0] | |
| def test_handler_non_attrs_generic_class(self): | |
| """ | |
| Raises `ValueError` if passed a non-*attrs* generic class. | |
| """ | |
| T = TypeVar("T") | |
| class B(Generic[T]): | |
| pass | |
| with pytest.raises(NotAnAttrsClassError) as e: | |
| fields(B[str]) | |
| assert ( | |
| f"{B[str]!r} is not an attrs-decorated class." | |
| ) == e.value.args[0] | |
| @given(simple_classes()) | |
| def test_fields(self, C): | |
| """ | |
| Returns a list of `Attribute`a. | |
| """ | |
| assert all(isinstance(a, Attribute) for a in fields(C)) | |
| @given(simple_classes()) | |
| def test_fields_properties(self, C): | |
| """ | |
| Fields returns a tuple with properties. | |
| """ | |
| for attribute in fields(C): | |
| assert getattr(fields(C), attribute.name) is attribute | |
| def test_generics(self): | |
| """ | |
| Fields work with generic classes. | |
| """ | |
| T = TypeVar("T") | |
| @attr.define | |
| class A(Generic[T]): | |
| a: T | |
| assert len(fields(A)) == 1 | |
| assert fields(A).a.name == "a" | |
| assert fields(A).a.default is attr.NOTHING | |
| assert len(fields(A[str])) == 1 | |
| assert fields(A[str]).a.name == "a" | |
| assert fields(A[str]).a.default is attr.NOTHING | |
| class TestFieldsDict: | |
| """ | |
| Tests for `fields_dict`. | |
| """ | |
| @given(simple_classes()) | |
| def test_instance(self, C): | |
| """ | |
| Raises `TypeError` on non-classes. | |
| """ | |
| with pytest.raises(TypeError) as e: | |
| fields_dict(C()) | |
| assert "Passed object must be a class." == e.value.args[0] | |
| def test_handler_non_attrs_class(self): | |
| """ | |
| Raises `ValueError` if passed a non-*attrs* instance. | |
| """ | |
| with pytest.raises(NotAnAttrsClassError) as e: | |
| fields_dict(object) | |
| assert ( | |
| f"{object!r} is not an attrs-decorated class." | |
| ) == e.value.args[0] | |
| @given(simple_classes()) | |
| def test_fields_dict(self, C): | |
| """ | |
| Returns an ordered dict of ``{attribute_name: Attribute}``. | |
| """ | |
| d = fields_dict(C) | |
| assert isinstance(d, dict) | |
| assert list(fields(C)) == list(d.values()) | |
| assert [a.name for a in fields(C)] == list(d) | |
| class TestConverter: | |
| """ | |
| Tests for attribute conversion. | |
| """ | |
| def test_converter(self): | |
| """ | |
| Return value of converter is used as the attribute's value. | |
| """ | |
| C = make_class( | |
| "C", {"x": attr.ib(converter=lambda v: v + 1), "y": attr.ib()} | |
| ) | |
| c = C(1, 2) | |
| assert c.x == 2 | |
| assert c.y == 2 | |
| def test_converter_wrapped_takes_self(self): | |
| """ | |
| When wrapped and passed `takes_self`, the converter receives the | |
| instance that's being initializes -- and the return value is used as | |
| the field's value. | |
| """ | |
| def converter_with_self(v, self_): | |
| return v * self_.y | |
| @attr.define | |
| class C: | |
| x: int = attr.field( | |
| converter=attr.Converter(converter_with_self, takes_self=True) | |
| ) | |
| y = 42 | |
| assert 84 == C(2).x | |
| def test_converter_wrapped_takes_field(self): | |
| """ | |
| When wrapped and passed `takes_field`, the converter receives the field | |
| definition -- and the return value is used as the field's value. | |
| """ | |
| def converter_with_field(v, field): | |
| assert isinstance(field, attr.Attribute) | |
| return v * field.metadata["x"] | |
| @attr.define | |
| class C: | |
| x: int = attr.field( | |
| converter=attr.Converter( | |
| converter_with_field, takes_field=True | |
| ), | |
| metadata={"x": 42}, | |
| ) | |
| assert 84 == C(2).x | |
| def test_converter_decorated(self): | |
| """ | |
| Same as Converter with both `takes_field` and `takes_self` | |
| """ | |
| @attr.define | |
| class C: | |
| factor: int = 5 | |
| x: int = attr.field(default=0, metadata={"offset": 200}) | |
| @x.converter | |
| def _convert_x(self, field, value): | |
| assert isinstance(field, attr.Attribute) | |
| return int(value) * self.factor + field.metadata["offset"] | |
| assert 410 == C(x="42").x | |
| @given(integers(), booleans()) | |
| def test_convert_property(self, val, init): | |
| """ | |
| Property tests for attributes using converter. | |
| """ | |
| C = make_class( | |
| "C", | |
| { | |
| "y": attr.ib(), | |
| "x": attr.ib( | |
| init=init, default=val, converter=lambda v: v + 1 | |
| ), | |
| }, | |
| ) | |
| c = C(2) | |
| assert c.x == val + 1 | |
| assert c.y == 2 | |
| @given(integers(), booleans()) | |
| def test_converter_factory_property(self, val, init): | |
| """ | |
| Property tests for attributes with converter, and a factory default. | |
| """ | |
| C = make_class( | |
| "C", | |
| { | |
| "y": attr.ib(), | |
| "x": attr.ib( | |
| init=init, | |
| default=Factory(lambda: val), | |
| converter=lambda v: v + 1, | |
| ), | |
| }, | |
| ) | |
| c = C(2) | |
| assert c.x == val + 1 | |
| assert c.y == 2 | |
| def test_convert_before_validate(self): | |
| """ | |
| Validation happens after conversion. | |
| """ | |
| def validator(inst, attr, val): | |
| raise RuntimeError("foo") | |
| C = make_class( | |
| "C", | |
| { | |
| "x": attr.ib(validator=validator, converter=lambda v: 1 / 0), | |
| "y": attr.ib(), | |
| }, | |
| ) | |
| with pytest.raises(ZeroDivisionError): | |
| C(1, 2) | |
| def test_frozen(self): | |
| """ | |
| Converters circumvent immutability. | |
| """ | |
| C = make_class("C", {"x": attr.ib(converter=int)}, frozen=True) | |
| C("1") | |
| class TestValidate: | |
| """ | |
| Tests for `validate`. | |
| """ | |
| def test_success(self): | |
| """ | |
| If the validator succeeds, nothing gets raised. | |
| """ | |
| C = make_class( | |
| "C", {"x": attr.ib(validator=lambda *a: None), "y": attr.ib()} | |
| ) | |
| validate(C(1, 2)) | |
| def test_propagates(self): | |
| """ | |
| The exception of the validator is handed through. | |
| """ | |
| def raiser(_, __, value): | |
| if value == 42: | |
| raise FloatingPointError | |
| C = make_class("C", {"x": attr.ib(validator=raiser)}) | |
| i = C(1) | |
| i.x = 42 | |
| with pytest.raises(FloatingPointError): | |
| validate(i) | |
| def test_run_validators(self): | |
| """ | |
| Setting `_run_validators` to False prevents validators from running. | |
| """ | |
| _config._run_validators = False | |
| obj = object() | |
| def raiser(_, __, ___): | |
| raise Exception(obj) | |
| C = make_class("C", {"x": attr.ib(validator=raiser)}) | |
| c = C(1) | |
| validate(c) | |
| assert 1 == c.x | |
| _config._run_validators = True | |
| with pytest.raises(Exception): | |
| validate(c) | |
| with pytest.raises(Exception) as e: | |
| C(1) | |
| assert (obj,) == e.value.args | |
| def test_multiple_validators(self): | |
| """ | |
| If a list is passed as a validator, all of its items are treated as one | |
| and must pass. | |
| """ | |
| def v1(_, __, value): | |
| if value == 23: | |
| raise TypeError("omg") | |
| def v2(_, __, value): | |
| if value == 42: | |
| raise ValueError("omg") | |
| C = make_class("C", {"x": attr.ib(validator=[v1, v2])}) | |
| validate(C(1)) | |
| with pytest.raises(TypeError) as e: | |
| C(23) | |
| assert "omg" == e.value.args[0] | |
| with pytest.raises(ValueError) as e: | |
| C(42) | |
| assert "omg" == e.value.args[0] | |
| def test_multiple_empty(self): | |
| """ | |
| Empty list/tuple for validator is the same as None. | |
| """ | |
| C1 = make_class("C", {"x": attr.ib(validator=[])}) | |
| C2 = make_class("C", {"x": attr.ib(validator=None)}) | |
| assert inspect.getsource(C1.__init__) == inspect.getsource(C2.__init__) | |
| # Hypothesis seems to cache values, so the lists of attributes come out | |
| # unsorted. | |
| sorted_lists_of_attrs = list_of_attrs.map( | |
| lambda ln: sorted(ln, key=attrgetter("counter")) | |
| ) | |
| class TestMetadata: | |
| """ | |
| Tests for metadata handling. | |
| """ | |
| @given(sorted_lists_of_attrs) | |
| def test_metadata_present(self, list_of_attrs): | |
| """ | |
| Assert dictionaries are copied and present. | |
| """ | |
| C = make_class( | |
| "C", dict(zip(gen_attr_names(), list_of_attrs, strict=False)) | |
| ) | |
| for hyp_attr, class_attr in zip(list_of_attrs, fields(C), strict=True): | |
| if hyp_attr.metadata is None: | |
| # The default is a singleton empty dict. | |
| assert class_attr.metadata is not None | |
| assert len(class_attr.metadata) == 0 | |
| else: | |
| assert hyp_attr.metadata == class_attr.metadata | |
| # Once more, just to assert getting items and iteration. | |
| for k in class_attr.metadata: | |
| assert hyp_attr.metadata[k] == class_attr.metadata[k] | |
| assert hyp_attr.metadata.get(k) == class_attr.metadata.get( | |
| k | |
| ) | |
| @given(simple_classes(), text()) | |
| def test_metadata_immutability(self, C, string): | |
| """ | |
| The metadata dict should be best-effort immutable. | |
| """ | |
| for a in fields(C): | |
| with pytest.raises(TypeError): | |
| a.metadata[string] = string | |
| with pytest.raises(AttributeError): | |
| a.metadata.update({string: string}) | |
| with pytest.raises(AttributeError): | |
| a.metadata.clear() | |
| with pytest.raises(AttributeError): | |
| a.metadata.setdefault(string, string) | |
| for k in a.metadata: | |
| # For some reason, MappingProxyType throws an IndexError for | |
| # deletes on a large integer key. | |
| with pytest.raises((TypeError, IndexError)): | |
| del a.metadata[k] | |
| with pytest.raises(AttributeError): | |
| a.metadata.pop(k) | |
| with pytest.raises(AttributeError): | |
| a.metadata.popitem() | |
| @given(lists(simple_attrs_without_metadata, min_size=2, max_size=5)) | |
| def test_empty_metadata_singleton(self, list_of_attrs): | |
| """ | |
| All empty metadata attributes share the same empty metadata dict. | |
| """ | |
| C = make_class( | |
| "C", dict(zip(gen_attr_names(), list_of_attrs, strict=False)) | |
| ) | |
| for a in fields(C)[1:]: | |
| assert a.metadata is fields(C)[0].metadata | |
| @given(lists(simple_attrs_without_metadata, min_size=2, max_size=5)) | |
| def test_empty_countingattr_metadata_independent(self, list_of_attrs): | |
| """ | |
| All empty metadata attributes are independent before ``@attr.s``. | |
| """ | |
| for x, y in itertools.combinations(list_of_attrs, 2): | |
| assert x.metadata is not y.metadata | |
| @given(lists(simple_attrs_with_metadata(), min_size=2, max_size=5)) | |
| def test_not_none_metadata(self, list_of_attrs): | |
| """ | |
| Non-empty metadata attributes exist as fields after ``@attr.s``. | |
| """ | |
| C = make_class( | |
| "C", dict(zip(gen_attr_names(), list_of_attrs, strict=False)) | |
| ) | |
| assert len(fields(C)) > 0 | |
| for cls_a, raw_a in zip(fields(C), list_of_attrs, strict=True): | |
| 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, | |
| ClassProps( | |
| is_exception=False, | |
| is_slotted=True, | |
| is_frozen=True, | |
| added_init=True, | |
| added_repr=True, | |
| added_eq=True, | |
| added_ordering=False, | |
| hashability=ClassProps.Hashability.UNHASHABLE, | |
| added_match_args=True, | |
| kw_only=ClassProps.KeywordOnly.NO, | |
| has_weakref_slot=False, | |
| collected_fields_by_mro=True, | |
| added_str=False, | |
| added_pickling=True, | |
| on_setattr_hook=None, | |
| field_transformer=None, | |
| ), | |
| has_custom_setattr=False, | |
| ) | |
| 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, | |
| False, | |
| ClassProps( | |
| is_exception=False, | |
| is_slotted=True, | |
| is_frozen=True, | |
| added_init=True, | |
| added_repr=True, | |
| added_eq=True, | |
| added_ordering=False, | |
| hashability=ClassProps.Hashability.UNHASHABLE, | |
| added_match_args=True, | |
| kw_only=ClassProps.KeywordOnly.NO, | |
| has_weakref_slot=False, | |
| collected_fields_by_mro=True, | |
| added_str=False, | |
| added_pickling=True, | |
| on_setattr_hook=None, | |
| field_transformer=None, | |
| ), | |
| has_custom_setattr=False, | |
| ) | |
| 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__", | |
| "__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, monkeypatch: pytest.MonkeyPatch | |
| ): | |
| """ | |
| If the class hasn't a __module__ or __qualname__, the method hasn't | |
| either. | |
| """ | |
| class C: | |
| pass | |
| orig_hasattr = __builtins__["hasattr"] | |
| def our_hasattr(obj, name, /) -> bool: | |
| if name in ("__module__", "__qualname__"): | |
| return False | |
| return orig_hasattr(obj, name) | |
| monkeypatch.setitem( | |
| _ClassBuilder.__init__.__globals__["__builtins__"], | |
| "hasattr", | |
| our_hasattr, | |
| ) | |
| b = _ClassBuilder( | |
| C, | |
| these=None, | |
| auto_attribs=False, | |
| props=ClassProps( | |
| is_exception=False, | |
| is_slotted=False, | |
| is_frozen=False, | |
| added_init=True, | |
| added_repr=True, | |
| added_eq=True, | |
| added_ordering=False, | |
| hashability=ClassProps.Hashability.UNHASHABLE, | |
| added_match_args=True, | |
| kw_only=ClassProps.KeywordOnly.NO, | |
| has_weakref_slot=True, | |
| collected_fields_by_mro=True, | |
| added_str=False, | |
| added_pickling=True, | |
| on_setattr_hook=None, | |
| field_transformer=None, | |
| ), | |
| has_custom_setattr=False, | |
| ) | |
| def fake_meth(self): | |
| pass | |
| fake_meth.__module__ = "42" | |
| fake_meth.__qualname__ = "23" | |
| b._cls = {} # No module and qualname | |
| 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 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, strict=True)) | |
| 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) | |
| def test_alias_is_default(self): | |
| """ | |
| alias_is_default is True for auto-generated aliases and False for | |
| explicitly provided ones -- even if the explicit value matches the | |
| auto-generated default. | |
| """ | |
| @attrs.define | |
| class C: | |
| auto: int | |
| _private_auto: int | |
| explicit: int = attrs.field(alias="custom") | |
| _matches_default: int = attrs.field(alias="matches_default") | |
| fields = attr.fields_dict(C) | |
| assert fields["auto"].alias_is_default is True | |
| assert fields["_private_auto"].alias_is_default is True | |
| assert fields["explicit"].alias_is_default is False | |
| assert fields["_matches_default"].alias_is_default is False | |
| def test_alias_is_default_pickle_roundtrip(self): | |
| """ | |
| alias_is_default survives pickle round-tripping. | |
| """ | |
| @attrs.define | |
| class C: | |
| auto: int | |
| explicit: int = attrs.field(alias="custom") | |
| fields = attr.fields(C) | |
| for a in fields: | |
| restored = pickle.loads(pickle.dumps(a)) | |
| assert a.alias == restored.alias | |
| assert a.alias_is_default == restored.alias_is_default | |
| X = ( | |
| b"\x80\x05\x95_\x00\x00\x00\x00\x00\x00\x00\x8c\nattr._make\x94\x8c\tAttrib" | |
| b"ute\x94\x93\x94)\x81\x94(\x8c\x01x\x94h\x00\x8c\x08_Nothing\x94\x93" | |
| b"\x94K\x01\x85\x94R\x94N\x88\x88N\x88NN\x88}\x94\x8c\x08builtins\x94" | |
| b"\x8c\x03int\x94\x93\x94N\x89\x89Nh\x04t\x94b." | |
| ) | |
| Y = ( | |
| b"\x80\x05\x95b\x00\x00\x00\x00\x00\x00\x00\x8c\nattr._make\x94\x8c\tAttrib" | |
| b"ute\x94\x93\x94)\x81\x94(\x8c\x02_y\x94h\x00\x8c\x08_Nothing\x94" | |
| b"\x93\x94K\x01\x85\x94R\x94N\x88\x88N\x88NN\x88}\x94\x8c\x08builtins" | |
| b"\x94\x8c\x03int\x94\x93\x94N\x89\x89N\x8c\x01y\x94t\x94b." | |
| ) | |
| Z = ( | |
| b"\x80\x05\x95c\x00\x00\x00\x00\x00\x00\x00\x8c\nattr._make\x94\x8c\tAttrib" | |
| b"ute\x94\x93\x94)\x81\x94(\x8c\x01z\x94h\x00\x8c\x08_Nothing\x94\x93" | |
| b"\x94K\x01\x85\x94R\x94N\x88\x88N\x88NN\x88}\x94\x8c\x08builtins\x94" | |
| b"\x8c\x03int\x94\x93\x94N\x89\x89N\x8c\x03_z_\x94t\x94b." | |
| ) | |
| @pytest.mark.parametrize( | |
| ("pickle_data", "name", "alias", "alias_is_default"), | |
| [ | |
| (X, "x", "x", True), | |
| (Y, "_y", "y", True), | |
| (Z, "z", "_z_", False), | |
| ], | |
| ) | |
| def test_can_unpickle_25_3_attributes( | |
| self, pickle_data, name, alias, alias_is_default | |
| ): | |
| """ | |
| Can unpickle attributes created in 25.3. | |
| """ | |
| assert Attribute( | |
| name=name, | |
| alias=alias, | |
| alias_is_default=alias_is_default, | |
| default=attrs.NOTHING, | |
| validator=None, | |
| repr=True, | |
| cmp=None, | |
| eq=True, | |
| eq_key=None, | |
| order=True, | |
| order_key=None, | |
| hash=None, | |
| init=True, | |
| metadata=types.MappingProxyType({}), | |
| type=int, | |
| converter=None, | |
| kw_only=False, | |
| inherited=False, | |
| on_setattr=None, | |
| ) == pickle.loads(pickle_data) | |
| 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__", | |
| "__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 | |
| ) | |
| 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__ |