1"""
2Unit tests for slot-related functionality.
3"""
4
5import weakref
6
7import pytest
8
9import attr
10
11from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell
12
13
14# Pympler doesn't work on PyPy.
15try:
16    from pympler.asizeof import asizeof
17
18    has_pympler = True
19except BaseException:  # Won't be an import error.
20    has_pympler = False
21
22
23@attr.s
24class C1(object):
25    x = attr.ib(validator=attr.validators.instance_of(int))
26    y = attr.ib()
27
28    def method(self):
29        return self.x
30
31    @classmethod
32    def classmethod(cls):
33        return "clsmethod"
34
35    @staticmethod
36    def staticmethod():
37        return "staticmethod"
38
39    if not PY2:
40
41        def my_class(self):
42            return __class__  # NOQA: F821
43
44        def my_super(self):
45            """Just to test out the no-arg super."""
46            return super().__repr__()
47
48
49@attr.s(slots=True, hash=True)
50class C1Slots(object):
51    x = attr.ib(validator=attr.validators.instance_of(int))
52    y = attr.ib()
53
54    def method(self):
55        return self.x
56
57    @classmethod
58    def classmethod(cls):
59        return "clsmethod"
60
61    @staticmethod
62    def staticmethod():
63        return "staticmethod"
64
65    if not PY2:
66
67        def my_class(self):
68            return __class__  # NOQA: F821
69
70        def my_super(self):
71            """Just to test out the no-arg super."""
72            return super().__repr__()
73
74
75def test_slots_being_used():
76    """
77    The class is really using __slots__.
78    """
79    non_slot_instance = C1(x=1, y="test")
80    slot_instance = C1Slots(x=1, y="test")
81
82    assert "__dict__" not in dir(slot_instance)
83    assert "__slots__" in dir(slot_instance)
84
85    assert "__dict__" in dir(non_slot_instance)
86    assert "__slots__" not in dir(non_slot_instance)
87
88    assert set(["__weakref__", "x", "y"]) == set(slot_instance.__slots__)
89
90    if has_pympler:
91        assert asizeof(slot_instance) < asizeof(non_slot_instance)
92
93    non_slot_instance.t = "test"
94    with pytest.raises(AttributeError):
95        slot_instance.t = "test"
96
97    assert 1 == non_slot_instance.method()
98    assert 1 == slot_instance.method()
99
100    assert attr.fields(C1Slots) == attr.fields(C1)
101    assert attr.asdict(slot_instance) == attr.asdict(non_slot_instance)
102
103
104def test_basic_attr_funcs():
105    """
106    Comparison, `__eq__`, `__hash__`, `__repr__`, `attrs.asdict` work.
107    """
108    a = C1Slots(x=1, y=2)
109    b = C1Slots(x=1, y=3)
110    a_ = C1Slots(x=1, y=2)
111
112    # Comparison.
113    assert b > a
114
115    assert a_ == a
116
117    # Hashing.
118    hash(b)  # Just to assert it doesn't raise.
119
120    # Repr.
121    assert "C1Slots(x=1, y=2)" == repr(a)
122
123    assert {"x": 1, "y": 2} == attr.asdict(a)
124
125
126def test_inheritance_from_nonslots():
127    """
128    Inheritance from a non-slot class works.
129
130    Note that a slotted class inheriting from an ordinary class loses most of
131    the benefits of slotted classes, but it should still work.
132    """
133
134    @attr.s(slots=True, hash=True)
135    class C2Slots(C1):
136        z = attr.ib()
137
138    c2 = C2Slots(x=1, y=2, z="test")
139
140    assert 1 == c2.x
141    assert 2 == c2.y
142    assert "test" == c2.z
143
144    c2.t = "test"  # This will work, using the base class.
145
146    assert "test" == c2.t
147
148    assert 1 == c2.method()
149    assert "clsmethod" == c2.classmethod()
150    assert "staticmethod" == c2.staticmethod()
151
152    assert set(["z"]) == set(C2Slots.__slots__)
153
154    c3 = C2Slots(x=1, y=3, z="test")
155
156    assert c3 > c2
157
158    c2_ = C2Slots(x=1, y=2, z="test")
159
160    assert c2 == c2_
161
162    assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
163
164    hash(c2)  # Just to assert it doesn't raise.
165
166    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
167
168
169def test_nonslots_these():
170    """
171    Enhancing a dict class using 'these' works.
172
173    This will actually *replace* the class with another one, using slots.
174    """
175
176    class SimpleOrdinaryClass(object):
177        def __init__(self, x, y, z):
178            self.x = x
179            self.y = y
180            self.z = z
181
182        def method(self):
183            return self.x
184
185        @classmethod
186        def classmethod(cls):
187            return "clsmethod"
188
189        @staticmethod
190        def staticmethod():
191            return "staticmethod"
192
193    C2Slots = attr.s(
194        these={"x": attr.ib(), "y": attr.ib(), "z": attr.ib()},
195        init=False,
196        slots=True,
197        hash=True,
198    )(SimpleOrdinaryClass)
199
200    c2 = C2Slots(x=1, y=2, z="test")
201    assert 1 == c2.x
202    assert 2 == c2.y
203    assert "test" == c2.z
204    with pytest.raises(AttributeError):
205        c2.t = "test"  # We have slots now.
206
207    assert 1 == c2.method()
208    assert "clsmethod" == c2.classmethod()
209    assert "staticmethod" == c2.staticmethod()
210
211    assert set(["__weakref__", "x", "y", "z"]) == set(C2Slots.__slots__)
212
213    c3 = C2Slots(x=1, y=3, z="test")
214    assert c3 > c2
215    c2_ = C2Slots(x=1, y=2, z="test")
216    assert c2 == c2_
217
218    assert "SimpleOrdinaryClass(x=1, y=2, z='test')" == repr(c2)
219
220    hash(c2)  # Just to assert it doesn't raise.
221
222    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
223
224
225def test_inheritance_from_slots():
226    """
227    Inheriting from an attr slot class works.
228    """
229
230    @attr.s(slots=True, hash=True)
231    class C2Slots(C1Slots):
232        z = attr.ib()
233
234    @attr.s(slots=True, hash=True)
235    class C2(C1):
236        z = attr.ib()
237
238    c2 = C2Slots(x=1, y=2, z="test")
239    assert 1 == c2.x
240    assert 2 == c2.y
241    assert "test" == c2.z
242
243    assert set(["z"]) == set(C2Slots.__slots__)
244
245    assert 1 == c2.method()
246    assert "clsmethod" == c2.classmethod()
247    assert "staticmethod" == c2.staticmethod()
248
249    with pytest.raises(AttributeError):
250        c2.t = "test"
251
252    non_slot_instance = C2(x=1, y=2, z="test")
253    if has_pympler:
254        assert asizeof(c2) < asizeof(non_slot_instance)
255
256    c3 = C2Slots(x=1, y=3, z="test")
257    assert c3 > c2
258    c2_ = C2Slots(x=1, y=2, z="test")
259    assert c2 == c2_
260
261    assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
262
263    hash(c2)  # Just to assert it doesn't raise.
264
265    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
266
267
268def test_bare_inheritance_from_slots():
269    """
270    Inheriting from a bare attr slot class works.
271    """
272
273    @attr.s(init=False, cmp=False, hash=False, repr=False, slots=True)
274    class C1BareSlots(object):
275        x = attr.ib(validator=attr.validators.instance_of(int))
276        y = attr.ib()
277
278        def method(self):
279            return self.x
280
281        @classmethod
282        def classmethod(cls):
283            return "clsmethod"
284
285        @staticmethod
286        def staticmethod():
287            return "staticmethod"
288
289    @attr.s(init=False, cmp=False, hash=False, repr=False)
290    class C1Bare(object):
291        x = attr.ib(validator=attr.validators.instance_of(int))
292        y = attr.ib()
293
294        def method(self):
295            return self.x
296
297        @classmethod
298        def classmethod(cls):
299            return "clsmethod"
300
301        @staticmethod
302        def staticmethod():
303            return "staticmethod"
304
305    @attr.s(slots=True, hash=True)
306    class C2Slots(C1BareSlots):
307        z = attr.ib()
308
309    @attr.s(slots=True, hash=True)
310    class C2(C1Bare):
311        z = attr.ib()
312
313    c2 = C2Slots(x=1, y=2, z="test")
314    assert 1 == c2.x
315    assert 2 == c2.y
316    assert "test" == c2.z
317
318    assert 1 == c2.method()
319    assert "clsmethod" == c2.classmethod()
320    assert "staticmethod" == c2.staticmethod()
321
322    with pytest.raises(AttributeError):
323        c2.t = "test"
324
325    non_slot_instance = C2(x=1, y=2, z="test")
326    if has_pympler:
327        assert asizeof(c2) < asizeof(non_slot_instance)
328
329    c3 = C2Slots(x=1, y=3, z="test")
330    assert c3 > c2
331    c2_ = C2Slots(x=1, y=2, z="test")
332    assert c2 == c2_
333
334    assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
335
336    hash(c2)  # Just to assert it doesn't raise.
337
338    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
339
340
341@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.")
342class TestClosureCellRewriting(object):
343    def test_closure_cell_rewriting(self):
344        """
345        Slot classes support proper closure cell rewriting.
346
347        This affects features like `__class__` and the no-arg super().
348        """
349        non_slot_instance = C1(x=1, y="test")
350        slot_instance = C1Slots(x=1, y="test")
351
352        assert non_slot_instance.my_class() is C1
353        assert slot_instance.my_class() is C1Slots
354
355        # Just assert they return something, and not an exception.
356        assert non_slot_instance.my_super()
357        assert slot_instance.my_super()
358
359    def test_inheritance(self):
360        """
361        Slot classes support proper closure cell rewriting when inheriting.
362
363        This affects features like `__class__` and the no-arg super().
364        """
365
366        @attr.s
367        class C2(C1):
368            def my_subclass(self):
369                return __class__  # NOQA: F821
370
371        @attr.s
372        class C2Slots(C1Slots):
373            def my_subclass(self):
374                return __class__  # NOQA: F821
375
376        non_slot_instance = C2(x=1, y="test")
377        slot_instance = C2Slots(x=1, y="test")
378
379        assert non_slot_instance.my_class() is C1
380        assert slot_instance.my_class() is C1Slots
381
382        # Just assert they return something, and not an exception.
383        assert non_slot_instance.my_super()
384        assert slot_instance.my_super()
385
386        assert non_slot_instance.my_subclass() is C2
387        assert slot_instance.my_subclass() is C2Slots
388
389    @pytest.mark.parametrize("slots", [True, False])
390    def test_cls_static(self, slots):
391        """
392        Slot classes support proper closure cell rewriting for class- and
393        static methods.
394        """
395        # Python can reuse closure cells, so we create new classes just for
396        # this test.
397
398        @attr.s(slots=slots)
399        class C:
400            @classmethod
401            def clsmethod(cls):
402                return __class__  # noqa: F821
403
404        assert C.clsmethod() is C
405
406        @attr.s(slots=slots)
407        class D:
408            @staticmethod
409            def statmethod():
410                return __class__  # noqa: F821
411
412        assert D.statmethod() is D
413
414    @pytest.mark.skipif(PYPY, reason="ctypes are used only on CPython")
415    def test_missing_ctypes(self, monkeypatch):
416        """
417        Keeps working if ctypes is missing.
418
419        A warning is emitted that points to the actual code.
420        """
421        monkeypatch.setattr(attr._compat, "import_ctypes", lambda: None)
422        func = make_set_closure_cell()
423
424        with pytest.warns(RuntimeWarning) as wr:
425            func()
426
427        w = wr.pop()
428        assert __file__ == w.filename
429        assert (
430            "Missing ctypes.  Some features like bare super() or accessing "
431            "__class__ will not work with slotted classes.",
432        ) == w.message.args
433
434        assert just_warn is func
435
436
437@pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython")
438def test_not_weakrefable():
439    """
440    Instance is not weak-referenceable when `weakref_slot=False` in CPython.
441    """
442
443    @attr.s(slots=True, weakref_slot=False)
444    class C(object):
445        pass
446
447    c = C()
448
449    with pytest.raises(TypeError):
450        weakref.ref(c)
451
452
453@pytest.mark.skipif(
454    not PYPY, reason="slots without weakref_slot should only work on PyPy"
455)
456def test_implicitly_weakrefable():
457    """
458    Instance is weak-referenceable even when `weakref_slot=False` in PyPy.
459    """
460
461    @attr.s(slots=True, weakref_slot=False)
462    class C(object):
463        pass
464
465    c = C()
466    w = weakref.ref(c)
467
468    assert c is w()
469
470
471def test_weakrefable():
472    """
473    Instance is weak-referenceable when `weakref_slot=True`.
474    """
475
476    @attr.s(slots=True, weakref_slot=True)
477    class C(object):
478        pass
479
480    c = C()
481    w = weakref.ref(c)
482
483    assert c is w()
484
485
486def test_weakref_does_not_add_a_field():
487    """
488    `weakref_slot=True` does not add a field to the class.
489    """
490
491    @attr.s(slots=True, weakref_slot=True)
492    class C(object):
493        field = attr.ib()
494
495    assert [f.name for f in attr.fields(C)] == ["field"]
496
497
498def tests_weakref_does_not_add_when_inheriting_with_weakref():
499    """
500    `weakref_slot=True` does not add a new __weakref__ slot when inheriting
501    one.
502    """
503
504    @attr.s(slots=True, weakref_slot=True)
505    class C(object):
506        pass
507
508    @attr.s(slots=True, weakref_slot=True)
509    class D(C):
510        pass
511
512    d = D()
513    w = weakref.ref(d)
514
515    assert d is w()
516
517
518def tests_weakref_does_not_add_with_weakref_attribute():
519    """
520    `weakref_slot=True` does not add a new __weakref__ slot when an attribute
521    of that name exists.
522    """
523
524    @attr.s(slots=True, weakref_slot=True)
525    class C(object):
526        __weakref__ = attr.ib(init=False, hash=False, repr=False, cmp=False)
527
528    c = C()
529    w = weakref.ref(c)
530
531    assert c is w()
532