7
7
8
8
import mypy .plugin # To avoid circular imports.
9
9
from mypy .exprtotype import expr_to_unanalyzed_type , TypeTranslationError
10
- from mypy .lookup import lookup_fully_qualified
11
10
from mypy .nodes import (
12
11
Context , Argument , Var , ARG_OPT , ARG_POS , TypeInfo , AssignmentStmt ,
13
12
TupleExpr , ListExpr , NameExpr , CallExpr , RefExpr , FuncDef ,
@@ -61,10 +60,12 @@ class Converter:
61
60
"""Holds information about a `converter=` argument"""
62
61
63
62
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
67
67
self .is_attr_converters_optional = is_attr_converters_optional
68
+ self .is_invalid_converter = is_invalid_converter
68
69
69
70
70
71
class Attribute :
@@ -89,29 +90,14 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
89
90
90
91
init_type = self .init_type or self .info [self .name ].type
91
92
92
- if self .converter .name :
93
+ if self .converter .type and not self . converter . is_invalid_converter :
93
94
# When a converter is set the init_type is overridden by the first argument
94
95
# 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
111
97
init_type = None
112
98
converter_type = get_proper_type (converter_type )
113
99
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 ]
115
101
elif isinstance (converter_type , Overloaded ):
116
102
types : List [Type ] = []
117
103
for item in converter_type .items :
@@ -124,8 +110,7 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
124
110
types .append (item .arg_types [0 ])
125
111
# Make a union of all the valid types.
126
112
if types :
127
- args = make_simplified_union (types )
128
- init_type = ctx .api .anal_type (args )
113
+ init_type = make_simplified_union (types )
129
114
130
115
if self .converter .is_attr_converters_optional and init_type :
131
116
# If the converter was attr.converter.optional(type) then add None to
@@ -135,9 +120,8 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
135
120
if not init_type :
136
121
ctx .api .fail ("Cannot determine __init__ type from converter" , self .context )
137
122
init_type = AnyType (TypeOfAny .from_error )
138
- elif self .converter .name == '' :
123
+ elif self .converter .is_invalid_converter :
139
124
# This means we had a converter but it's not of a type we can infer.
140
- # Error was shown in _get_converter_name
141
125
init_type = AnyType (TypeOfAny .from_error )
142
126
143
127
if init_type is None :
@@ -170,8 +154,9 @@ def serialize(self) -> JsonDict:
170
154
'has_default' : self .has_default ,
171
155
'init' : self .init ,
172
156
'kw_only' : self .kw_only ,
173
- 'converter_name ' : self .converter .name ,
157
+ 'converter_type ' : self .converter .type . serialize () if self . converter . type else None ,
174
158
'converter_is_attr_converters_optional' : self .converter .is_attr_converters_optional ,
159
+ 'converter_is_invalid_converter' : self .converter .is_invalid_converter ,
175
160
'context_line' : self .context .line ,
176
161
'context_column' : self .context .column ,
177
162
'init_type' : self .init_type .serialize () if self .init_type else None ,
@@ -185,22 +170,26 @@ def deserialize(cls, info: TypeInfo,
185
170
raw_init_type = data ['init_type' ]
186
171
init_type = deserialize_and_fixup_type (raw_init_type , api ) if raw_init_type else None
187
172
173
+ converter_type = None
174
+ if data ['converter_type' ]:
175
+ converter_type = deserialize_and_fixup_type (data ['converter_type' ], api )
188
176
return Attribute (data ['name' ],
189
177
info ,
190
178
data ['has_default' ],
191
179
data ['init' ],
192
180
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' ]),
194
183
Context (line = data ['context_line' ], column = data ['context_column' ]),
195
184
init_type )
196
185
197
186
def expand_typevar_from_subtype (self , sub_type : TypeInfo ) -> None :
198
187
"""Expands type vars in the context of a subtype when an attribute is inherited
199
188
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
204
193
205
194
206
195
def _determine_eq_order (ctx : 'mypy.plugin.ClassDefContext' ) -> bool :
@@ -258,9 +247,19 @@ def _get_decorator_optional_bool_argument(
258
247
return default
259
248
260
249
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
+
261
260
def attr_class_maker_callback (ctx : 'mypy.plugin.ClassDefContext' ,
262
261
auto_attribs_default : Optional [bool ] = False ,
263
- frozen_default : bool = False ) -> None :
262
+ frozen_default : bool = False ) -> bool :
264
263
"""Add necessary dunder methods to classes decorated with attr.s.
265
264
266
265
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',
271
270
into properties.
272
271
273
272
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.
274
276
"""
275
277
info = ctx .cls .info
276
278
@@ -283,30 +285,37 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
283
285
kw_only = _get_decorator_bool_argument (ctx , 'kw_only' , False )
284
286
match_args = _get_decorator_bool_argument (ctx , 'match_args' , True )
285
287
288
+ early_fail = False
286
289
if ctx .api .options .python_version [0 ] < 3 :
287
290
if auto_attribs :
288
291
ctx .api .fail ("auto_attribs is not supported in Python 2" , ctx .reason )
289
- return
292
+ early_fail = True
290
293
if not info .defn .base_type_exprs :
291
294
# Note: This will not catch subclassing old-style classes.
292
295
ctx .api .fail ("attrs only works with new-style classes" , info .defn )
293
- return
296
+ early_fail = True
294
297
if kw_only :
295
298
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
297
308
298
309
attributes = _analyze_class (ctx , auto_attribs , kw_only )
299
310
300
311
# Check if attribute types are ready.
301
312
for attr in attributes :
302
313
node = info .get (attr .name )
303
314
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
310
319
311
320
_add_attrs_magic_attribute (ctx , [(attr .name , info [attr .name ].type ) for attr in attributes ])
312
321
if slots :
@@ -330,6 +339,8 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
330
339
if frozen :
331
340
_make_frozen (ctx , attributes )
332
341
342
+ return True
343
+
333
344
334
345
def _get_frozen (ctx : 'mypy.plugin.ClassDefContext' , frozen_default : bool ) -> bool :
335
346
"""Return whether this class is frozen."""
@@ -423,6 +434,14 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
423
434
return attributes
424
435
425
436
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
+
426
445
def _detect_auto_attribs (ctx : 'mypy.plugin.ClassDefContext' ) -> bool :
427
446
"""Return whether auto_attribs should be enabled or disabled.
428
447
@@ -602,12 +621,13 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext',
602
621
if (isinstance (converter .node , FuncDef )
603
622
and converter .node .type
604
623
and isinstance (converter .node .type , FunctionLike )):
605
- return Converter (converter .node .fullname )
624
+ return Converter (converter .node .type )
606
625
elif (isinstance (converter .node , OverloadedFuncDef )
607
626
and is_valid_overloaded_converter (converter .node )):
608
- return Converter (converter .node .fullname )
627
+ return Converter (converter .node .type )
609
628
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 ))
611
631
612
632
if (isinstance (converter , CallExpr )
613
633
and isinstance (converter .callee , RefExpr )
@@ -625,7 +645,7 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext',
625
645
"Unsupported converter, only named functions and types are currently supported" ,
626
646
converter
627
647
)
628
- return Converter ('' )
648
+ return Converter (None , is_invalid_converter = True )
629
649
return Converter (None )
630
650
631
651
0 commit comments