1"""
2Unit tests for slots-related functionality.
3"""
4
5import pickle
6import sys
7import types
8import weakref
9
10import pytest
11
12import attr
13
14from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell
15
16
17# Pympler doesn't work on PyPy.
18try:
19    from pympler.asizeof import asizeof
20
21    has_pympler = True
22except BaseException:  # Won't be an import error.
23    has_pympler = False
24
25
26@attr.s
27class C1(object):
28    x = attr.ib(validator=attr.validators.instance_of(int))
29    y = attr.ib()
30
31    def method(self):
32        return self.x
33
34    @classmethod
35    def classmethod(cls):
36        return "clsmethod"
37
38    @staticmethod
39    def staticmethod():
40        return "staticmethod"
41
42    if not PY2:
43
44        def my_class(self):
45            return __class__
46
47        def my_super(self):
48            """Just to test out the no-arg super."""
49            return super().__repr__()
50
51
52@attr.s(slots=True, hash=True)
53class C1Slots(object):
54    x = attr.ib(validator=attr.validators.instance_of(int))
55    y = attr.ib()
56
57    def method(self):
58        return self.x
59
60    @classmethod
61    def classmethod(cls):
62        return "clsmethod"
63
64    @staticmethod
65    def staticmethod():
66        return "staticmethod"
67
68    if not PY2:
69
70        def my_class(self):
71            return __class__
72
73        def my_super(self):
74            """Just to test out the no-arg super."""
75            return super().__repr__()
76
77
78def test_slots_being_used():
79    """
80    The class is really using __slots__.
81    """
82    non_slot_instance = C1(x=1, y="test")
83    slot_instance = C1Slots(x=1, y="test")
84
85    assert "__dict__" not in dir(slot_instance)
86    assert "__slots__" in dir(slot_instance)
87
88    assert "__dict__" in dir(non_slot_instance)
89    assert "__slots__" not in dir(non_slot_instance)
90
91    assert set(["__weakref__", "x", "y"]) == set(slot_instance.__slots__)
92
93    if has_pympler:
94        assert asizeof(slot_instance) < asizeof(non_slot_instance)
95
96    non_slot_instance.t = "test"
97    with pytest.raises(AttributeError):
98        slot_instance.t = "test"
99
100    assert 1 == non_slot_instance.method()
101    assert 1 == slot_instance.method()
102
103    assert attr.fields(C1Slots) == attr.fields(C1)
104    assert attr.asdict(slot_instance) == attr.asdict(non_slot_instance)
105
106
107def test_basic_attr_funcs():
108    """
109    Comparison, `__eq__`, `__hash__`, `__repr__`, `attrs.asdict` work.
110    """
111    a = C1Slots(x=1, y=2)
112    b = C1Slots(x=1, y=3)
113    a_ = C1Slots(x=1, y=2)
114
115    # Comparison.
116    assert b > a
117
118    assert a_ == a
119
120    # Hashing.
121    hash(b)  # Just to assert it doesn't raise.
122
123    # Repr.
124    assert "C1Slots(x=1, y=2)" == repr(a)
125
126    assert {"x": 1, "y": 2} == attr.asdict(a)
127
128
129def test_inheritance_from_nonslots():
130    """
131    Inheritance from a non-slotted class works.
132
133    Note that a slotted class inheriting from an ordinary class loses most of
134    the benefits of slotted classes, but it should still work.
135    """
136
137    @attr.s(slots=True, hash=True)
138    class C2Slots(C1):
139        z = attr.ib()
140
141    c2 = C2Slots(x=1, y=2, z="test")
142
143    assert 1 == c2.x
144    assert 2 == c2.y
145    assert "test" == c2.z
146
147    c2.t = "test"  # This will work, using the base class.
148
149    assert "test" == c2.t
150
151    assert 1 == c2.method()
152    assert "clsmethod" == c2.classmethod()
153    assert "staticmethod" == c2.staticmethod()
154
155    assert set(["z"]) == set(C2Slots.__slots__)
156
157    c3 = C2Slots(x=1, y=3, z="test")
158
159    assert c3 > c2
160
161    c2_ = C2Slots(x=1, y=2, z="test")
162
163    assert c2 == c2_
164
165    assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
166
167    hash(c2)  # Just to assert it doesn't raise.
168
169    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
170
171
172def test_nonslots_these():
173    """
174    Enhancing a dict class using 'these' works.
175
176    This will actually *replace* the class with another one, using slots.
177    """
178
179    class SimpleOrdinaryClass(object):
180        def __init__(self, x, y, z):
181            self.x = x
182            self.y = y
183            self.z = z
184
185        def method(self):
186            return self.x
187
188        @classmethod
189        def classmethod(cls):
190            return "clsmethod"
191
192        @staticmethod
193        def staticmethod():
194            return "staticmethod"
195
196    C2Slots = attr.s(
197        these={"x": attr.ib(), "y": attr.ib(), "z": attr.ib()},
198        init=False,
199        slots=True,
200        hash=True,
201    )(SimpleOrdinaryClass)
202
203    c2 = C2Slots(x=1, y=2, z="test")
204    assert 1 == c2.x
205    assert 2 == c2.y
206    assert "test" == c2.z
207    with pytest.raises(AttributeError):
208        c2.t = "test"  # We have slots now.
209
210    assert 1 == c2.method()
211    assert "clsmethod" == c2.classmethod()
212    assert "staticmethod" == c2.staticmethod()
213
214    assert set(["__weakref__", "x", "y", "z"]) == set(C2Slots.__slots__)
215
216    c3 = C2Slots(x=1, y=3, z="test")
217    assert c3 > c2
218    c2_ = C2Slots(x=1, y=2, z="test")
219    assert c2 == c2_
220
221    assert "SimpleOrdinaryClass(x=1, y=2, z='test')" == repr(c2)
222
223    hash(c2)  # Just to assert it doesn't raise.
224
225    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
226
227
228def test_inheritance_from_slots():
229    """
230    Inheriting from an attrs slotted class works.
231    """
232
233    @attr.s(slots=True, hash=True)
234    class C2Slots(C1Slots):
235        z = attr.ib()
236
237    @attr.s(slots=True, hash=True)
238    class C2(C1):
239        z = attr.ib()
240
241    c2 = C2Slots(x=1, y=2, z="test")
242    assert 1 == c2.x
243    assert 2 == c2.y
244    assert "test" == c2.z
245
246    assert set(["z"]) == set(C2Slots.__slots__)
247
248    assert 1 == c2.method()
249    assert "clsmethod" == c2.classmethod()
250    assert "staticmethod" == c2.staticmethod()
251
252    with pytest.raises(AttributeError):
253        c2.t = "test"
254
255    non_slot_instance = C2(x=1, y=2, z="test")
256    if has_pympler:
257        assert asizeof(c2) < asizeof(non_slot_instance)
258
259    c3 = C2Slots(x=1, y=3, z="test")
260    assert c3 > c2
261    c2_ = C2Slots(x=1, y=2, z="test")
262    assert c2 == c2_
263
264    assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
265
266    hash(c2)  # Just to assert it doesn't raise.
267
268    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
269
270
271def test_inheritance_from_slots_with_attribute_override():
272    """
273    Inheriting from a slotted class doesn't re-create existing slots
274    """
275
276    class HasXSlot(object):
277        __slots__ = ("x",)
278
279    @attr.s(slots=True, hash=True)
280    class C2Slots(C1Slots):
281        # y re-defined here but it shouldn't get a slot
282        y = attr.ib()
283        z = attr.ib()
284
285    @attr.s(slots=True, hash=True)
286    class NonAttrsChild(HasXSlot):
287        # Parent class has slot for "x" already, so we skip it
288        x = attr.ib()
289        y = attr.ib()
290        z = attr.ib()
291
292    c2 = C2Slots(1, 2, "test")
293    assert 1 == c2.x
294    assert 2 == c2.y
295    assert "test" == c2.z
296
297    assert {"z"} == set(C2Slots.__slots__)
298
299    na = NonAttrsChild(1, 2, "test")
300    assert 1 == na.x
301    assert 2 == na.y
302    assert "test" == na.z
303
304    assert {"__weakref__", "y", "z"} == set(NonAttrsChild.__slots__)
305
306
307def test_inherited_slot_reuses_slot_descriptor():
308    """
309    We reuse slot descriptor for an attr.ib defined in a slotted attr.s
310    """
311
312    class HasXSlot(object):
313        __slots__ = ("x",)
314
315    class OverridesX(HasXSlot):
316        @property
317        def x(self):
318            return None
319
320    @attr.s(slots=True)
321    class Child(OverridesX):
322        x = attr.ib()
323
324    assert Child.x is not OverridesX.x
325    assert Child.x is HasXSlot.x
326
327    c = Child(1)
328    assert 1 == c.x
329    assert set() == set(Child.__slots__)
330
331    ox = OverridesX()
332    assert ox.x is None
333
334
335def test_bare_inheritance_from_slots():
336    """
337    Inheriting from a bare attrs slotted class works.
338    """
339
340    @attr.s(
341        init=False, eq=False, order=False, hash=False, repr=False, slots=True
342    )
343    class C1BareSlots(object):
344        x = attr.ib(validator=attr.validators.instance_of(int))
345        y = attr.ib()
346
347        def method(self):
348            return self.x
349
350        @classmethod
351        def classmethod(cls):
352            return "clsmethod"
353
354        @staticmethod
355        def staticmethod():
356            return "staticmethod"
357
358    @attr.s(init=False, eq=False, order=False, hash=False, repr=False)
359    class C1Bare(object):
360        x = attr.ib(validator=attr.validators.instance_of(int))
361        y = attr.ib()
362
363        def method(self):
364            return self.x
365
366        @classmethod
367        def classmethod(cls):
368            return "clsmethod"
369
370        @staticmethod
371        def staticmethod():
372            return "staticmethod"
373
374    @attr.s(slots=True, hash=True)
375    class C2Slots(C1BareSlots):
376        z = attr.ib()
377
378    @attr.s(slots=True, hash=True)
379    class C2(C1Bare):
380        z = attr.ib()
381
382    c2 = C2Slots(x=1, y=2, z="test")
383    assert 1 == c2.x
384    assert 2 == c2.y
385    assert "test" == c2.z
386
387    assert 1 == c2.method()
388    assert "clsmethod" == c2.classmethod()
389    assert "staticmethod" == c2.staticmethod()
390
391    with pytest.raises(AttributeError):
392        c2.t = "test"
393
394    non_slot_instance = C2(x=1, y=2, z="test")
395    if has_pympler:
396        assert asizeof(c2) < asizeof(non_slot_instance)
397
398    c3 = C2Slots(x=1, y=3, z="test")
399    assert c3 > c2
400    c2_ = C2Slots(x=1, y=2, z="test")
401    assert c2 == c2_
402
403    assert "C2Slots(x=1, y=2, z='test')" == repr(c2)
404
405    hash(c2)  # Just to assert it doesn't raise.
406
407    assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
408
409
410@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.")
411class TestClosureCellRewriting(object):
412    def test_closure_cell_rewriting(self):
413        """
414        Slotted classes support proper closure cell rewriting.
415
416        This affects features like `__class__` and the no-arg super().
417        """
418        non_slot_instance = C1(x=1, y="test")
419        slot_instance = C1Slots(x=1, y="test")
420
421        assert non_slot_instance.my_class() is C1
422        assert slot_instance.my_class() is C1Slots
423
424        # Just assert they return something, and not an exception.
425        assert non_slot_instance.my_super()
426        assert slot_instance.my_super()
427
428    def test_inheritance(self):
429        """
430        Slotted classes support proper closure cell rewriting when inheriting.
431
432        This affects features like `__class__` and the no-arg super().
433        """
434
435        @attr.s
436        class C2(C1):
437            def my_subclass(self):
438                return __class__
439
440        @attr.s
441        class C2Slots(C1Slots):
442            def my_subclass(self):
443                return __class__
444
445        non_slot_instance = C2(x=1, y="test")
446        slot_instance = C2Slots(x=1, y="test")
447
448        assert non_slot_instance.my_class() is C1
449        assert slot_instance.my_class() is C1Slots
450
451        # Just assert they return something, and not an exception.
452        assert non_slot_instance.my_super()
453        assert slot_instance.my_super()
454
455        assert non_slot_instance.my_subclass() is C2
456        assert slot_instance.my_subclass() is C2Slots
457
458    @pytest.mark.parametrize("slots", [True, False])
459    def test_cls_static(self, slots):
460        """
461        Slotted classes support proper closure cell rewriting for class- and
462        static methods.
463        """
464        # Python can reuse closure cells, so we create new classes just for
465        # this test.
466
467        @attr.s(slots=slots)
468        class C:
469            @classmethod
470            def clsmethod(cls):
471                return __class__
472
473        assert C.clsmethod() is C
474
475        @attr.s(slots=slots)
476        class D:
477            @staticmethod
478            def statmethod():
479                return __class__
480
481        assert D.statmethod() is D
482
483    @pytest.mark.skipif(PYPY, reason="set_closure_cell always works on PyPy")
484    @pytest.mark.skipif(
485        sys.version_info >= (3, 8),
486        reason="can't break CodeType.replace() via monkeypatch",
487    )
488    def test_code_hack_failure(self, monkeypatch):
489        """
490        Keeps working if function/code object introspection doesn't work
491        on this (nonstandard) interpeter.
492
493        A warning is emitted that points to the actual code.
494        """
495        # This is a pretty good approximation of the behavior of
496        # the actual types.CodeType on Brython.
497        monkeypatch.setattr(types, "CodeType", lambda: None)
498        func = make_set_closure_cell()
499
500        with pytest.warns(RuntimeWarning) as wr:
501            func()
502
503        w = wr.pop()
504        assert __file__ == w.filename
505        assert (
506            "Running interpreter doesn't sufficiently support code object "
507            "introspection.  Some features like bare super() or accessing "
508            "__class__ will not work with slotted classes.",
509        ) == w.message.args
510
511        assert just_warn is func
512
513
514@pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython")
515def test_not_weakrefable():
516    """
517    Instance is not weak-referenceable when `weakref_slot=False` in CPython.
518    """
519
520    @attr.s(slots=True, weakref_slot=False)
521    class C(object):
522        pass
523
524    c = C()
525
526    with pytest.raises(TypeError):
527        weakref.ref(c)
528
529
530@pytest.mark.skipif(
531    not PYPY, reason="slots without weakref_slot should only work on PyPy"
532)
533def test_implicitly_weakrefable():
534    """
535    Instance is weak-referenceable even when `weakref_slot=False` in PyPy.
536    """
537
538    @attr.s(slots=True, weakref_slot=False)
539    class C(object):
540        pass
541
542    c = C()
543    w = weakref.ref(c)
544
545    assert c is w()
546
547
548def test_weakrefable():
549    """
550    Instance is weak-referenceable when `weakref_slot=True`.
551    """
552
553    @attr.s(slots=True, weakref_slot=True)
554    class C(object):
555        pass
556
557    c = C()
558    w = weakref.ref(c)
559
560    assert c is w()
561
562
563def test_weakref_does_not_add_a_field():
564    """
565    `weakref_slot=True` does not add a field to the class.
566    """
567
568    @attr.s(slots=True, weakref_slot=True)
569    class C(object):
570        field = attr.ib()
571
572    assert [f.name for f in attr.fields(C)] == ["field"]
573
574
575def tests_weakref_does_not_add_when_inheriting_with_weakref():
576    """
577    `weakref_slot=True` does not add a new __weakref__ slot when inheriting
578    one.
579    """
580
581    @attr.s(slots=True, weakref_slot=True)
582    class C(object):
583        pass
584
585    @attr.s(slots=True, weakref_slot=True)
586    class D(C):
587        pass
588
589    d = D()
590    w = weakref.ref(d)
591
592    assert d is w()
593
594
595def tests_weakref_does_not_add_with_weakref_attribute():
596    """
597    `weakref_slot=True` does not add a new __weakref__ slot when an attribute
598    of that name exists.
599    """
600
601    @attr.s(slots=True, weakref_slot=True)
602    class C(object):
603        __weakref__ = attr.ib(
604            init=False, hash=False, repr=False, eq=False, order=False
605        )
606
607    c = C()
608    w = weakref.ref(c)
609
610    assert c is w()
611
612
613def test_slots_empty_cell():
614    """
615    Tests that no `ValueError: Cell is empty` exception is raised when
616    closure cells are present with no contents in a `slots=True` class.
617    (issue https://github.com/python-attrs/attrs/issues/589)
618
619    On Python 3, if a method mentions `__class__` or uses the no-arg `super()`,
620    the compiler will bake a reference to the class in the method itself as
621    `method.__closure__`. Since `attrs` replaces the class with a clone,
622    `_ClassBuilder._create_slots_class(self)` will rewrite these references so
623    it keeps working. This method was not properly covering the edge case where
624    the closure cell was empty, we fixed it and this is the non-regression
625    test.
626    """
627
628    @attr.s(slots=True)
629    class C(object):
630        field = attr.ib()
631
632        def f(self, a):
633            super(C, self).__init__()
634
635    C(field=1)
636
637
638@attr.s(getstate_setstate=True)
639class C2(object):
640    x = attr.ib()
641
642
643@attr.s(slots=True, getstate_setstate=True)
644class C2Slots(object):
645    x = attr.ib()
646
647
648class TestPickle(object):
649    @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL))
650    def test_pickleable_by_default(self, protocol):
651        """
652        If nothing else is passed, slotted classes can be pickled and
653        unpickled with all supported protocols.
654        """
655        i1 = C1Slots(1, 2)
656        i2 = pickle.loads(pickle.dumps(i1, protocol))
657
658        assert i1 == i2
659        assert i1 is not i2
660
661    def test_no_getstate_setstate_for_dict_classes(self):
662        """
663        As long as getstate_setstate is None, nothing is done to dict
664        classes.
665        """
666        i = C1(1, 2)
667
668        assert None is getattr(i, "__getstate__", None)
669        assert None is getattr(i, "__setstate__", None)
670
671    def test_no_getstate_setstate_if_option_false(self):
672        """
673        Don't add getstate/setstate if getstate_setstate is False.
674        """
675
676        @attr.s(slots=True, getstate_setstate=False)
677        class C(object):
678            x = attr.ib()
679
680        i = C(42)
681
682        assert None is getattr(i, "__getstate__", None)
683        assert None is getattr(i, "__setstate__", None)
684
685    @pytest.mark.parametrize("cls", [C2(1), C2Slots(1)])
686    def test_getstate_set_state_force_true(self, cls):
687        """
688        If getstate_setstate is True, add them unconditionally.
689        """
690        assert None is not getattr(cls, "__getstate__", None)
691        assert None is not getattr(cls, "__setstate__", None)
692
693
694def test_slots_super_property_get():
695    """
696    On Python 2/3: the `super(self.__class__, self)` works.
697    """
698
699    @attr.s(slots=True)
700    class A(object):
701        x = attr.ib()
702
703        @property
704        def f(self):
705            return self.x
706
707    @attr.s(slots=True)
708    class B(A):
709        @property
710        def f(self):
711            return super(B, self).f ** 2
712
713    assert B(11).f == 121
714    assert B(17).f == 289
715
716
717@pytest.mark.skipif(PY2, reason="shortcut super() is PY3-only.")
718def test_slots_super_property_get_shurtcut():
719    """
720    On Python 3, the `super()` shortcut is allowed.
721    """
722
723    @attr.s(slots=True)
724    class A(object):
725        x = attr.ib()
726
727        @property
728        def f(self):
729            return self.x
730
731    @attr.s(slots=True)
732    class B(A):
733        @property
734        def f(self):
735            return super().f ** 2
736
737    assert B(11).f == 121
738    assert B(17).f == 289
739