1"""
2Tests for dunder methods from `attrib._make`.
3"""
4
5from __future__ import absolute_import, division, print_function
6
7import copy
8
9import pytest
10
11from hypothesis import given
12from hypothesis.strategies import booleans
13
14import attr
15
16from attr._make import (
17    NOTHING, Factory, _add_init, _add_repr, _Nothing, fields, make_class
18)
19from attr.validators import instance_of
20
21from .utils import simple_attr, simple_class
22
23
24CmpC = simple_class(cmp=True)
25CmpCSlots = simple_class(cmp=True, slots=True)
26ReprC = simple_class(repr=True)
27ReprCSlots = simple_class(repr=True, slots=True)
28
29# HashC is hashable by explicit definition while HashCSlots is hashable
30# implicitly.
31HashC = simple_class(hash=True)
32HashCSlots = simple_class(hash=None, cmp=True, frozen=True, slots=True)
33
34
35class InitC(object):
36    __attrs_attrs__ = [simple_attr("a"), simple_attr("b")]
37
38
39InitC = _add_init(InitC, False)
40
41
42class TestAddCmp(object):
43    """
44    Tests for `_add_cmp`.
45    """
46    @given(booleans())
47    def test_cmp(self, slots):
48        """
49        If `cmp` is False, ignore that attribute.
50        """
51        C = make_class("C", {
52            "a": attr.ib(cmp=False),
53            "b": attr.ib()
54        }, slots=slots)
55
56        assert C(1, 2) == C(2, 2)
57
58    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
59    def test_equal(self, cls):
60        """
61        Equal objects are detected as equal.
62        """
63        assert cls(1, 2) == cls(1, 2)
64        assert not (cls(1, 2) != cls(1, 2))
65
66    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
67    def test_unequal_same_class(self, cls):
68        """
69        Unequal objects of correct type are detected as unequal.
70        """
71        assert cls(1, 2) != cls(2, 1)
72        assert not (cls(1, 2) == cls(2, 1))
73
74    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
75    def test_unequal_different_class(self, cls):
76        """
77        Unequal objects of different type are detected even if their attributes
78        match.
79        """
80        class NotCmpC(object):
81            a = 1
82            b = 2
83        assert cls(1, 2) != NotCmpC()
84        assert not (cls(1, 2) == NotCmpC())
85
86    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
87    def test_lt(self, cls):
88        """
89        __lt__ compares objects as tuples of attribute values.
90        """
91        for a, b in [
92            ((1, 2),  (2, 1)),
93            ((1, 2),  (1, 3)),
94            (("a", "b"), ("b", "a")),
95        ]:
96            assert cls(*a) < cls(*b)
97
98    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
99    def test_lt_unordable(self, cls):
100        """
101        __lt__ returns NotImplemented if classes differ.
102        """
103        assert NotImplemented == (cls(1, 2).__lt__(42))
104
105    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
106    def test_le(self, cls):
107        """
108        __le__ compares objects as tuples of attribute values.
109        """
110        for a, b in [
111            ((1, 2),  (2, 1)),
112            ((1, 2),  (1, 3)),
113            ((1, 1),  (1, 1)),
114            (("a", "b"), ("b", "a")),
115            (("a", "b"), ("a", "b")),
116        ]:
117            assert cls(*a) <= cls(*b)
118
119    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
120    def test_le_unordable(self, cls):
121        """
122        __le__ returns NotImplemented if classes differ.
123        """
124        assert NotImplemented == (cls(1, 2).__le__(42))
125
126    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
127    def test_gt(self, cls):
128        """
129        __gt__ compares objects as tuples of attribute values.
130        """
131        for a, b in [
132            ((2, 1), (1, 2)),
133            ((1, 3), (1, 2)),
134            (("b", "a"), ("a", "b")),
135        ]:
136            assert cls(*a) > cls(*b)
137
138    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
139    def test_gt_unordable(self, cls):
140        """
141        __gt__ returns NotImplemented if classes differ.
142        """
143        assert NotImplemented == (cls(1, 2).__gt__(42))
144
145    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
146    def test_ge(self, cls):
147        """
148        __ge__ compares objects as tuples of attribute values.
149        """
150        for a, b in [
151            ((2, 1), (1, 2)),
152            ((1, 3), (1, 2)),
153            ((1, 1), (1, 1)),
154            (("b", "a"), ("a", "b")),
155            (("a", "b"), ("a", "b")),
156        ]:
157            assert cls(*a) >= cls(*b)
158
159    @pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
160    def test_ge_unordable(self, cls):
161        """
162        __ge__ returns NotImplemented if classes differ.
163        """
164        assert NotImplemented == (cls(1, 2).__ge__(42))
165
166
167class TestAddRepr(object):
168    """
169    Tests for `_add_repr`.
170    """
171    @pytest.mark.parametrize("slots", [True, False])
172    def test_repr(self, slots):
173        """
174        If `repr` is False, ignore that attribute.
175        """
176        C = make_class("C", {
177            "a": attr.ib(repr=False),
178            "b": attr.ib()
179        }, slots=slots)
180
181        assert "C(b=2)" == repr(C(1, 2))
182
183    @pytest.mark.parametrize("cls", [ReprC, ReprCSlots])
184    def test_repr_works(self, cls):
185        """
186        repr returns a sensible value.
187        """
188        assert "C(a=1, b=2)" == repr(cls(1, 2))
189
190    def test_infinite_recursion(self):
191        """
192        In the presence of a cyclic graph, repr will emit an ellipsis and not
193        raise an exception.
194        """
195        @attr.s
196        class Cycle(object):
197            value = attr.ib(default=7)
198            cycle = attr.ib(default=None)
199
200        cycle = Cycle()
201        cycle.cycle = cycle
202        assert "Cycle(value=7, cycle=...)" == repr(cycle)
203
204    def test_underscores(self):
205        """
206        repr does not strip underscores.
207        """
208        class C(object):
209            __attrs_attrs__ = [simple_attr("_x")]
210
211        C = _add_repr(C)
212        i = C()
213        i._x = 42
214
215        assert "C(_x=42)" == repr(i)
216
217    def test_repr_uninitialized_member(self):
218        """
219        repr signals unset attributes
220        """
221        C = make_class("C", {
222            "a": attr.ib(init=False),
223        })
224
225        assert "C(a=NOTHING)" == repr(C())
226
227    @given(add_str=booleans(), slots=booleans())
228    def test_str(self, add_str, slots):
229        """
230        If str is True, it returns the same as repr.
231
232        This only makes sense when subclassing a class with an poor __str__
233        (like Exceptions).
234        """
235        @attr.s(str=add_str, slots=slots)
236        class Error(Exception):
237            x = attr.ib()
238
239        e = Error(42)
240
241        assert (str(e) == repr(e)) is add_str
242
243    def test_str_no_repr(self):
244        """
245        Raises a ValueError if repr=False and str=True.
246        """
247        with pytest.raises(ValueError) as e:
248            simple_class(repr=False, str=True)
249
250        assert (
251            "__str__ can only be generated if a __repr__ exists."
252        ) == e.value.args[0]
253
254
255class TestAddHash(object):
256    """
257    Tests for `_add_hash`.
258    """
259    def test_enforces_type(self):
260        """
261        The `hash` argument to both attrs and attrib must be None, True, or
262        False.
263        """
264        exc_args = ("Invalid value for hash.  Must be True, False, or None.",)
265
266        with pytest.raises(TypeError) as e:
267            make_class("C", {}, hash=1),
268
269        assert exc_args == e.value.args
270
271        with pytest.raises(TypeError) as e:
272            make_class("C", {"a": attr.ib(hash=1)}),
273
274        assert exc_args == e.value.args
275
276    @given(booleans())
277    def test_hash_attribute(self, slots):
278        """
279        If `hash` is False on an attribute, ignore that attribute.
280        """
281        C = make_class("C", {"a": attr.ib(hash=False), "b": attr.ib()},
282                       slots=slots, hash=True)
283
284        assert hash(C(1, 2)) == hash(C(2, 2))
285
286    @given(booleans())
287    def test_hash_attribute_mirrors_cmp(self, cmp):
288        """
289        If `hash` is None, the hash generation mirrors `cmp`.
290        """
291        C = make_class("C", {"a": attr.ib(cmp=cmp)}, cmp=True, frozen=True)
292
293        if cmp:
294            assert C(1) != C(2)
295            assert hash(C(1)) != hash(C(2))
296            assert hash(C(1)) == hash(C(1))
297        else:
298            assert C(1) == C(2)
299            assert hash(C(1)) == hash(C(2))
300
301    @given(booleans())
302    def test_hash_mirrors_cmp(self, cmp):
303        """
304        If `hash` is None, the hash generation mirrors `cmp`.
305        """
306        C = make_class("C", {"a": attr.ib()}, cmp=cmp, frozen=True)
307
308        i = C(1)
309
310        assert i == i
311        assert hash(i) == hash(i)
312
313        if cmp:
314            assert C(1) == C(1)
315            assert hash(C(1)) == hash(C(1))
316        else:
317            assert C(1) != C(1)
318            assert hash(C(1)) != hash(C(1))
319
320    @pytest.mark.parametrize("cls", [HashC, HashCSlots])
321    def test_hash_works(self, cls):
322        """
323        __hash__ returns different hashes for different values.
324        """
325        assert hash(cls(1, 2)) != hash(cls(1, 1))
326
327    def test_hash_default(self):
328        """
329        Classes are not hashable by default.
330        """
331        C = make_class("C", {})
332
333        with pytest.raises(TypeError) as e:
334            hash(C())
335
336        assert e.value.args[0] in (
337            "'C' objects are unhashable",  # PyPy
338            "unhashable type: 'C'",  # CPython
339        )
340
341
342class TestAddInit(object):
343    """
344    Tests for `_add_init`.
345    """
346    @given(booleans(), booleans())
347    def test_init(self, slots, frozen):
348        """
349        If `init` is False, ignore that attribute.
350        """
351        C = make_class("C", {"a": attr.ib(init=False), "b": attr.ib()},
352                       slots=slots, frozen=frozen)
353        with pytest.raises(TypeError) as e:
354            C(a=1, b=2)
355
356        assert (
357            "__init__() got an unexpected keyword argument 'a'" ==
358            e.value.args[0]
359        )
360
361    @given(booleans(), booleans())
362    def test_no_init_default(self, slots, frozen):
363        """
364        If `init` is False but a Factory is specified, don't allow passing that
365        argument but initialize it anyway.
366        """
367        C = make_class("C", {
368            "_a": attr.ib(init=False, default=42),
369            "_b": attr.ib(init=False, default=Factory(list)),
370            "c": attr.ib()
371        }, slots=slots, frozen=frozen)
372        with pytest.raises(TypeError):
373            C(a=1, c=2)
374        with pytest.raises(TypeError):
375            C(b=1, c=2)
376
377        i = C(23)
378        assert (42, [], 23) == (i._a, i._b, i.c)
379
380    @given(booleans(), booleans())
381    def test_no_init_order(self, slots, frozen):
382        """
383        If an attribute is `init=False`, it's legal to come after a mandatory
384        attribute.
385        """
386        make_class("C", {
387            "a": attr.ib(default=Factory(list)),
388            "b": attr.ib(init=False),
389        }, slots=slots, frozen=frozen)
390
391    def test_sets_attributes(self):
392        """
393        The attributes are initialized using the passed keywords.
394        """
395        obj = InitC(a=1, b=2)
396        assert 1 == obj.a
397        assert 2 == obj.b
398
399    def test_default(self):
400        """
401        If a default value is present, it's used as fallback.
402        """
403        class C(object):
404            __attrs_attrs__ = [
405                simple_attr(name="a", default=2),
406                simple_attr(name="b", default="hallo"),
407                simple_attr(name="c", default=None),
408            ]
409
410        C = _add_init(C, False)
411        i = C()
412        assert 2 == i.a
413        assert "hallo" == i.b
414        assert None is i.c
415
416    def test_factory(self):
417        """
418        If a default factory is present, it's used as fallback.
419        """
420        class D(object):
421            pass
422
423        class C(object):
424            __attrs_attrs__ = [
425                simple_attr(name="a", default=Factory(list)),
426                simple_attr(name="b", default=Factory(D)),
427            ]
428        C = _add_init(C, False)
429        i = C()
430
431        assert [] == i.a
432        assert isinstance(i.b, D)
433
434    def test_validator(self):
435        """
436        If a validator is passed, call it with the preliminary instance, the
437        Attribute, and the argument.
438        """
439        class VException(Exception):
440            pass
441
442        def raiser(*args):
443            raise VException(*args)
444
445        C = make_class("C", {"a": attr.ib("a", validator=raiser)})
446        with pytest.raises(VException) as e:
447            C(42)
448
449        assert (fields(C).a, 42,) == e.value.args[1:]
450        assert isinstance(e.value.args[0], C)
451
452    def test_validator_slots(self):
453        """
454        If a validator is passed, call it with the preliminary instance, the
455        Attribute, and the argument.
456        """
457        class VException(Exception):
458            pass
459
460        def raiser(*args):
461            raise VException(*args)
462
463        C = make_class("C", {"a": attr.ib("a", validator=raiser)}, slots=True)
464        with pytest.raises(VException) as e:
465            C(42)
466
467        assert (fields(C)[0], 42,) == e.value.args[1:]
468        assert isinstance(e.value.args[0], C)
469
470    @given(booleans())
471    def test_validator_others(self, slots):
472        """
473        Does not interfere when setting non-attrs attributes.
474        """
475        C = make_class("C", {
476            "a": attr.ib("a", validator=instance_of(int))
477        }, slots=slots)
478        i = C(1)
479
480        assert 1 == i.a
481
482        if not slots:
483            i.b = "foo"
484            assert "foo" == i.b
485        else:
486            with pytest.raises(AttributeError):
487                i.b = "foo"
488
489    def test_underscores(self):
490        """
491        The argument names in `__init__` are without leading and trailing
492        underscores.
493        """
494        class C(object):
495            __attrs_attrs__ = [simple_attr("_private")]
496
497        C = _add_init(C, False)
498        i = C(private=42)
499        assert 42 == i._private
500
501
502class TestNothing(object):
503    """
504    Tests for `_Nothing`.
505    """
506    def test_copy(self):
507        """
508        __copy__ returns the same object.
509        """
510        n = _Nothing()
511        assert n is copy.copy(n)
512
513    def test_deepcopy(self):
514        """
515        __deepcopy__ returns the same object.
516        """
517        n = _Nothing()
518        assert n is copy.deepcopy(n)
519
520    def test_eq(self):
521        """
522        All instances are equal.
523        """
524        assert _Nothing() == _Nothing() == NOTHING
525        assert not (_Nothing() != _Nothing())
526        assert 1 != _Nothing()
527