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