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