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