1from __future__ import absolute_import, division, print_function 2 3import linecache 4import sys 5import warnings 6 7import pytest 8 9from characteristic import ( 10 Attribute, 11 NOTHING, 12 PY26, 13 _attrs_to_script, 14 _ensure_attributes, 15 attributes, 16 immutable, 17 with_cmp, 18 with_init, 19 with_repr, 20) 21 22PY2 = sys.version_info[0] == 2 23 24warnings.simplefilter("always") 25 26 27class TestAttribute(object): 28 def test_init_simple(self): 29 """ 30 Instantiating with just the name initializes properly. 31 """ 32 a = Attribute("foo") 33 assert "foo" == a.name 34 assert NOTHING is a.default_value 35 36 def test_init_default_factory(self): 37 """ 38 Instantiating with default_factory creates a proper descriptor for 39 _default. 40 """ 41 a = Attribute("foo", default_factory=list) 42 assert NOTHING is a.default_value 43 assert list() == a.default_factory() 44 45 def test_init_default_value(self): 46 """ 47 Instantiating with default_value initializes default properly. 48 """ 49 a = Attribute("foo", default_value="bar") 50 assert "bar" == a.default_value 51 52 def test_ambiguous_defaults(self): 53 """ 54 Instantiating with both default_value and default_factory raises 55 ValueError. 56 """ 57 with pytest.raises(ValueError): 58 Attribute( 59 "foo", 60 default_value="bar", 61 default_factory=lambda: 42 62 ) 63 64 def test_missing_attr(self): 65 """ 66 Accessing inexistent attributes still raises an AttributeError. 67 """ 68 a = Attribute("foo") 69 with pytest.raises(AttributeError): 70 a.bar 71 72 def test_alias(self): 73 """ 74 If an attribute with a leading _ is defined, the initializer keyword 75 is stripped of it. 76 """ 77 a = Attribute("_private") 78 assert "private" == a._kw_name 79 80 def test_non_alias(self): 81 """ 82 The keyword name of a non-private 83 """ 84 a = Attribute("public") 85 assert "public" == a._kw_name 86 87 def test_dunder(self): 88 """ 89 Dunder gets all _ stripped. 90 """ 91 a = Attribute("__very_private") 92 assert "very_private" == a._kw_name 93 94 def test_init_aliaser_none(self): 95 """ 96 No aliasing if init_aliaser is None. 97 """ 98 a = Attribute("_private", init_aliaser=None) 99 assert a.name == a._kw_name 100 101 def test_init_aliaser(self): 102 """ 103 Any callable works for aliasing. 104 """ 105 a = Attribute("a", init_aliaser=lambda _: "foo") 106 assert "foo" == a._kw_name 107 108 def test_repr(self): 109 """ 110 repr returns the correct string. 111 """ 112 a = Attribute( 113 name="name", 114 exclude_from_cmp=True, 115 exclude_from_init=True, 116 exclude_from_repr=True, 117 exclude_from_immutable=True, 118 default_value=42, 119 instance_of=str, 120 init_aliaser=None 121 ) 122 assert ( 123 "<Attribute(name='name', exclude_from_cmp=True, " 124 "exclude_from_init=True, exclude_from_repr=True, " 125 "exclude_from_immutable=True, " 126 "default_value=42, default_factory=None, instance_of=<{0} 'str'>," 127 " init_aliaser=None)>" 128 ).format("type" if PY2 else "class") == repr(a) 129 130 def test_eq_different_types(self): 131 """ 132 Comparing Attribute with something else returns NotImplemented. 133 """ 134 assert NotImplemented == Attribute(name="name").__eq__(None) 135 136 def test_eq_equal(self): 137 """ 138 Equal Attributes are detected equal. 139 """ 140 kw = { 141 "name": "name", 142 "exclude_from_cmp": True, 143 "exclude_from_init": False, 144 "exclude_from_repr": True, 145 "exclude_from_immutable": False, 146 "default_value": 42, 147 "instance_of": int, 148 } 149 assert Attribute(**kw) == Attribute(**kw) 150 151 def test_eq_unequal(self): 152 """ 153 Equal Attributes are detected equal. 154 """ 155 kw = { 156 "name": "name", 157 "exclude_from_cmp": True, 158 "exclude_from_init": False, 159 "exclude_from_repr": True, 160 "exclude_from_immutable": False, 161 "default_value": 42, 162 "instance_of": int, 163 } 164 for arg in kw.keys(): 165 kw_mutated = dict(**kw) 166 kw_mutated[arg] = "mutated" 167 assert Attribute(**kw) != Attribute(**kw_mutated) 168 169 170@with_cmp(["a", "b"]) 171class CmpC(object): 172 def __init__(self, a, b): 173 self.a = a 174 self.b = b 175 176 177class TestWithCmp(object): 178 def test_equal(self): 179 """ 180 Equal objects are detected as equal. 181 """ 182 assert CmpC(1, 2) == CmpC(1, 2) 183 assert not (CmpC(1, 2) != CmpC(1, 2)) 184 185 def test_unequal_same_class(self): 186 """ 187 Unequal objects of correct type are detected as unequal. 188 """ 189 assert CmpC(1, 2) != CmpC(2, 1) 190 assert not (CmpC(1, 2) == CmpC(2, 1)) 191 192 def test_unequal_different_class(self): 193 """ 194 Unequal objects of differnt type are detected even if their attributes 195 match. 196 """ 197 class NotCmpC(object): 198 a = 1 199 b = 2 200 assert CmpC(1, 2) != NotCmpC() 201 assert not (CmpC(1, 2) == NotCmpC()) 202 203 @pytest.mark.parametrize( 204 "a,b", [ 205 ((1, 2), (2, 1)), 206 ((1, 2), (1, 3)), 207 (("a", "b"), ("b", "a")), 208 ] 209 ) 210 def test_lt(self, a, b): 211 """ 212 __lt__ compares objects as tuples of attribute values. 213 """ 214 assert CmpC(*a) < CmpC(*b) 215 216 def test_lt_unordable(self): 217 """ 218 __lt__ returns NotImplemented if classes differ. 219 """ 220 assert NotImplemented == (CmpC(1, 2).__lt__(42)) 221 222 @pytest.mark.parametrize( 223 "a,b", [ 224 ((1, 2), (2, 1)), 225 ((1, 2), (1, 3)), 226 ((1, 1), (1, 1)), 227 (("a", "b"), ("b", "a")), 228 (("a", "b"), ("a", "b")), 229 ] 230 ) 231 def test_le(self, a, b): 232 """ 233 __le__ compares objects as tuples of attribute values. 234 """ 235 assert CmpC(*a) <= CmpC(*b) 236 237 def test_le_unordable(self): 238 """ 239 __le__ returns NotImplemented if classes differ. 240 """ 241 assert NotImplemented == (CmpC(1, 2).__le__(42)) 242 243 @pytest.mark.parametrize( 244 "a,b", [ 245 ((2, 1), (1, 2)), 246 ((1, 3), (1, 2)), 247 (("b", "a"), ("a", "b")), 248 ] 249 ) 250 def test_gt(self, a, b): 251 """ 252 __gt__ compares objects as tuples of attribute values. 253 """ 254 assert CmpC(*a) > CmpC(*b) 255 256 def test_gt_unordable(self): 257 """ 258 __gt__ returns NotImplemented if classes differ. 259 """ 260 assert NotImplemented == (CmpC(1, 2).__gt__(42)) 261 262 @pytest.mark.parametrize( 263 "a,b", [ 264 ((2, 1), (1, 2)), 265 ((1, 3), (1, 2)), 266 ((1, 1), (1, 1)), 267 (("b", "a"), ("a", "b")), 268 (("a", "b"), ("a", "b")), 269 ] 270 ) 271 def test_ge(self, a, b): 272 """ 273 __ge__ compares objects as tuples of attribute values. 274 """ 275 assert CmpC(*a) >= CmpC(*b) 276 277 def test_ge_unordable(self): 278 """ 279 __ge__ returns NotImplemented if classes differ. 280 """ 281 assert NotImplemented == (CmpC(1, 2).__ge__(42)) 282 283 def test_hash(self): 284 """ 285 __hash__ returns different hashes for different values. 286 """ 287 assert hash(CmpC(1, 2)) != hash(CmpC(1, 1)) 288 289 def test_Attribute_exclude_from_cmp(self): 290 """ 291 Ignores attribute if exclude_from_cmp=True. 292 """ 293 @with_cmp([Attribute("a", exclude_from_cmp=True), "b"]) 294 class C(object): 295 def __init__(self, a, b): 296 self.a = a 297 self.b = b 298 299 assert C(42, 1) == C(23, 1) 300 301 302@with_repr(["a", "b"]) 303class ReprC(object): 304 def __init__(self, a, b): 305 self.a = a 306 self.b = b 307 308 309class TestReprAttrs(object): 310 def test_repr(self): 311 """ 312 Test repr returns a sensible value. 313 """ 314 assert "<ReprC(a=1, b=2)>" == repr(ReprC(1, 2)) 315 316 def test_Attribute_exclude_from_repr(self): 317 """ 318 Ignores attribute if exclude_from_repr=True. 319 """ 320 @with_repr([Attribute("a", exclude_from_repr=True), "b"]) 321 class C(object): 322 def __init__(self, a, b): 323 self.a = a 324 self.b = b 325 326 assert "<C(b=2)>" == repr(C(1, 2)) 327 328 329@with_init([Attribute("a"), Attribute("b")]) 330class InitC(object): 331 def __init__(self): 332 if self.a == self.b: 333 raise ValueError 334 335 336class TestWithInit(object): 337 def test_sets_attributes(self): 338 """ 339 The attributes are initialized using the passed keywords. 340 """ 341 obj = InitC(a=1, b=2) 342 assert 1 == obj.a 343 assert 2 == obj.b 344 345 def test_custom_init(self): 346 """ 347 The class initializer is called too. 348 """ 349 with pytest.raises(ValueError): 350 InitC(a=1, b=1) 351 352 def test_passes_args(self): 353 """ 354 All positional parameters are passed to the original initializer. 355 """ 356 @with_init(["a"]) 357 class InitWithArg(object): 358 def __init__(self, arg): 359 self.arg = arg 360 361 obj = InitWithArg(42, a=1) 362 assert 42 == obj.arg 363 assert 1 == obj.a 364 365 def test_passes_remaining_kw(self): 366 """ 367 Keyword arguments that aren't used for attributes are passed to the 368 original initializer. 369 """ 370 @with_init(["a"]) 371 class InitWithKWArg(object): 372 def __init__(self, kw_arg=None): 373 self.kw_arg = kw_arg 374 375 obj = InitWithKWArg(a=1, kw_arg=42) 376 assert 42 == obj.kw_arg 377 assert 1 == obj.a 378 379 def test_does_not_pass_attrs(self): 380 """ 381 The attributes are removed from the keyword arguments before they are 382 passed to the original initializer. 383 """ 384 @with_init(["a"]) 385 class InitWithKWArgs(object): 386 def __init__(self, **kw): 387 assert "a" not in kw 388 assert "b" in kw 389 InitWithKWArgs(a=1, b=42) 390 391 def test_defaults(self): 392 """ 393 If defaults are passed, they are used as fallback. 394 """ 395 @with_init(["a", "b"], defaults={"b": 2}) 396 class InitWithDefaults(object): 397 pass 398 obj = InitWithDefaults(a=1) 399 assert 2 == obj.b 400 401 def test_missing_arg(self): 402 """ 403 Raises `ValueError` if a value isn't passed. 404 """ 405 with pytest.raises(ValueError) as e: 406 InitC(a=1) 407 assert "Missing keyword value for 'b'." == e.value.args[0] 408 409 def test_defaults_conflict(self): 410 """ 411 Raises `ValueError` if both defaults and an Attribute are passed. 412 """ 413 with pytest.raises(ValueError) as e: 414 @with_init([Attribute("a")], defaults={"a": 42}) 415 class C(object): 416 pass 417 assert ( 418 "Mixing of the 'defaults' keyword argument and passing instances " 419 "of Attribute for 'attrs' is prohibited. Please don't use " 420 "'defaults' anymore, it has been deprecated in 14.0." 421 == e.value.args[0] 422 ) 423 424 def test_attribute(self): 425 """ 426 String attributes are converted to Attributes and thus work. 427 """ 428 @with_init(["a"]) 429 class C(object): 430 pass 431 o = C(a=1) 432 assert 1 == o.a 433 434 def test_default_factory(self): 435 """ 436 The default factory is used for each instance of missing keyword 437 argument. 438 """ 439 @with_init([Attribute("a", default_factory=list)]) 440 class C(object): 441 pass 442 o1 = C() 443 o2 = C() 444 assert o1.a is not o2.a 445 446 def test_underscores(self): 447 """ 448 with_init takes keyword aliasing into account. 449 """ 450 @with_init([Attribute("_a")]) 451 class C(object): 452 pass 453 c = C(a=1) 454 assert 1 == c._a 455 456 def test_plain_no_alias(self): 457 """ 458 str-based attributes don't get aliased for backward-compatibility. 459 """ 460 @with_init(["_a"]) 461 class C(object): 462 pass 463 c = C(_a=1) 464 assert 1 == c._a 465 466 def test_instance_of_fail(self): 467 """ 468 Raise `TypeError` if an Attribute with an `instance_of` is is attempted 469 to be set to a mismatched type. 470 """ 471 @with_init([Attribute("a", instance_of=int)]) 472 class C(object): 473 pass 474 with pytest.raises(TypeError) as e: 475 C(a="not an int!") 476 assert ( 477 "Attribute 'a' must be an instance of 'int'." 478 == e.value.args[0] 479 ) 480 481 def test_instance_of_success(self): 482 """ 483 Setting an attribute to a value that doesn't conflict with an 484 `instance_of` declaration works. 485 """ 486 @with_init([Attribute("a", instance_of=int)]) 487 class C(object): 488 pass 489 c = C(a=42) 490 assert 42 == c.a 491 492 def test_Attribute_exclude_from_init(self): 493 """ 494 Ignores attribute if exclude_from_init=True. 495 """ 496 @with_init([Attribute("a", exclude_from_init=True), "b"]) 497 class C(object): 498 pass 499 500 C(b=1) 501 502 def test_deprecation_defaults(self): 503 """ 504 Emits a DeprecationWarning if `defaults` is used. 505 """ 506 with warnings.catch_warnings(record=True) as w: 507 @with_init(["a"], defaults={"a": 42}) 508 class C(object): 509 pass 510 assert ( 511 '`defaults` has been deprecated in 14.0, please use the ' 512 '`Attribute` class instead.' 513 ) == w[0].message.args[0] 514 assert issubclass(w[0].category, DeprecationWarning) 515 516 def test_linecache(self): 517 """ 518 The created init method is added to the linecache so PDB shows it 519 properly. 520 """ 521 attrs = [Attribute("a")] 522 523 @with_init(attrs) 524 class C(object): 525 pass 526 527 assert tuple == type(linecache.cache[C.__init__.__code__.co_filename]) 528 529 def test_linecache_attrs_unique(self): 530 """ 531 If the attributes are the same, only one linecache entry is created. 532 Since the key within the cache is the filename, this effectively means 533 that the filenames must be equal if the attributes are equal. 534 """ 535 attrs = [Attribute("a")] 536 537 @with_init(attrs[:]) 538 class C1(object): 539 pass 540 541 @with_init(attrs[:]) 542 class C2(object): 543 pass 544 545 assert ( 546 C1.__init__.__code__.co_filename 547 == C2.__init__.__code__.co_filename 548 ) 549 550 def test_linecache_different_attrs(self): 551 """ 552 Different Attributes have different generated filenames. 553 """ 554 @with_init([Attribute("a")]) 555 class C1(object): 556 pass 557 558 @with_init([Attribute("b")]) 559 class C2(object): 560 pass 561 562 assert ( 563 C1.__init__.__code__.co_filename 564 != C2.__init__.__code__.co_filename 565 ) 566 567 def test_no_attributes(self): 568 """ 569 Specifying no attributes doesn't raise an exception. 570 """ 571 @with_init([]) 572 class C(object): 573 pass 574 C() 575 576 577class TestAttributes(object): 578 def test_leaves_init_alone(self): 579 """ 580 If *apply_with_init* or *create_init* is `False`, leave __init__ alone. 581 """ 582 @attributes(["a"], apply_with_init=False) 583 class C(object): 584 pass 585 586 @attributes(["a"], create_init=False) 587 class CDeprecated(object): 588 pass 589 590 obj1 = C() 591 obj2 = CDeprecated() 592 593 with pytest.raises(AttributeError): 594 obj1.a 595 with pytest.raises(AttributeError): 596 obj2.a 597 598 def test_wraps_init(self): 599 """ 600 If *create_init* is `True`, build initializer. 601 """ 602 @attributes(["a", "b"], apply_with_init=True) 603 class C(object): 604 pass 605 606 obj = C(a=1, b=2) 607 assert 1 == obj.a 608 assert 2 == obj.b 609 610 def test_immutable(self): 611 """ 612 If *apply_immutable* is `True`, make class immutable. 613 """ 614 @attributes(["a"], apply_immutable=True) 615 class ImmuClass(object): 616 pass 617 618 obj = ImmuClass(a=42) 619 with pytest.raises(AttributeError): 620 obj.a = "23" 621 622 def test_apply_with_cmp(self): 623 """ 624 Don't add cmp methods if *apply_with_cmp* is `False`. 625 """ 626 @attributes(["a"], apply_with_cmp=False) 627 class C(object): 628 pass 629 630 obj = C(a=1) 631 if PY2: 632 assert None is getattr(obj, "__eq__", None) 633 else: 634 assert object.__eq__ == C.__eq__ 635 636 def test_apply_with_repr(self): 637 """ 638 Don't add __repr__ if *apply_with_repr* is `False`. 639 """ 640 @attributes(["a"], apply_with_repr=False) 641 class C(object): 642 pass 643 644 assert repr(C(a=1)).startswith("<test_characteristic.") 645 646 def test_store_attributes(self): 647 """ 648 store_attributes is called on the class to store the attributes that 649 were passed in. 650 """ 651 attrs = [Attribute("a"), Attribute("b")] 652 653 @attributes( 654 attrs, store_attributes=lambda cls, a: setattr(cls, "foo", a) 655 ) 656 class C(object): 657 pass 658 659 assert C.foo == attrs 660 661 def test_store_attributes_stores_Attributes(self): 662 """ 663 The attributes passed to store_attributes are always instances of 664 Attribute, even if they were simple strings when provided. 665 """ 666 @attributes(["a", "b"]) 667 class C(object): 668 pass 669 670 assert C.characteristic_attributes == [Attribute("a"), Attribute("b")] 671 672 def test_store_attributes_defaults_to_characteristic_attributes(self): 673 """ 674 By default, store_attributes stores the attributes in 675 `characteristic_attributes` on the class. 676 """ 677 attrs = [Attribute("a")] 678 679 @attributes(attrs) 680 class C(object): 681 pass 682 683 assert C.characteristic_attributes == attrs 684 685 def test_private(self): 686 """ 687 Integration test for name mangling/aliasing. 688 """ 689 @attributes([Attribute("_a")]) 690 class C(object): 691 pass 692 c = C(a=42) 693 assert 42 == c._a 694 695 def test_private_no_alias(self): 696 """ 697 Integration test for name mangling/aliasing. 698 """ 699 @attributes([Attribute("_a", init_aliaser=None)]) 700 class C(object): 701 pass 702 c = C(_a=42) 703 assert 42 == c._a 704 705 def test_deprecation_create_init(self): 706 """ 707 Emits a DeprecationWarning if `create_init` is used. 708 """ 709 with warnings.catch_warnings(record=True) as w: 710 @attributes(["a"], create_init=False) 711 class C(object): 712 pass 713 assert ( 714 '`create_init` has been deprecated in 14.0, please use ' 715 '`apply_with_init`.' 716 ) == w[0].message.args[0] 717 assert issubclass(w[0].category, DeprecationWarning) 718 719 def test_deprecation_defaults(self): 720 """ 721 Emits a DeprecationWarning if `defaults` is used. 722 """ 723 with warnings.catch_warnings(record=True) as w: 724 @attributes(["a"], defaults={"a": 42}) 725 class C(object): 726 pass 727 assert ( 728 '`defaults` has been deprecated in 14.0, please use the ' 729 '`Attribute` class instead.' 730 ) == w[0].message.args[0] 731 assert issubclass(w[0].category, DeprecationWarning) 732 733 def test_does_not_allow_extra_keyword_arguments(self): 734 """ 735 Keyword arguments other than the ones consumed are still TypeErrors. 736 """ 737 with pytest.raises(TypeError) as e: 738 @attributes(["a"], not_an_arg=12) 739 class C(object): 740 pass 741 assert e.value.args == ( 742 "attributes() got an unexpected keyword argument 'not_an_arg'", 743 ) 744 745 def test_no_attributes(self): 746 """ 747 Specifying no attributes doesn't raise an exception. 748 """ 749 @attributes([]) 750 class C(object): 751 pass 752 C() 753 754 755class TestEnsureAttributes(object): 756 def test_leaves_attribute_alone(self): 757 """ 758 List items that are an Attribute stay an Attribute. 759 """ 760 a = Attribute("a") 761 assert a is _ensure_attributes([a], {})[0] 762 763 def test_converts_rest(self): 764 """ 765 Any other item will be transformed into an Attribute. 766 """ 767 l = _ensure_attributes(["a"], {}) 768 assert isinstance(l[0], Attribute) 769 assert "a" == l[0].name 770 771 def test_defaults(self): 772 """ 773 Legacy defaults are translated into default_value attributes. 774 """ 775 l = _ensure_attributes(["a"], {"a": 42}) 776 assert 42 == l[0].default_value 777 778 def test_defaults_Attribute(self): 779 """ 780 Raises ValueError on defaults != {} and an Attribute within attrs. 781 """ 782 with pytest.raises(ValueError): 783 _ensure_attributes([Attribute("a")], defaults={"a": 42}) 784 785 786class TestImmutable(object): 787 def test_bare(self): 788 """ 789 In an immutable class, setting an definition-time attribute raises an 790 AttributeError. 791 """ 792 @immutable(["foo"]) 793 class ImmuClass(object): 794 foo = "bar" 795 796 i = ImmuClass() 797 with pytest.raises(AttributeError): 798 i.foo = "not bar" 799 800 def test_Attribute(self): 801 """ 802 Mutation is caught if user passes an Attribute instance. 803 """ 804 @immutable([Attribute("foo")]) 805 class ImmuClass(object): 806 def __init__(self): 807 self.foo = "bar" 808 809 i = ImmuClass() 810 with pytest.raises(AttributeError): 811 i.foo = "not bar" 812 813 def test_init(self): 814 """ 815 Changes within __init__ are allowed. 816 """ 817 @immutable(["foo"]) 818 class ImmuClass(object): 819 def __init__(self): 820 self.foo = "bar" 821 822 i = ImmuClass() 823 assert "bar" == i.foo 824 825 def test_with_init(self): 826 """ 827 Changes in with_init's initializer are allowed. 828 """ 829 @immutable(["foo"]) 830 @with_init(["foo"]) 831 class ImmuClass(object): 832 pass 833 834 i = ImmuClass(foo="qux") 835 assert "qux" == i.foo 836 837 def test_Attribute_exclude_from_immutable(self): 838 """ 839 Ignores attribute if exclude_from_immutable=True. 840 """ 841 @immutable([Attribute("a", exclude_from_immutable=True), "b"]) 842 class C(object): 843 def __init__(self, a, b): 844 self.a = a 845 self.b = b 846 847 c = C(1, 2) 848 c.a = 3 849 with pytest.raises(AttributeError): 850 c.b = 4 851 852 853class TestAttrsToScript(object): 854 @pytest.mark.skipif(PY26, reason="Optimization works only on Python 2.7.") 855 def test_optimizes_simple(self): 856 """ 857 If no defaults and extra checks are passed, an optimized version is 858 used on Python 2.7+. 859 """ 860 attrs = [Attribute("a")] 861 script = _attrs_to_script(attrs) 862 assert "except KeyError as e:" in script 863 864 865def test_nothing(): 866 """ 867 ``NOTHING`` has a sensible repr. 868 """ 869 assert "NOTHING" == repr(NOTHING) 870 871 872def test_doc(): 873 """ 874 The characteristic module has a docstring. 875 """ 876 import characteristic 877 assert characteristic.__doc__ 878