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