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