Skip to content

Commit 50a653e

Browse files
authored
Fix forward references and generic inheritance in attrs classes (#12772)
Move the attrs plugin to a later pass, so that we won't have placeholders. Fix various issues related to forward references and generic inheritance, including some crashes. This is follow-up to #12762 and related to #12656 and #12633.
1 parent 5ceaf3d commit 50a653e

File tree

4 files changed

+235
-62
lines changed

4 files changed

+235
-62
lines changed

mypy/plugins/attrs.py

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import mypy.plugin # To avoid circular imports.
99
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
10-
from mypy.lookup import lookup_fully_qualified
1110
from mypy.nodes import (
1211
Context, Argument, Var, ARG_OPT, ARG_POS, TypeInfo, AssignmentStmt,
1312
TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, FuncDef,
@@ -61,10 +60,12 @@ class Converter:
6160
"""Holds information about a `converter=` argument"""
6261

6362
def __init__(self,
64-
name: Optional[str] = None,
65-
is_attr_converters_optional: bool = False) -> None:
66-
self.name = name
63+
type: Optional[Type] = None,
64+
is_attr_converters_optional: bool = False,
65+
is_invalid_converter: bool = False) -> None:
66+
self.type = type
6767
self.is_attr_converters_optional = is_attr_converters_optional
68+
self.is_invalid_converter = is_invalid_converter
6869

6970

7071
class Attribute:
@@ -89,29 +90,14 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
8990

9091
init_type = self.init_type or self.info[self.name].type
9192

92-
if self.converter.name:
93+
if self.converter.type and not self.converter.is_invalid_converter:
9394
# When a converter is set the init_type is overridden by the first argument
9495
# of the converter method.
95-
converter = lookup_fully_qualified(self.converter.name, ctx.api.modules,
96-
raise_on_missing=False)
97-
if not converter:
98-
# The converter may be a local variable. Check there too.
99-
converter = ctx.api.lookup_qualified(self.converter.name, self.info, True)
100-
101-
# Get the type of the converter.
102-
converter_type: Optional[Type] = None
103-
if converter and isinstance(converter.node, TypeInfo):
104-
from mypy.checkmember import type_object_type # To avoid import cycle.
105-
converter_type = type_object_type(converter.node, ctx.api.named_type)
106-
elif converter and isinstance(converter.node, OverloadedFuncDef):
107-
converter_type = converter.node.type
108-
elif converter and converter.type:
109-
converter_type = converter.type
110-
96+
converter_type = self.converter.type
11197
init_type = None
11298
converter_type = get_proper_type(converter_type)
11399
if isinstance(converter_type, CallableType) and converter_type.arg_types:
114-
init_type = ctx.api.anal_type(converter_type.arg_types[0])
100+
init_type = converter_type.arg_types[0]
115101
elif isinstance(converter_type, Overloaded):
116102
types: List[Type] = []
117103
for item in converter_type.items:
@@ -124,8 +110,7 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
124110
types.append(item.arg_types[0])
125111
# Make a union of all the valid types.
126112
if types:
127-
args = make_simplified_union(types)
128-
init_type = ctx.api.anal_type(args)
113+
init_type = make_simplified_union(types)
129114

130115
if self.converter.is_attr_converters_optional and init_type:
131116
# If the converter was attr.converter.optional(type) then add None to
@@ -135,9 +120,8 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
135120
if not init_type:
136121
ctx.api.fail("Cannot determine __init__ type from converter", self.context)
137122
init_type = AnyType(TypeOfAny.from_error)
138-
elif self.converter.name == '':
123+
elif self.converter.is_invalid_converter:
139124
# This means we had a converter but it's not of a type we can infer.
140-
# Error was shown in _get_converter_name
141125
init_type = AnyType(TypeOfAny.from_error)
142126

143127
if init_type is None:
@@ -170,8 +154,9 @@ def serialize(self) -> JsonDict:
170154
'has_default': self.has_default,
171155
'init': self.init,
172156
'kw_only': self.kw_only,
173-
'converter_name': self.converter.name,
157+
'converter_type': self.converter.type.serialize() if self.converter.type else None,
174158
'converter_is_attr_converters_optional': self.converter.is_attr_converters_optional,
159+
'converter_is_invalid_converter': self.converter.is_invalid_converter,
175160
'context_line': self.context.line,
176161
'context_column': self.context.column,
177162
'init_type': self.init_type.serialize() if self.init_type else None,
@@ -185,22 +170,26 @@ def deserialize(cls, info: TypeInfo,
185170
raw_init_type = data['init_type']
186171
init_type = deserialize_and_fixup_type(raw_init_type, api) if raw_init_type else None
187172

173+
converter_type = None
174+
if data['converter_type']:
175+
converter_type = deserialize_and_fixup_type(data['converter_type'], api)
188176
return Attribute(data['name'],
189177
info,
190178
data['has_default'],
191179
data['init'],
192180
data['kw_only'],
193-
Converter(data['converter_name'], data['converter_is_attr_converters_optional']),
181+
Converter(converter_type, data['converter_is_attr_converters_optional'],
182+
data['converter_is_invalid_converter']),
194183
Context(line=data['context_line'], column=data['context_column']),
195184
init_type)
196185

197186
def expand_typevar_from_subtype(self, sub_type: TypeInfo) -> None:
198187
"""Expands type vars in the context of a subtype when an attribute is inherited
199188
from a generic super type."""
200-
if not isinstance(self.init_type, TypeVarType):
201-
return
202-
203-
self.init_type = map_type_from_supertype(self.init_type, sub_type, self.info)
189+
if self.init_type:
190+
self.init_type = map_type_from_supertype(self.init_type, sub_type, self.info)
191+
else:
192+
self.init_type = None
204193

205194

206195
def _determine_eq_order(ctx: 'mypy.plugin.ClassDefContext') -> bool:
@@ -258,9 +247,19 @@ def _get_decorator_optional_bool_argument(
258247
return default
259248

260249

250+
def attr_tag_callback(ctx: 'mypy.plugin.ClassDefContext') -> None:
251+
"""Record that we have an attrs class in the main semantic analysis pass.
252+
253+
The later pass implemented by attr_class_maker_callback will use this
254+
to detect attrs lasses in base classes.
255+
"""
256+
# The value is ignored, only the existence matters.
257+
ctx.cls.info.metadata['attrs_tag'] = {}
258+
259+
261260
def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
262261
auto_attribs_default: Optional[bool] = False,
263-
frozen_default: bool = False) -> None:
262+
frozen_default: bool = False) -> bool:
264263
"""Add necessary dunder methods to classes decorated with attr.s.
265264
266265
attrs is a package that lets you define classes without writing dull boilerplate code.
@@ -271,6 +270,9 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
271270
into properties.
272271
273272
See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works.
273+
274+
If this returns False, some required metadata was not ready yet and we need another
275+
pass.
274276
"""
275277
info = ctx.cls.info
276278

@@ -283,30 +285,37 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
283285
kw_only = _get_decorator_bool_argument(ctx, 'kw_only', False)
284286
match_args = _get_decorator_bool_argument(ctx, 'match_args', True)
285287

288+
early_fail = False
286289
if ctx.api.options.python_version[0] < 3:
287290
if auto_attribs:
288291
ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason)
289-
return
292+
early_fail = True
290293
if not info.defn.base_type_exprs:
291294
# Note: This will not catch subclassing old-style classes.
292295
ctx.api.fail("attrs only works with new-style classes", info.defn)
293-
return
296+
early_fail = True
294297
if kw_only:
295298
ctx.api.fail(KW_ONLY_PYTHON_2_UNSUPPORTED, ctx.reason)
296-
return
299+
early_fail = True
300+
if early_fail:
301+
_add_empty_metadata(info)
302+
return True
303+
304+
for super_info in ctx.cls.info.mro[1:-1]:
305+
if 'attrs_tag' in super_info.metadata and 'attrs' not in super_info.metadata:
306+
# Super class is not ready yet. Request another pass.
307+
return False
297308

298309
attributes = _analyze_class(ctx, auto_attribs, kw_only)
299310

300311
# Check if attribute types are ready.
301312
for attr in attributes:
302313
node = info.get(attr.name)
303314
if node is None:
304-
# This name is likely blocked by a star import. We don't need to defer because
305-
# defer() is already called by mark_incomplete().
306-
return
307-
if node.type is None and not ctx.api.final_iteration:
308-
ctx.api.defer()
309-
return
315+
# This name is likely blocked by some semantic analysis error that
316+
# should have been reported already.
317+
_add_empty_metadata(info)
318+
return True
310319

311320
_add_attrs_magic_attribute(ctx, [(attr.name, info[attr.name].type) for attr in attributes])
312321
if slots:
@@ -330,6 +339,8 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
330339
if frozen:
331340
_make_frozen(ctx, attributes)
332341

342+
return True
343+
333344

334345
def _get_frozen(ctx: 'mypy.plugin.ClassDefContext', frozen_default: bool) -> bool:
335346
"""Return whether this class is frozen."""
@@ -423,6 +434,14 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
423434
return attributes
424435

425436

437+
def _add_empty_metadata(info: TypeInfo) -> None:
438+
"""Add empty metadata to mark that we've finished processing this class."""
439+
info.metadata['attrs'] = {
440+
'attributes': [],
441+
'frozen': False,
442+
}
443+
444+
426445
def _detect_auto_attribs(ctx: 'mypy.plugin.ClassDefContext') -> bool:
427446
"""Return whether auto_attribs should be enabled or disabled.
428447
@@ -602,12 +621,13 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext',
602621
if (isinstance(converter.node, FuncDef)
603622
and converter.node.type
604623
and isinstance(converter.node.type, FunctionLike)):
605-
return Converter(converter.node.fullname)
624+
return Converter(converter.node.type)
606625
elif (isinstance(converter.node, OverloadedFuncDef)
607626
and is_valid_overloaded_converter(converter.node)):
608-
return Converter(converter.node.fullname)
627+
return Converter(converter.node.type)
609628
elif isinstance(converter.node, TypeInfo):
610-
return Converter(converter.node.fullname)
629+
from mypy.checkmember import type_object_type # To avoid import cycle.
630+
return Converter(type_object_type(converter.node, ctx.api.named_type))
611631

612632
if (isinstance(converter, CallExpr)
613633
and isinstance(converter.callee, RefExpr)
@@ -625,7 +645,7 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext',
625645
"Unsupported converter, only named functions and types are currently supported",
626646
converter
627647
)
628-
return Converter('')
648+
return Converter(None, is_invalid_converter=True)
629649
return Converter(None)
630650

631651

mypy/plugins/default.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,34 @@ def get_attribute_hook(self, fullname: str
9494

9595
def get_class_decorator_hook(self, fullname: str
9696
) -> Optional[Callable[[ClassDefContext], None]]:
97+
from mypy.plugins import dataclasses
9798
from mypy.plugins import attrs
99+
100+
# These dataclass and attrs hooks run in the main semantic analysis pass
101+
# and only tag known dataclasses/attrs classes, so that the second
102+
# hooks (in get_class_decorator_hook_2) can detect dataclasses/attrs classes
103+
# in the MRO.
104+
if fullname in dataclasses.dataclass_makers:
105+
return dataclasses.dataclass_tag_callback
106+
if (fullname in attrs.attr_class_makers
107+
or fullname in attrs.attr_dataclass_makers
108+
or fullname in attrs.attr_frozen_makers
109+
or fullname in attrs.attr_define_makers):
110+
return attrs.attr_tag_callback
111+
112+
return None
113+
114+
def get_class_decorator_hook_2(self, fullname: str
115+
) -> Optional[Callable[[ClassDefContext], bool]]:
98116
from mypy.plugins import dataclasses
117+
from mypy.plugins import functools
118+
from mypy.plugins import attrs
99119

100-
if fullname in attrs.attr_class_makers:
120+
if fullname in dataclasses.dataclass_makers:
121+
return dataclasses.dataclass_class_maker_callback
122+
elif fullname in functools.functools_total_ordering_makers:
123+
return functools.functools_total_ordering_maker_callback
124+
elif fullname in attrs.attr_class_makers:
101125
return attrs.attr_class_maker_callback
102126
elif fullname in attrs.attr_dataclass_makers:
103127
return partial(
@@ -115,20 +139,6 @@ def get_class_decorator_hook(self, fullname: str
115139
attrs.attr_class_maker_callback,
116140
auto_attribs_default=None,
117141
)
118-
elif fullname in dataclasses.dataclass_makers:
119-
return dataclasses.dataclass_tag_callback
120-
121-
return None
122-
123-
def get_class_decorator_hook_2(self, fullname: str
124-
) -> Optional[Callable[[ClassDefContext], bool]]:
125-
from mypy.plugins import dataclasses
126-
from mypy.plugins import functools
127-
128-
if fullname in dataclasses.dataclass_makers:
129-
return dataclasses.dataclass_class_maker_callback
130-
elif fullname in functools.functools_total_ordering_makers:
131-
return functools.functools_total_ordering_maker_callback
132142

133143
return None
134144

0 commit comments

Comments
 (0)