2
2
3
3
from __future__ import annotations
4
4
5
- from typing import Final
5
+ from typing import Collection , Final
6
6
7
7
from mypy import errorcodes as codes , message_registry
8
8
from mypy .errorcodes import ErrorCode
@@ -97,21 +97,23 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
97
97
existing_info = None
98
98
if isinstance (defn .analyzed , TypedDictExpr ):
99
99
existing_info = defn .analyzed .info
100
+
101
+ field_types : dict [str , Type ] | None
100
102
if (
101
103
len (defn .base_type_exprs ) == 1
102
104
and isinstance (defn .base_type_exprs [0 ], RefExpr )
103
105
and defn .base_type_exprs [0 ].fullname in TPDICT_NAMES
104
106
):
105
107
# Building a new TypedDict
106
- fields , types , statements , required_keys , readonly_keys = (
108
+ field_types , statements , required_keys , readonly_keys = (
107
109
self .analyze_typeddict_classdef_fields (defn )
108
110
)
109
- if fields is None :
111
+ if field_types is None :
110
112
return True , None # Defer
111
113
if self .api .is_func_scope () and "@" not in defn .name :
112
114
defn .name += "@" + str (defn .line )
113
115
info = self .build_typeddict_typeinfo (
114
- defn .name , fields , types , required_keys , readonly_keys , defn .line , existing_info
116
+ defn .name , field_types , required_keys , readonly_keys , defn .line , existing_info
115
117
)
116
118
defn .analyzed = TypedDictExpr (info )
117
119
defn .analyzed .line = defn .line
@@ -154,26 +156,24 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
154
156
else :
155
157
self .fail ("All bases of a new TypedDict must be TypedDict types" , defn )
156
158
157
- keys : list [str ] = []
158
- types = []
159
+ field_types = {}
159
160
required_keys = set ()
160
161
readonly_keys = set ()
161
162
# Iterate over bases in reverse order so that leftmost base class' keys take precedence
162
163
for base in reversed (typeddict_bases ):
163
164
self .add_keys_and_types_from_base (
164
- base , keys , types , required_keys , readonly_keys , defn
165
+ base , field_types , required_keys , readonly_keys , defn
165
166
)
166
- (new_keys , new_types , new_statements , new_required_keys , new_readonly_keys ) = (
167
- self .analyze_typeddict_classdef_fields (defn , keys )
167
+ (new_field_types , new_statements , new_required_keys , new_readonly_keys ) = (
168
+ self .analyze_typeddict_classdef_fields (defn , oldfields = field_types )
168
169
)
169
- if new_keys is None :
170
+ if new_field_types is None :
170
171
return True , None # Defer
171
- keys .extend (new_keys )
172
- types .extend (new_types )
172
+ field_types .update (new_field_types )
173
173
required_keys .update (new_required_keys )
174
174
readonly_keys .update (new_readonly_keys )
175
175
info = self .build_typeddict_typeinfo (
176
- defn .name , keys , types , required_keys , readonly_keys , defn .line , existing_info
176
+ defn .name , field_types , required_keys , readonly_keys , defn .line , existing_info
177
177
)
178
178
defn .analyzed = TypedDictExpr (info )
179
179
defn .analyzed .line = defn .line
@@ -184,8 +184,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
184
184
def add_keys_and_types_from_base (
185
185
self ,
186
186
base : Expression ,
187
- keys : list [str ],
188
- types : list [Type ],
187
+ field_types : dict [str , Type ],
189
188
required_keys : set [str ],
190
189
readonly_keys : set [str ],
191
190
ctx : Context ,
@@ -224,10 +223,10 @@ def add_keys_and_types_from_base(
224
223
with state .strict_optional_set (self .options .strict_optional ):
225
224
valid_items = self .map_items_to_base (valid_items , tvars , base_args )
226
225
for key in base_items :
227
- if key in keys :
226
+ if key in field_types :
228
227
self .fail (TYPEDDICT_OVERRIDE_MERGE .format (key ), ctx )
229
- keys . extend ( valid_items . keys ())
230
- types . extend (valid_items . values () )
228
+
229
+ field_types . update (valid_items )
231
230
required_keys .update (base_typed_dict .required_keys )
232
231
readonly_keys .update (base_typed_dict .readonly_keys )
233
232
@@ -280,8 +279,8 @@ def map_items_to_base(
280
279
return mapped_items
281
280
282
281
def analyze_typeddict_classdef_fields (
283
- self , defn : ClassDef , oldfields : list [str ] | None = None
284
- ) -> tuple [list [str ] | None , list [ Type ] , list [Statement ], set [str ], set [str ]]:
282
+ self , defn : ClassDef , oldfields : Collection [str ] | None = None
283
+ ) -> tuple [dict [str , Type ] | None , list [Statement ], set [str ], set [str ]]:
285
284
"""Analyze fields defined in a TypedDict class definition.
286
285
287
286
This doesn't consider inherited fields (if any). Also consider totality,
@@ -294,9 +293,19 @@ def analyze_typeddict_classdef_fields(
294
293
part of a TypedDict definition
295
294
* Set of required keys
296
295
"""
297
- fields : list [str ] = []
298
- types : list [Type ] = []
296
+ fields : dict [str , Type ] = {}
297
+ readonly_keys = set ()
298
+ required_keys = set ()
299
299
statements : list [Statement ] = []
300
+
301
+ total : bool | None = True
302
+ for key in defn .keywords :
303
+ if key == "total" :
304
+ total = require_bool_literal_argument (self .api , defn .keywords ["total" ], "total" , True )
305
+ continue
306
+ for_function = ' for "__init_subclass__" of "TypedDict"'
307
+ self .msg .unexpected_keyword_argument_for_function (for_function , key , defn )
308
+
300
309
for stmt in defn .defs .body :
301
310
if not isinstance (stmt , AssignmentStmt ):
302
311
# Still allow pass or ... (for empty TypedDict's) and docstrings
@@ -320,10 +329,11 @@ def analyze_typeddict_classdef_fields(
320
329
self .fail (f'Duplicate TypedDict key "{ name } "' , stmt )
321
330
continue
322
331
# Append stmt, name, and type in this case...
323
- fields .append (name )
324
332
statements .append (stmt )
333
+
334
+ field_type : Type
325
335
if stmt .unanalyzed_type is None :
326
- types . append ( AnyType (TypeOfAny .unannotated ) )
336
+ field_type = AnyType (TypeOfAny .unannotated )
327
337
else :
328
338
analyzed = self .api .anal_type (
329
339
stmt .unanalyzed_type ,
@@ -333,38 +343,27 @@ def analyze_typeddict_classdef_fields(
333
343
prohibit_special_class_field_types = "TypedDict" ,
334
344
)
335
345
if analyzed is None :
336
- return None , [], [], set (), set () # Need to defer
337
- types . append ( analyzed )
346
+ return None , [], set (), set () # Need to defer
347
+ field_type = analyzed
338
348
if not has_placeholder (analyzed ):
339
349
stmt .type = self .extract_meta_info (analyzed , stmt )[0 ]
350
+
351
+ field_type , required , readonly = self .extract_meta_info (field_type )
352
+ fields [name ] = field_type
353
+
354
+ if (total or required is True ) and required is not False :
355
+ required_keys .add (name )
356
+ if readonly :
357
+ readonly_keys .add (name )
358
+
340
359
# ...despite possible minor failures that allow further analysis.
341
360
if stmt .type is None or hasattr (stmt , "new_syntax" ) and not stmt .new_syntax :
342
361
self .fail (TPDICT_CLASS_ERROR , stmt )
343
362
elif not isinstance (stmt .rvalue , TempNode ):
344
363
# x: int assigns rvalue to TempNode(AnyType())
345
364
self .fail ("Right hand side values are not supported in TypedDict" , stmt )
346
- total : bool | None = True
347
- if "total" in defn .keywords :
348
- total = require_bool_literal_argument (self .api , defn .keywords ["total" ], "total" , True )
349
- if defn .keywords and defn .keywords .keys () != {"total" }:
350
- for_function = ' for "__init_subclass__" of "TypedDict"'
351
- for key in defn .keywords :
352
- if key == "total" :
353
- continue
354
- self .msg .unexpected_keyword_argument_for_function (for_function , key , defn )
355
-
356
- res_types = []
357
- readonly_keys = set ()
358
- required_keys = set ()
359
- for field , t in zip (fields , types ):
360
- typ , required , readonly = self .extract_meta_info (t )
361
- res_types .append (typ )
362
- if (total or required is True ) and required is not False :
363
- required_keys .add (field )
364
- if readonly :
365
- readonly_keys .add (field )
366
365
367
- return fields , res_types , statements , required_keys , readonly_keys
366
+ return fields , statements , required_keys , readonly_keys
368
367
369
368
def extract_meta_info (
370
369
self , typ : Type , context : Context | None = None
@@ -433,7 +432,7 @@ def check_typeddict(
433
432
name += "@" + str (call .line )
434
433
else :
435
434
name = var_name = "TypedDict@" + str (call .line )
436
- info = self .build_typeddict_typeinfo (name , [], [] , set (), set (), call .line , None )
435
+ info = self .build_typeddict_typeinfo (name , {} , set (), set (), call .line , None )
437
436
else :
438
437
if var_name is not None and name != var_name :
439
438
self .fail (
@@ -473,7 +472,7 @@ def check_typeddict(
473
472
if isinstance (node .analyzed , TypedDictExpr ):
474
473
existing_info = node .analyzed .info
475
474
info = self .build_typeddict_typeinfo (
476
- name , items , types , required_keys , readonly_keys , call .line , existing_info
475
+ name , dict ( zip ( items , types )) , required_keys , readonly_keys , call .line , existing_info
477
476
)
478
477
info .line = node .line
479
478
# Store generated TypeInfo under both names, see semanal_namedtuple for more details.
@@ -578,8 +577,7 @@ def fail_typeddict_arg(
578
577
def build_typeddict_typeinfo (
579
578
self ,
580
579
name : str ,
581
- items : list [str ],
582
- types : list [Type ],
580
+ item_types : dict [str , Type ],
583
581
required_keys : set [str ],
584
582
readonly_keys : set [str ],
585
583
line : int ,
@@ -594,7 +592,7 @@ def build_typeddict_typeinfo(
594
592
assert fallback is not None
595
593
info = existing_info or self .api .basic_new_typeinfo (name , fallback , line )
596
594
typeddict_type = TypedDictType (
597
- dict ( zip ( items , types )) , required_keys , readonly_keys , fallback
595
+ item_types , required_keys , readonly_keys , fallback
598
596
)
599
597
if info .special_alias and has_placeholder (info .special_alias .target ):
600
598
self .api .process_placeholder (
0 commit comments