Skip to content

Commit 6e99a2d

Browse files
authored
Add support for next-gen attrs API (#9396)
These include the attr class makers: define, mutable, frozen And the attrib maker: field Also includes support for auto_attribs=None which means auto_detect which method of attributes are being used.
1 parent 849a7f7 commit 6e99a2d

File tree

4 files changed

+219
-7
lines changed

4 files changed

+219
-7
lines changed

mypy/plugins/attrs.py

+46-6
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,18 @@
3838
attr_dataclass_makers = {
3939
'attr.dataclass',
4040
} # type: Final
41+
attr_frozen_makers = {
42+
'attr.frozen'
43+
} # type: Final
44+
attr_define_makers = {
45+
'attr.define',
46+
'attr.mutable'
47+
} # type: Final
4148
attr_attrib_makers = {
4249
'attr.ib',
4350
'attr.attrib',
4451
'attr.attr',
52+
'attr.field',
4553
} # type: Final
4654

4755
SELF_TVAR_NAME = '_AT' # type: Final
@@ -232,7 +240,8 @@ def _get_decorator_optional_bool_argument(
232240

233241

234242
def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
235-
auto_attribs_default: bool = False) -> None:
243+
auto_attribs_default: Optional[bool] = False,
244+
frozen_default: bool = False) -> None:
236245
"""Add necessary dunder methods to classes decorated with attr.s.
237246
238247
attrs is a package that lets you define classes without writing dull boilerplate code.
@@ -247,10 +256,10 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
247256
info = ctx.cls.info
248257

249258
init = _get_decorator_bool_argument(ctx, 'init', True)
250-
frozen = _get_frozen(ctx)
259+
frozen = _get_frozen(ctx, frozen_default)
251260
order = _determine_eq_order(ctx)
252261

253-
auto_attribs = _get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default)
262+
auto_attribs = _get_decorator_optional_bool_argument(ctx, 'auto_attribs', auto_attribs_default)
254263
kw_only = _get_decorator_bool_argument(ctx, 'kw_only', False)
255264

256265
if ctx.api.options.python_version[0] < 3:
@@ -293,9 +302,9 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
293302
_make_frozen(ctx, attributes)
294303

295304

296-
def _get_frozen(ctx: 'mypy.plugin.ClassDefContext') -> bool:
305+
def _get_frozen(ctx: 'mypy.plugin.ClassDefContext', frozen_default: bool) -> bool:
297306
"""Return whether this class is frozen."""
298-
if _get_decorator_bool_argument(ctx, 'frozen', False):
307+
if _get_decorator_bool_argument(ctx, 'frozen', frozen_default):
299308
return True
300309
# Subclasses of frozen classes are frozen so check that.
301310
for super_info in ctx.cls.info.mro[1:-1]:
@@ -305,14 +314,18 @@ def _get_frozen(ctx: 'mypy.plugin.ClassDefContext') -> bool:
305314

306315

307316
def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
308-
auto_attribs: bool,
317+
auto_attribs: Optional[bool],
309318
kw_only: bool) -> List[Attribute]:
310319
"""Analyze the class body of an attr maker, its parents, and return the Attributes found.
311320
312321
auto_attribs=True means we'll generate attributes from type annotations also.
322+
auto_attribs=None means we'll detect which mode to use.
313323
kw_only=True means that all attributes created here will be keyword only args in __init__.
314324
"""
315325
own_attrs = OrderedDict() # type: OrderedDict[str, Attribute]
326+
if auto_attribs is None:
327+
auto_attribs = _detect_auto_attribs(ctx)
328+
316329
# Walk the body looking for assignments and decorators.
317330
for stmt in ctx.cls.defs.body:
318331
if isinstance(stmt, AssignmentStmt):
@@ -380,6 +393,33 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
380393
return attributes
381394

382395

396+
def _detect_auto_attribs(ctx: 'mypy.plugin.ClassDefContext') -> bool:
397+
"""Return whether auto_attribs should be enabled or disabled.
398+
399+
It's disabled if there are any unannotated attribs()
400+
"""
401+
for stmt in ctx.cls.defs.body:
402+
if isinstance(stmt, AssignmentStmt):
403+
for lvalue in stmt.lvalues:
404+
lvalues, rvalues = _parse_assignments(lvalue, stmt)
405+
406+
if len(lvalues) != len(rvalues):
407+
# This means we have some assignment that isn't 1 to 1.
408+
# It can't be an attrib.
409+
continue
410+
411+
for lhs, rvalue in zip(lvalues, rvalues):
412+
# Check if the right hand side is a call to an attribute maker.
413+
if (isinstance(rvalue, CallExpr)
414+
and isinstance(rvalue.callee, RefExpr)
415+
and rvalue.callee.fullname in attr_attrib_makers
416+
and not stmt.new_syntax):
417+
# This means we have an attrib without an annotation and so
418+
# we can't do auto_attribs=True
419+
return False
420+
return True
421+
422+
383423
def _attributes_from_assignment(ctx: 'mypy.plugin.ClassDefContext',
384424
stmt: AssignmentStmt, auto_attribs: bool,
385425
kw_only: bool) -> Iterable[Attribute]:

mypy/plugins/default.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,18 @@ def get_class_decorator_hook(self, fullname: str
9999
elif fullname in attrs.attr_dataclass_makers:
100100
return partial(
101101
attrs.attr_class_maker_callback,
102-
auto_attribs_default=True
102+
auto_attribs_default=True,
103+
)
104+
elif fullname in attrs.attr_frozen_makers:
105+
return partial(
106+
attrs.attr_class_maker_callback,
107+
auto_attribs_default=None,
108+
frozen_default=True,
109+
)
110+
elif fullname in attrs.attr_define_makers:
111+
return partial(
112+
attrs.attr_class_maker_callback,
113+
auto_attribs_default=None,
103114
)
104115
elif fullname in dataclasses.dataclass_makers:
105116
return dataclasses.dataclass_class_maker_callback

test-data/unit/check-attr.test

+40
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,46 @@ class A:
361361
a = A(5)
362362
a.a = 16 # E: Property "a" defined in "A" is read-only
363363
[builtins fixtures/bool.pyi]
364+
[case testAttrsNextGenFrozen]
365+
from attr import frozen, field
366+
367+
@frozen
368+
class A:
369+
a = field()
370+
371+
a = A(5)
372+
a.a = 16 # E: Property "a" defined in "A" is read-only
373+
[builtins fixtures/bool.pyi]
374+
375+
[case testAttrsNextGenDetect]
376+
from attr import define, field
377+
378+
@define
379+
class A:
380+
a = field()
381+
382+
@define
383+
class B:
384+
a: int
385+
386+
@define
387+
class C:
388+
a: int = field()
389+
b = field()
390+
391+
@define
392+
class D:
393+
a: int
394+
b = field()
395+
396+
reveal_type(A) # N: Revealed type is 'def (a: Any) -> __main__.A'
397+
reveal_type(B) # N: Revealed type is 'def (a: builtins.int) -> __main__.B'
398+
reveal_type(C) # N: Revealed type is 'def (a: builtins.int, b: Any) -> __main__.C'
399+
reveal_type(D) # N: Revealed type is 'def (b: Any) -> __main__.D'
400+
401+
[builtins fixtures/bool.pyi]
402+
403+
364404

365405
[case testAttrsDataClass]
366406
import attr

test-data/unit/lib-stub/attr.pyi

+121
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,124 @@ def attrs(maybe_cls: None = ...,
119119
s = attributes = attrs
120120
ib = attr = attrib
121121
dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;)
122+
123+
# Next Generation API
124+
@overload
125+
def define(
126+
maybe_cls: _C,
127+
*,
128+
these: Optional[Mapping[str, Any]] = ...,
129+
repr: bool = ...,
130+
hash: Optional[bool] = ...,
131+
init: bool = ...,
132+
slots: bool = ...,
133+
frozen: bool = ...,
134+
weakref_slot: bool = ...,
135+
str: bool = ...,
136+
auto_attribs: bool = ...,
137+
kw_only: bool = ...,
138+
cache_hash: bool = ...,
139+
auto_exc: bool = ...,
140+
eq: Optional[bool] = ...,
141+
order: Optional[bool] = ...,
142+
auto_detect: bool = ...,
143+
getstate_setstate: Optional[bool] = ...,
144+
on_setattr: Optional[object] = ...,
145+
) -> _C: ...
146+
@overload
147+
def define(
148+
maybe_cls: None = ...,
149+
*,
150+
these: Optional[Mapping[str, Any]] = ...,
151+
repr: bool = ...,
152+
hash: Optional[bool] = ...,
153+
init: bool = ...,
154+
slots: bool = ...,
155+
frozen: bool = ...,
156+
weakref_slot: bool = ...,
157+
str: bool = ...,
158+
auto_attribs: bool = ...,
159+
kw_only: bool = ...,
160+
cache_hash: bool = ...,
161+
auto_exc: bool = ...,
162+
eq: Optional[bool] = ...,
163+
order: Optional[bool] = ...,
164+
auto_detect: bool = ...,
165+
getstate_setstate: Optional[bool] = ...,
166+
on_setattr: Optional[object] = ...,
167+
) -> Callable[[_C], _C]: ...
168+
169+
mutable = define
170+
frozen = define # they differ only in their defaults
171+
172+
@overload
173+
def field(
174+
*,
175+
default: None = ...,
176+
validator: None = ...,
177+
repr: object = ...,
178+
hash: Optional[bool] = ...,
179+
init: bool = ...,
180+
metadata: Optional[Mapping[Any, Any]] = ...,
181+
converter: None = ...,
182+
factory: None = ...,
183+
kw_only: bool = ...,
184+
eq: Optional[bool] = ...,
185+
order: Optional[bool] = ...,
186+
on_setattr: Optional[_OnSetAttrArgType] = ...,
187+
) -> Any: ...
188+
189+
# This form catches an explicit None or no default and infers the type from the
190+
# other arguments.
191+
@overload
192+
def field(
193+
*,
194+
default: None = ...,
195+
validator: Optional[_ValidatorArgType[_T]] = ...,
196+
repr: object = ...,
197+
hash: Optional[bool] = ...,
198+
init: bool = ...,
199+
metadata: Optional[Mapping[Any, Any]] = ...,
200+
converter: Optional[_ConverterType] = ...,
201+
factory: Optional[Callable[[], _T]] = ...,
202+
kw_only: bool = ...,
203+
eq: Optional[bool] = ...,
204+
order: Optional[bool] = ...,
205+
on_setattr: Optional[object] = ...,
206+
) -> _T: ...
207+
208+
# This form catches an explicit default argument.
209+
@overload
210+
def field(
211+
*,
212+
default: _T,
213+
validator: Optional[_ValidatorArgType[_T]] = ...,
214+
repr: object = ...,
215+
hash: Optional[bool] = ...,
216+
init: bool = ...,
217+
metadata: Optional[Mapping[Any, Any]] = ...,
218+
converter: Optional[_ConverterType] = ...,
219+
factory: Optional[Callable[[], _T]] = ...,
220+
kw_only: bool = ...,
221+
eq: Optional[bool] = ...,
222+
order: Optional[bool] = ...,
223+
on_setattr: Optional[object] = ...,
224+
) -> _T: ...
225+
226+
# This form covers type=non-Type: e.g. forward references (str), Any
227+
@overload
228+
def field(
229+
*,
230+
default: Optional[_T] = ...,
231+
validator: Optional[_ValidatorArgType[_T]] = ...,
232+
repr: object = ...,
233+
hash: Optional[bool] = ...,
234+
init: bool = ...,
235+
metadata: Optional[Mapping[Any, Any]] = ...,
236+
converter: Optional[_ConverterType] = ...,
237+
factory: Optional[Callable[[], _T]] = ...,
238+
kw_only: bool = ...,
239+
eq: Optional[bool] = ...,
240+
order: Optional[bool] = ...,
241+
on_setattr: Optional[object] = ...,
242+
) -> Any: ...

0 commit comments

Comments
 (0)