""" Unit tests for slot-related functionality. """ import pytest import attr from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell # Pympler doesn't work on PyPy. try: from pympler.asizeof import asizeof has_pympler = True except BaseException: # Won't be an import error. has_pympler = False @attr.s class C1(object): 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" if not PY2: def my_class(self): return __class__ # NOQA: F821 def my_super(self): """Just to test out the no-arg super.""" return super().__repr__() @attr.s(slots=True, hash=True) class C1Slots(object): 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" if not PY2: def my_class(self): return __class__ # NOQA: F821 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 set(["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_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-slot class works. Note that a slots class inheriting from an ordinary class loses most of the benefits of slots classes, but it should still work. """ @attr.s(slots=True, 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 set(["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 non-slots class using 'these' works. This will actually *replace* the class with another one, using slots. """ class SimpleOrdinaryClass(object): 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, 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 set(["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 attr slot class works. """ @attr.s(slots=True, hash=True) class C2Slots(C1Slots): z = attr.ib() @attr.s(slots=True, 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 set(["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_bare_inheritance_from_slots(): """ Inheriting from a bare attr slot class works. """ @attr.s(init=False, cmp=False, hash=False, repr=False, slots=True) class C1BareSlots(object): 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, cmp=False, hash=False, repr=False) class C1Bare(object): 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, hash=True) class C2Slots(C1BareSlots): z = attr.ib() @attr.s(slots=True, 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) @pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.") class TestClosureCellRewriting(object): def test_closure_cell_rewriting(self): """ Slot 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): """ Slot 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__ # NOQA: F821 @attr.s class C2Slots(C1Slots): def my_subclass(self): return __class__ # NOQA: F821 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 @pytest.mark.parametrize("slots", [True, False]) def test_cls_static(self, slots): """ Slot 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__ # noqa: F821 assert C.clsmethod() is C @attr.s(slots=slots) class D: @staticmethod def statmethod(): return __class__ # noqa: F821 assert D.statmethod() is D @pytest.mark.skipif( PYPY, reason="ctypes are used only on CPython" ) def test_missing_ctypes(self, monkeypatch): """ Keeps working if ctypes is missing. A warning is emitted that points to the actual code. """ monkeypatch.setattr(attr._compat, "import_ctypes", lambda: None) func = make_set_closure_cell() with pytest.warns(RuntimeWarning) as wr: func() w = wr.pop() assert __file__ == w.filename assert ( "Missing ctypes. Some features like bare super() or accessing " "__class__ will not work with slots classes.", ) == w.message.args assert just_warn is func