| # SPDX-License-Identifier: MIT |
| |
| """ |
| Unit tests for slots-related functionality. |
| """ |
| |
| import functools |
| import pickle |
| import weakref |
| |
| from unittest import mock |
| |
| import pytest |
| |
| import attr |
| import attrs |
| |
| from attr._compat import PY_3_14_PLUS, PYPY |
| |
| |
| # Pympler doesn't work on PyPy. |
| try: |
| from pympler.asizeof import asizeof |
| |
| has_pympler = True |
| except BaseException: # Won't be an import error. # noqa: BLE001 |
| has_pympler = False |
| |
| |
| @attr.s |
| class C1: |
| x = attr.ib(validator=attr.validators.instance_of(int)) |
| y = attr.ib() |
| |
| def method(self): |
| return self.x |
| |
| @classmethod |
| def classmethod(cls): |
| return "clsmethod" |
| |
| @staticmethod |
| def staticmethod(): |
| return "staticmethod" |
| |
| def my_class(self): |
| return __class__ |
| |
| def my_super(self): |
| """Just to test out the no-arg super.""" |
| return super().__repr__() |
| |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class C1Slots: |
| x = attr.ib(validator=attr.validators.instance_of(int)) |
| y = attr.ib() |
| |
| def method(self): |
| return self.x |
| |
| @classmethod |
| def classmethod(cls): |
| return "clsmethod" |
| |
| @staticmethod |
| def staticmethod(): |
| return "staticmethod" |
| |
| def my_class(self): |
| return __class__ |
| |
| def my_super(self): |
| """Just to test out the no-arg super.""" |
| return super().__repr__() |
| |
| |
| def test_slots_being_used(): |
| """ |
| The class is really using __slots__. |
| """ |
| non_slot_instance = C1(x=1, y="test") |
| slot_instance = C1Slots(x=1, y="test") |
| |
| assert "__dict__" not in dir(slot_instance) |
| assert "__slots__" in dir(slot_instance) |
| |
| assert "__dict__" in dir(non_slot_instance) |
| assert "__slots__" not in dir(non_slot_instance) |
| |
| assert {"__weakref__", "x", "y"} == set(slot_instance.__slots__) |
| |
| if has_pympler: |
| assert asizeof(slot_instance) < asizeof(non_slot_instance) |
| |
| non_slot_instance.t = "test" |
| with pytest.raises(AttributeError): |
| slot_instance.t = "test" |
| |
| assert 1 == non_slot_instance.method() |
| assert 1 == slot_instance.method() |
| |
| assert attr.fields(C1Slots) == attr.fields(C1) |
| assert attr.asdict(slot_instance) == attr.asdict(non_slot_instance) |
| |
| |
| def test_slots_base_of_slotted(): |
| """ |
| The (hopefully gc'ed) temporary base class of a slotted class contains a |
| reference to the slotted class. |
| """ |
| |
| class Base: |
| pass |
| |
| Slotted = attr.s(slots=True)(Base) |
| |
| assert Slotted is Base.__attrs_base_of_slotted__() |
| |
| |
| def test_basic_attr_funcs(): |
| """ |
| Comparison, `__eq__`, `__hash__`, `__repr__`, `attrs.asdict` work. |
| """ |
| a = C1Slots(x=1, y=2) |
| b = C1Slots(x=1, y=3) |
| a_ = C1Slots(x=1, y=2) |
| |
| # Comparison. |
| assert b > a |
| |
| assert a_ == a |
| |
| # Hashing. |
| hash(b) # Just to assert it doesn't raise. |
| |
| # Repr. |
| assert "C1Slots(x=1, y=2)" == repr(a) |
| |
| assert {"x": 1, "y": 2} == attr.asdict(a) |
| |
| |
| def test_inheritance_from_nonslots(): |
| """ |
| Inheritance from a non-slotted class works. |
| |
| Note that a slotted class inheriting from an ordinary class loses most of |
| the benefits of slotted classes, but it should still work. |
| """ |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class C2Slots(C1): |
| z = attr.ib() |
| |
| c2 = C2Slots(x=1, y=2, z="test") |
| |
| assert 1 == c2.x |
| assert 2 == c2.y |
| assert "test" == c2.z |
| |
| c2.t = "test" # This will work, using the base class. |
| |
| assert "test" == c2.t |
| |
| assert 1 == c2.method() |
| assert "clsmethod" == c2.classmethod() |
| assert "staticmethod" == c2.staticmethod() |
| |
| assert {"z"} == set(C2Slots.__slots__) |
| |
| c3 = C2Slots(x=1, y=3, z="test") |
| |
| assert c3 > c2 |
| |
| c2_ = C2Slots(x=1, y=2, z="test") |
| |
| assert c2 == c2_ |
| |
| assert "C2Slots(x=1, y=2, z='test')" == repr(c2) |
| |
| hash(c2) # Just to assert it doesn't raise. |
| |
| assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) |
| |
| |
| def test_nonslots_these(): |
| """ |
| Enhancing a dict class using 'these' works. |
| |
| This will actually *replace* the class with another one, using slots. |
| """ |
| |
| class SimpleOrdinaryClass: |
| def __init__(self, x, y, z): |
| self.x = x |
| self.y = y |
| self.z = z |
| |
| def method(self): |
| return self.x |
| |
| @classmethod |
| def classmethod(cls): |
| return "clsmethod" |
| |
| @staticmethod |
| def staticmethod(): |
| return "staticmethod" |
| |
| C2Slots = attr.s( |
| these={"x": attr.ib(), "y": attr.ib(), "z": attr.ib()}, |
| init=False, |
| slots=True, |
| unsafe_hash=True, |
| )(SimpleOrdinaryClass) |
| |
| c2 = C2Slots(x=1, y=2, z="test") |
| assert 1 == c2.x |
| assert 2 == c2.y |
| assert "test" == c2.z |
| with pytest.raises(AttributeError): |
| c2.t = "test" # We have slots now. |
| |
| assert 1 == c2.method() |
| assert "clsmethod" == c2.classmethod() |
| assert "staticmethod" == c2.staticmethod() |
| |
| assert {"__weakref__", "x", "y", "z"} == set(C2Slots.__slots__) |
| |
| c3 = C2Slots(x=1, y=3, z="test") |
| assert c3 > c2 |
| c2_ = C2Slots(x=1, y=2, z="test") |
| assert c2 == c2_ |
| |
| assert "SimpleOrdinaryClass(x=1, y=2, z='test')" == repr(c2) |
| |
| hash(c2) # Just to assert it doesn't raise. |
| |
| assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) |
| |
| |
| def test_inheritance_from_slots(): |
| """ |
| Inheriting from an attrs slotted class works. |
| """ |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class C2Slots(C1Slots): |
| z = attr.ib() |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class C2(C1): |
| z = attr.ib() |
| |
| c2 = C2Slots(x=1, y=2, z="test") |
| assert 1 == c2.x |
| assert 2 == c2.y |
| assert "test" == c2.z |
| |
| assert {"z"} == set(C2Slots.__slots__) |
| |
| assert 1 == c2.method() |
| assert "clsmethod" == c2.classmethod() |
| assert "staticmethod" == c2.staticmethod() |
| |
| with pytest.raises(AttributeError): |
| c2.t = "test" |
| |
| non_slot_instance = C2(x=1, y=2, z="test") |
| if has_pympler: |
| assert asizeof(c2) < asizeof(non_slot_instance) |
| |
| c3 = C2Slots(x=1, y=3, z="test") |
| assert c3 > c2 |
| c2_ = C2Slots(x=1, y=2, z="test") |
| assert c2 == c2_ |
| |
| assert "C2Slots(x=1, y=2, z='test')" == repr(c2) |
| |
| hash(c2) # Just to assert it doesn't raise. |
| |
| assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) |
| |
| |
| def test_inheritance_from_slots_with_attribute_override(): |
| """ |
| Inheriting from a slotted class doesn't re-create existing slots |
| """ |
| |
| class HasXSlot: |
| __slots__ = ("x",) |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class C2Slots(C1Slots): |
| # y re-defined here but it shouldn't get a slot |
| y = attr.ib() |
| z = attr.ib() |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class NonAttrsChild(HasXSlot): |
| # Parent class has slot for "x" already, so we skip it |
| x = attr.ib() |
| y = attr.ib() |
| z = attr.ib() |
| |
| c2 = C2Slots(1, 2, "test") |
| assert 1 == c2.x |
| assert 2 == c2.y |
| assert "test" == c2.z |
| |
| assert {"z"} == set(C2Slots.__slots__) |
| |
| na = NonAttrsChild(1, 2, "test") |
| assert 1 == na.x |
| assert 2 == na.y |
| assert "test" == na.z |
| |
| assert {"__weakref__", "y", "z"} == set(NonAttrsChild.__slots__) |
| |
| |
| def test_inherited_slot_reuses_slot_descriptor(): |
| """ |
| We reuse slot descriptor for an attr.ib defined in a slotted attr.s |
| """ |
| |
| class HasXSlot: |
| __slots__ = ("x",) |
| |
| class OverridesX(HasXSlot): |
| @property |
| def x(self): |
| return None |
| |
| @attr.s(slots=True) |
| class Child(OverridesX): |
| x = attr.ib() |
| |
| assert Child.x is not OverridesX.x |
| assert Child.x is HasXSlot.x |
| |
| c = Child(1) |
| assert 1 == c.x |
| assert set() == set(Child.__slots__) |
| |
| ox = OverridesX() |
| assert ox.x is None |
| |
| |
| def test_bare_inheritance_from_slots(): |
| """ |
| Inheriting from a bare attrs slotted class works. |
| """ |
| |
| @attr.s( |
| init=False, |
| eq=False, |
| order=False, |
| unsafe_hash=False, |
| repr=False, |
| slots=True, |
| ) |
| class C1BareSlots: |
| x = attr.ib(validator=attr.validators.instance_of(int)) |
| y = attr.ib() |
| |
| def method(self): |
| return self.x |
| |
| @classmethod |
| def classmethod(cls): |
| return "clsmethod" |
| |
| @staticmethod |
| def staticmethod(): |
| return "staticmethod" |
| |
| @attr.s(init=False, eq=False, order=False, unsafe_hash=False, repr=False) |
| class C1Bare: |
| x = attr.ib(validator=attr.validators.instance_of(int)) |
| y = attr.ib() |
| |
| def method(self): |
| return self.x |
| |
| @classmethod |
| def classmethod(cls): |
| return "clsmethod" |
| |
| @staticmethod |
| def staticmethod(): |
| return "staticmethod" |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class C2Slots(C1BareSlots): |
| z = attr.ib() |
| |
| @attr.s(slots=True, unsafe_hash=True) |
| class C2(C1Bare): |
| z = attr.ib() |
| |
| c2 = C2Slots(x=1, y=2, z="test") |
| assert 1 == c2.x |
| assert 2 == c2.y |
| assert "test" == c2.z |
| |
| assert 1 == c2.method() |
| assert "clsmethod" == c2.classmethod() |
| assert "staticmethod" == c2.staticmethod() |
| |
| with pytest.raises(AttributeError): |
| c2.t = "test" |
| |
| non_slot_instance = C2(x=1, y=2, z="test") |
| if has_pympler: |
| assert asizeof(c2) < asizeof(non_slot_instance) |
| |
| c3 = C2Slots(x=1, y=3, z="test") |
| assert c3 > c2 |
| c2_ = C2Slots(x=1, y=2, z="test") |
| assert c2 == c2_ |
| |
| assert "C2Slots(x=1, y=2, z='test')" == repr(c2) |
| |
| hash(c2) # Just to assert it doesn't raise. |
| |
| assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) |
| |
| |
| class TestClosureCellRewriting: |
| def test_closure_cell_rewriting(self): |
| """ |
| Slotted classes support proper closure cell rewriting. |
| |
| This affects features like `__class__` and the no-arg super(). |
| """ |
| non_slot_instance = C1(x=1, y="test") |
| slot_instance = C1Slots(x=1, y="test") |
| |
| assert non_slot_instance.my_class() is C1 |
| assert slot_instance.my_class() is C1Slots |
| |
| # Just assert they return something, and not an exception. |
| assert non_slot_instance.my_super() |
| assert slot_instance.my_super() |
| |
| def test_inheritance(self): |
| """ |
| Slotted classes support proper closure cell rewriting when inheriting. |
| |
| This affects features like `__class__` and the no-arg super(). |
| """ |
| |
| @attr.s |
| class C2(C1): |
| def my_subclass(self): |
| return __class__ |
| |
| @attr.s |
| class C2Slots(C1Slots): |
| def my_subclass(self): |
| return __class__ |
| |
| non_slot_instance = C2(x=1, y="test") |
| slot_instance = C2Slots(x=1, y="test") |
| |
| assert non_slot_instance.my_class() is C1 |
| assert slot_instance.my_class() is C1Slots |
| |
| # Just assert they return something, and not an exception. |
| assert non_slot_instance.my_super() |
| assert slot_instance.my_super() |
| |
| assert non_slot_instance.my_subclass() is C2 |
| assert slot_instance.my_subclass() is C2Slots |
| |
| def test_cls_static(self, slots): |
| """ |
| Slotted classes support proper closure cell rewriting for class- and |
| static methods. |
| """ |
| # Python can reuse closure cells, so we create new classes just for |
| # this test. |
| |
| @attr.s(slots=slots) |
| class C: |
| @classmethod |
| def clsmethod(cls): |
| return __class__ |
| |
| assert C.clsmethod() is C |
| |
| @attr.s(slots=slots) |
| class D: |
| @staticmethod |
| def statmethod(): |
| return __class__ |
| |
| assert D.statmethod() is D |
| |
| |
| @pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython") |
| def test_not_weakrefable(): |
| """ |
| Instance is not weak-referenceable when `weakref_slot=False` in CPython. |
| """ |
| |
| @attr.s(slots=True, weakref_slot=False) |
| class C: |
| pass |
| |
| c = C() |
| |
| with pytest.raises(TypeError): |
| weakref.ref(c) |
| |
| |
| @pytest.mark.skipif( |
| not PYPY, reason="slots without weakref_slot should only work on PyPy" |
| ) |
| def test_implicitly_weakrefable(): |
| """ |
| Instance is weak-referenceable even when `weakref_slot=False` in PyPy. |
| """ |
| |
| @attr.s(slots=True, weakref_slot=False) |
| class C: |
| pass |
| |
| c = C() |
| w = weakref.ref(c) |
| |
| assert c is w() |
| |
| |
| def test_weakrefable(): |
| """ |
| Instance is weak-referenceable when `weakref_slot=True`. |
| """ |
| |
| @attr.s(slots=True, weakref_slot=True) |
| class C: |
| pass |
| |
| c = C() |
| w = weakref.ref(c) |
| |
| assert c is w() |
| |
| |
| def test_weakref_does_not_add_a_field(): |
| """ |
| `weakref_slot=True` does not add a field to the class. |
| """ |
| |
| @attr.s(slots=True, weakref_slot=True) |
| class C: |
| field = attr.ib() |
| |
| assert [f.name for f in attr.fields(C)] == ["field"] |
| |
| |
| def tests_weakref_does_not_add_when_inheriting_with_weakref(): |
| """ |
| `weakref_slot=True` does not add a new __weakref__ slot when inheriting |
| one. |
| """ |
| |
| @attr.s(slots=True, weakref_slot=True) |
| class C: |
| pass |
| |
| @attr.s(slots=True, weakref_slot=True) |
| class D(C): |
| pass |
| |
| d = D() |
| w = weakref.ref(d) |
| |
| assert d is w() |
| |
| |
| def tests_weakref_does_not_add_with_weakref_attribute(): |
| """ |
| `weakref_slot=True` does not add a new __weakref__ slot when an attribute |
| of that name exists. |
| """ |
| |
| @attr.s(slots=True, weakref_slot=True) |
| class C: |
| __weakref__ = attr.ib( |
| init=False, hash=False, repr=False, eq=False, order=False |
| ) |
| |
| c = C() |
| w = weakref.ref(c) |
| |
| assert c is w() |
| |
| |
| def test_slots_empty_cell(): |
| """ |
| Tests that no `ValueError: Cell is empty` exception is raised when |
| closure cells are present with no contents in a `slots=True` class. |
| (issue https://github.com/python-attrs/attrs/issues/589) |
| |
| If a method mentions `__class__` or uses the no-arg `super()`, the compiler |
| will bake a reference to the class in the method itself as |
| `method.__closure__`. Since `attrs` replaces the class with a clone, |
| `_ClassBuilder._create_slots_class(self)` will rewrite these references so |
| it keeps working. This method was not properly covering the edge case where |
| the closure cell was empty, we fixed it and this is the non-regression |
| test. |
| """ |
| |
| @attr.s(slots=True) |
| class C: |
| field = attr.ib() |
| |
| def f(self, a): |
| super(C, self).__init__() # noqa: UP008 |
| |
| C(field=1) |
| |
| |
| @attr.s(getstate_setstate=True) |
| class C2: |
| x = attr.ib() |
| |
| |
| @attr.s(slots=True, getstate_setstate=True) |
| class C2Slots: |
| x = attr.ib() |
| |
| |
| class TestPickle: |
| @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL)) |
| def test_pickleable_by_default(self, protocol): |
| """ |
| If nothing else is passed, slotted classes can be pickled and |
| unpickled with all supported protocols. |
| """ |
| i1 = C1Slots(1, 2) |
| i2 = pickle.loads(pickle.dumps(i1, protocol)) |
| |
| assert i1 == i2 |
| assert i1 is not i2 |
| |
| def test_no_getstate_setstate_for_dict_classes(self): |
| """ |
| As long as getstate_setstate is None, nothing is done to dict |
| classes. |
| """ |
| assert getattr(object, "__getstate__", None) is getattr( |
| C1, "__getstate__", None |
| ) |
| assert getattr(object, "__setstate__", None) is getattr( |
| C1, "__setstate__", None |
| ) |
| |
| def test_no_getstate_setstate_if_option_false(self): |
| """ |
| Don't add getstate/setstate if getstate_setstate is False. |
| """ |
| |
| @attr.s(slots=True, getstate_setstate=False) |
| class C: |
| x = attr.ib() |
| |
| assert getattr(object, "__getstate__", None) is getattr( |
| C, "__getstate__", None |
| ) |
| assert getattr(object, "__setstate__", None) is getattr( |
| C, "__setstate__", None |
| ) |
| |
| @pytest.mark.parametrize("cls", [C2(1), C2Slots(1)]) |
| def test_getstate_set_state_force_true(self, cls): |
| """ |
| If getstate_setstate is True, add them unconditionally. |
| """ |
| assert None is not getattr(cls, "__getstate__", None) |
| assert None is not getattr(cls, "__setstate__", None) |
| |
| |
| def test_slots_super_property_get(): |
| """ |
| Both `super()` and `super(self.__class__, self)` work. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @property |
| def f(self): |
| return self.x |
| |
| @attr.s(slots=True) |
| class B(A): |
| @property |
| def f(self): |
| return super().f ** 2 |
| |
| @attr.s(slots=True) |
| class C(A): |
| @property |
| def f(self): |
| return super(C, self).f ** 2 # noqa: UP008 |
| |
| assert B(11).f == 121 |
| assert B(17).f == 289 |
| assert C(11).f == 121 |
| assert C(17).f == 289 |
| |
| |
| def test_slots_super_property_get_shortcut(): |
| """ |
| The `super()` shortcut is allowed. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @property |
| def f(self): |
| return self.x |
| |
| @attr.s(slots=True) |
| class B(A): |
| @property |
| def f(self): |
| return super().f ** 2 |
| |
| assert B(11).f == 121 |
| assert B(17).f == 289 |
| |
| |
| def test_slots_cached_property_allows_call(): |
| """ |
| cached_property in slotted class allows call. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| assert A(11).f == 11 |
| |
| |
| def test_slots_cached_property_class_does_not_have__dict__(): |
| """ |
| slotted class with cached property has no __dict__ attribute. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| assert set(A.__slots__) == {"x", "f", "__weakref__"} |
| assert "__dict__" not in dir(A) |
| |
| |
| def test_slots_cached_property_works_on_frozen_instances(): |
| """ |
| Infers type of cached property. |
| """ |
| |
| @attrs.frozen(slots=True) |
| class A: |
| x: int |
| |
| @functools.cached_property |
| def f(self) -> int: |
| return self.x |
| |
| assert A(x=1).f == 1 |
| |
| |
| @pytest.mark.xfail( |
| PY_3_14_PLUS, reason="3.14 does not infer the type anymore." |
| ) |
| def test_slots_cached_property_infers_type(): |
| """ |
| Infers type of cached property on Python 3.13 and earlier. |
| |
| See also #1431. |
| """ |
| |
| @attrs.frozen(slots=True) |
| class A: |
| x: int |
| |
| @functools.cached_property |
| def f(self) -> int: |
| return self.x |
| |
| assert A.__annotations__ == {"x": int, "f": int} |
| |
| |
| def test_slots_cached_property_with_empty_getattr_raises_attribute_error_of_requested(): |
| """ |
| Ensures error information is not lost. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| a = A(1) |
| with pytest.raises( |
| AttributeError, match="'A' object has no attribute 'z'" |
| ): |
| a.z |
| |
| |
| def test_slots_cached_property_raising_attributeerror(): |
| """ |
| Ensures AttributeError raised by a property is preserved by __getattr__() |
| implementation. |
| |
| Regression test for issue https://github.com/python-attrs/attrs/issues/1230 |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.p |
| |
| @property |
| def p(self): |
| raise AttributeError("I am a property") |
| |
| @functools.cached_property |
| def g(self): |
| return self.q |
| |
| @property |
| def q(self): |
| return 2 |
| |
| a = A(1) |
| with pytest.raises(AttributeError, match=r"^I am a property$"): |
| a.p |
| with pytest.raises(AttributeError, match=r"^I am a property$"): |
| a.f |
| |
| assert a.g == 2 |
| assert a.q == 2 |
| |
| |
| def test_slots_cached_property_with_getattr_calls_getattr_for_missing_attributes(): |
| """ |
| Ensure __getattr__ implementation is maintained for non cached_properties. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| def __getattr__(self, item): |
| return item |
| |
| a = A(1) |
| assert a.f == 1 |
| assert a.z == "z" |
| |
| |
| def test_slots_getattr_in_superclass__is_called_for_missing_attributes_when_cached_property_present(): |
| """ |
| Ensure __getattr__ implementation is maintained in subclass. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| def __getattr__(self, item): |
| return item |
| |
| @attr.s(slots=True) |
| class B(A): |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| b = B(1) |
| assert b.f == 1 |
| assert b.z == "z" |
| |
| |
| def test_slots_getattr_in_subclass_gets_superclass_cached_property(): |
| """ |
| Ensure super() in __getattr__ is not broken through cached_property re-write. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| def __getattr__(self, item): |
| return item |
| |
| @attr.s(slots=True) |
| class B(A): |
| @functools.cached_property |
| def g(self): |
| return self.x |
| |
| def __getattr__(self, item): |
| return super().__getattr__(item) |
| |
| b = B(1) |
| assert b.f == 1 |
| assert b.z == "z" |
| |
| |
| def test_slots_sub_class_with_independent_cached_properties_both_work(): |
| """ |
| Subclassing shouldn't break cached properties. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| @attr.s(slots=True) |
| class B(A): |
| @functools.cached_property |
| def g(self): |
| return self.x * 2 |
| |
| assert B(1).f == 1 |
| assert B(1).g == 2 |
| |
| |
| def test_slots_with_multiple_cached_property_subclasses_works(): |
| """ |
| Multiple sub-classes shouldn't break cached properties. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib(kw_only=True) |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| @attr.s(slots=False) |
| class B: |
| @functools.cached_property |
| def g(self): |
| return self.x * 2 |
| |
| def __getattr__(self, item): |
| if hasattr(super(), "__getattr__"): |
| return super().__getattr__(item) |
| return item |
| |
| @attr.s(slots=True) |
| class AB(A, B): |
| pass |
| |
| ab = AB(x=1) |
| |
| assert ab.f == 1 |
| assert ab.g == 2 |
| assert ab.h == "h" |
| |
| |
| def test_slotted_cached_property_can_access_super(): |
| """ |
| Multiple sub-classes shouldn't break cached properties. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib(kw_only=True) |
| |
| @attr.s(slots=True) |
| class B(A): |
| @functools.cached_property |
| def f(self): |
| return super().x * 2 |
| |
| assert B(x=1).f == 2 |
| |
| |
| def test_slots_sub_class_avoids_duplicated_slots(): |
| """ |
| Duplicating the slots is a waste of memory. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| @attr.s(slots=True) |
| class B(A): |
| @functools.cached_property |
| def f(self): |
| return self.x * 2 |
| |
| assert B(1).f == 2 |
| assert B.__slots__ == () |
| |
| |
| def test_slots_sub_class_with_actual_slot(): |
| """ |
| A sub-class can have an explicit attrs field that replaces a cached property. |
| """ |
| |
| @attr.s(slots=True) |
| class A: # slots : (x, f) |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| @attr.s(slots=True) |
| class B(A): |
| f: int = attr.ib() |
| |
| assert B(1, 2).f == 2 |
| assert B.__slots__ == () |
| |
| |
| def test_slots_cached_property_is_not_called_at_construction(): |
| """ |
| A cached property function should only be called at property access point. |
| """ |
| call_count = 0 |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| nonlocal call_count |
| call_count += 1 |
| return self.x |
| |
| A(1) |
| assert call_count == 0 |
| |
| |
| def test_slots_cached_property_repeat_call_only_once(): |
| """ |
| A cached property function should be called only once, on repeated attribute access. |
| """ |
| call_count = 0 |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| nonlocal call_count |
| call_count += 1 |
| return self.x |
| |
| obj = A(1) |
| obj.f |
| obj.f |
| assert call_count == 1 |
| |
| |
| def test_slots_cached_property_called_independent_across_instances(): |
| """ |
| A cached property value should be specific to the given instance. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f(self): |
| return self.x |
| |
| obj_1 = A(1) |
| obj_2 = A(2) |
| |
| assert obj_1.f == 1 |
| assert obj_2.f == 2 |
| |
| |
| def test_slots_cached_properties_work_independently(): |
| """ |
| Multiple cached properties should work independently. |
| """ |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| |
| @functools.cached_property |
| def f_1(self): |
| return self.x |
| |
| @functools.cached_property |
| def f_2(self): |
| return self.x * 2 |
| |
| obj = A(1) |
| |
| assert obj.f_1 == 1 |
| assert obj.f_2 == 2 |
| |
| |
| @attr.s(slots=True) |
| class A: |
| x = attr.ib() |
| b = attr.ib() |
| c = attr.ib() |
| |
| |
| def test_slots_unpickle_after_attr_removed(): |
| """ |
| We don't assign attributes we don't have anymore if the class has |
| removed it. |
| """ |
| a = A(1, 2, 3) |
| a_pickled = pickle.dumps(a) |
| a_unpickled = pickle.loads(a_pickled) |
| assert a_unpickled == a |
| |
| @attr.s(slots=True) |
| class NEW_A: |
| x = attr.ib() |
| c = attr.ib() |
| |
| with mock.patch(f"{__name__}.A", NEW_A): |
| new_a = pickle.loads(a_pickled) |
| |
| assert new_a.x == 1 |
| assert new_a.c == 3 |
| assert not hasattr(new_a, "b") |
| |
| |
| def test_slots_unpickle_after_attr_added(frozen): |
| """ |
| We don't assign attribute we haven't had before if the class has one added. |
| """ |
| a = A(1, 2, 3) |
| a_pickled = pickle.dumps(a) |
| a_unpickled = pickle.loads(a_pickled) |
| |
| assert a_unpickled == a |
| |
| @attr.s(slots=True, frozen=frozen) |
| class NEW_A: |
| x = attr.ib() |
| b = attr.ib() |
| d = attr.ib() |
| c = attr.ib() |
| |
| with mock.patch(f"{__name__}.A", NEW_A): |
| new_a = pickle.loads(a_pickled) |
| |
| assert new_a.x == 1 |
| assert new_a.b == 2 |
| assert new_a.c == 3 |
| assert not hasattr(new_a, "d") |
| |
| |
| def test_slots_unpickle_is_backward_compatible(frozen): |
| """ |
| Ensure object pickled before v22.2.0 can still be unpickled. |
| """ |
| a = A(1, 2, 3) |
| |
| a_pickled = ( |
| b"\x80\x04\x95&\x00\x00\x00\x00\x00\x00\x00\x8c\x10" |
| + a.__module__.encode() |
| + b"\x94\x8c\x01A\x94\x93\x94)\x81\x94K\x01K\x02K\x03\x87\x94b." |
| ) |
| |
| a_unpickled = pickle.loads(a_pickled) |
| |
| assert a_unpickled == a |