1"""
2Tests for PEP-526 type annotations.
3
4Python 3.6+ only.
5"""
6
7import types
8import typing
9
10import pytest
11
12import attr
13
14from attr._make import _classvar_prefixes
15from attr.exceptions import UnannotatedAttributeError
16
17
18class TestAnnotations:
19    """
20    Tests for types derived from variable annotations (PEP-526).
21    """
22
23    def test_basic_annotations(self):
24        """
25        Sets the `Attribute.type` attr from basic type annotations.
26        """
27        @attr.s
28        class C:
29            x: int = attr.ib()
30            y = attr.ib(type=str)
31            z = attr.ib()
32
33        assert int is attr.fields(C).x.type
34        assert str is attr.fields(C).y.type
35        assert None is attr.fields(C).z.type
36        assert C.__init__.__annotations__ == {
37            'x': int,
38            'y': str,
39            'return': None,
40        }
41
42    def test_catches_basic_type_conflict(self):
43        """
44        Raises ValueError if type is specified both ways.
45        """
46        with pytest.raises(ValueError) as e:
47            @attr.s
48            class C:
49                x: int = attr.ib(type=int)
50
51        assert (
52            "Type annotation and type argument cannot both be present",
53        ) == e.value.args
54
55    def test_typing_annotations(self):
56        """
57        Sets the `Attribute.type` attr from typing annotations.
58        """
59        @attr.s
60        class C:
61            x: typing.List[int] = attr.ib()
62            y = attr.ib(type=typing.Optional[str])
63
64        assert typing.List[int] is attr.fields(C).x.type
65        assert typing.Optional[str] is attr.fields(C).y.type
66        assert C.__init__.__annotations__ == {
67            'x': typing.List[int],
68            'y': typing.Optional[str],
69            'return': None,
70        }
71
72    def test_only_attrs_annotations_collected(self):
73        """
74        Annotations that aren't set to an attr.ib are ignored.
75        """
76        @attr.s
77        class C:
78            x: typing.List[int] = attr.ib()
79            y: int
80
81        assert 1 == len(attr.fields(C))
82        assert C.__init__.__annotations__ == {
83            'x': typing.List[int],
84            'return': None,
85        }
86
87    @pytest.mark.parametrize("slots", [True, False])
88    def test_auto_attribs(self, slots):
89        """
90        If *auto_attribs* is True, bare annotations are collected too.
91        Defaults work and class variables are ignored.
92        """
93        @attr.s(auto_attribs=True, slots=slots)
94        class C:
95            cls_var: typing.ClassVar[int] = 23
96            a: int
97            x: typing.List[int] = attr.Factory(list)
98            y: int = 2
99            z: int = attr.ib(default=3)
100            foo: typing.Any = None
101
102        i = C(42)
103        assert "C(a=42, x=[], y=2, z=3, foo=None)" == repr(i)
104
105        attr_names = set(a.name for a in C.__attrs_attrs__)
106        assert "a" in attr_names  # just double check that the set works
107        assert "cls_var" not in attr_names
108
109        assert int == attr.fields(C).a.type
110
111        assert attr.Factory(list) == attr.fields(C).x.default
112        assert typing.List[int] == attr.fields(C).x.type
113
114        assert int == attr.fields(C).y.type
115        assert 2 == attr.fields(C).y.default
116
117        assert int == attr.fields(C).z.type
118
119        assert typing.Any == attr.fields(C).foo.type
120
121        # Class body is clean.
122        if slots is False:
123            with pytest.raises(AttributeError):
124                C.y
125
126            assert 2 == i.y
127        else:
128            assert isinstance(C.y, types.MemberDescriptorType)
129
130            i.y = 23
131            assert 23 == i.y
132
133        assert C.__init__.__annotations__ == {
134            'a': int,
135            'x': typing.List[int],
136            'y': int,
137            'z': int,
138            'foo': typing.Any,
139            'return': None,
140        }
141
142    @pytest.mark.parametrize("slots", [True, False])
143    def test_auto_attribs_unannotated(self, slots):
144        """
145        Unannotated `attr.ib`s raise an error.
146        """
147        with pytest.raises(UnannotatedAttributeError) as e:
148            @attr.s(slots=slots, auto_attribs=True)
149            class C:
150                v = attr.ib()
151                x: int
152                y = attr.ib()
153                z: str
154
155        assert (
156            "The following `attr.ib`s lack a type annotation: v, y.",
157        ) == e.value.args
158
159    @pytest.mark.parametrize("slots", [True, False])
160    def test_auto_attribs_subclassing(self, slots):
161        """
162        Attributes from super classes are inherited, it doesn't matter if the
163        subclass has annotations or not.
164
165        Ref #291
166        """
167        @attr.s(slots=slots, auto_attribs=True)
168        class A:
169            a: int = 1
170
171        @attr.s(slots=slots, auto_attribs=True)
172        class B(A):
173            b: int = 2
174
175        @attr.s(slots=slots, auto_attribs=True)
176        class C(A):
177            pass
178
179        assert "B(a=1, b=2)" == repr(B())
180        assert "C(a=1)" == repr(C())
181
182        assert A.__init__.__annotations__ == {
183            'a': int,
184            'return': None,
185        }
186        assert B.__init__.__annotations__ == {
187            'a': int,
188            'b': int,
189            'return': None,
190        }
191        assert C.__init__.__annotations__ == {
192            'a': int,
193            'return': None,
194        }
195
196    def test_converter_annotations(self):
197        """
198        Attributes with converters don't have annotations.
199        """
200
201        @attr.s(auto_attribs=True)
202        class A:
203            a: int = attr.ib(converter=int)
204
205        assert A.__init__.__annotations__ == {'return': None}
206
207    @pytest.mark.parametrize("slots", [True, False])
208    @pytest.mark.parametrize("classvar", _classvar_prefixes)
209    def test_annotations_strings(self, slots, classvar):
210        """
211        String annotations are passed into __init__ as is.
212        """
213        @attr.s(auto_attribs=True, slots=slots)
214        class C:
215            cls_var: classvar + '[int]' = 23
216            a: 'int'
217            x: 'typing.List[int]' = attr.Factory(list)
218            y: 'int' = 2
219            z: 'int' = attr.ib(default=3)
220            foo: 'typing.Any' = None
221
222        assert C.__init__.__annotations__ == {
223            'a': 'int',
224            'x': 'typing.List[int]',
225            'y': 'int',
226            'z': 'int',
227            'foo': 'typing.Any',
228            'return': None,
229        }
230