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