Skip to content

Commit 7569d88

Browse files
authored
Fix crash with malformed TypedDicts and disllow-any-expr (#13963)
Fixes #13066 During the semanal phase, mypy opts to ignore and skip processing any malformed or illegal statements inside of a TypedDict class definition, such as method definitions. Skipping semanal analysis on these statements can cause any number of odd downstream problems: the type-checking phase assumes that all semanal-only semantic constructs (e.g. FakeInfo) have been purged by this point, and so can crash at any point once this precondition has been violated. This diff opts to solve this problem by filtering down the list of statements so we keep only the ones we know are legal within a TypedDict definition. The other possible solution to this problem is to modify mypy so we skip checking TypedDict class bodies entirely during type checking and fine-grained deps analysis. Doing this would also let address #10007 and supersede my other diff #13732. I decided against doing this for now because: 1. I wasn't sure if this was actually safe, especially in the fine-grained deps phase and for mypyc. 2. I think no matter what, the semanal phase should not leak semanal-only types: relaxing this postcondition would make it harder to reason about mypy. So, we'd probably want to make this change regardless of what we do in the later phases.
1 parent 758f43c commit 7569d88

File tree

2 files changed

+36
-9
lines changed

2 files changed

+36
-9
lines changed

mypy/semanal_typeddict.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
NameExpr,
2424
PassStmt,
2525
RefExpr,
26+
Statement,
2627
StrExpr,
2728
TempNode,
2829
TupleExpr,
@@ -93,7 +94,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
9394
and defn.base_type_exprs[0].fullname in TPDICT_NAMES
9495
):
9596
# Building a new TypedDict
96-
fields, types, required_keys = self.analyze_typeddict_classdef_fields(defn)
97+
fields, types, statements, required_keys = self.analyze_typeddict_classdef_fields(defn)
9798
if fields is None:
9899
return True, None # Defer
99100
info = self.build_typeddict_typeinfo(
@@ -102,6 +103,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
102103
defn.analyzed = TypedDictExpr(info)
103104
defn.analyzed.line = defn.line
104105
defn.analyzed.column = defn.column
106+
defn.defs.body = statements
105107
return True, info
106108

107109
# Extending/merging existing TypedDicts
@@ -139,7 +141,12 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
139141
# Iterate over bases in reverse order so that leftmost base class' keys take precedence
140142
for base in reversed(typeddict_bases):
141143
self.add_keys_and_types_from_base(base, keys, types, required_keys, defn)
142-
new_keys, new_types, new_required_keys = self.analyze_typeddict_classdef_fields(defn, keys)
144+
(
145+
new_keys,
146+
new_types,
147+
new_statements,
148+
new_required_keys,
149+
) = self.analyze_typeddict_classdef_fields(defn, keys)
143150
if new_keys is None:
144151
return True, None # Defer
145152
keys.extend(new_keys)
@@ -151,6 +158,7 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
151158
defn.analyzed = TypedDictExpr(info)
152159
defn.analyzed.line = defn.line
153160
defn.analyzed.column = defn.column
161+
defn.defs.body = new_statements
154162
return True, info
155163

156164
def add_keys_and_types_from_base(
@@ -250,7 +258,7 @@ def map_items_to_base(
250258

251259
def analyze_typeddict_classdef_fields(
252260
self, defn: ClassDef, oldfields: list[str] | None = None
253-
) -> tuple[list[str] | None, list[Type], set[str]]:
261+
) -> tuple[list[str] | None, list[Type], list[Statement], set[str]]:
254262
"""Analyze fields defined in a TypedDict class definition.
255263
256264
This doesn't consider inherited fields (if any). Also consider totality,
@@ -259,17 +267,22 @@ def analyze_typeddict_classdef_fields(
259267
Return tuple with these items:
260268
* List of keys (or None if found an incomplete reference --> deferral)
261269
* List of types for each key
270+
* List of statements from defn.defs.body that are legally allowed to be a
271+
part of a TypedDict definition
262272
* Set of required keys
263273
"""
264274
fields: list[str] = []
265275
types: list[Type] = []
276+
statements: list[Statement] = []
266277
for stmt in defn.defs.body:
267278
if not isinstance(stmt, AssignmentStmt):
268-
# Still allow pass or ... (for empty TypedDict's).
269-
if not isinstance(stmt, PassStmt) and not (
279+
# Still allow pass or ... (for empty TypedDict's) and docstrings
280+
if isinstance(stmt, PassStmt) or (
270281
isinstance(stmt, ExpressionStmt)
271282
and isinstance(stmt.expr, (EllipsisExpr, StrExpr))
272283
):
284+
statements.append(stmt)
285+
else:
273286
self.fail(TPDICT_CLASS_ERROR, stmt)
274287
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
275288
# An assignment, but an invalid one.
@@ -281,8 +294,9 @@ def analyze_typeddict_classdef_fields(
281294
if name in fields:
282295
self.fail(f'Duplicate TypedDict key "{name}"', stmt)
283296
continue
284-
# Append name and type in this case...
297+
# Append stmt, name, and type in this case...
285298
fields.append(name)
299+
statements.append(stmt)
286300
if stmt.type is None:
287301
types.append(AnyType(TypeOfAny.unannotated))
288302
else:
@@ -293,9 +307,9 @@ def analyze_typeddict_classdef_fields(
293307
and not self.api.is_func_scope(),
294308
)
295309
if analyzed is None:
296-
return None, [], set() # Need to defer
310+
return None, [], [], set() # Need to defer
297311
types.append(analyzed)
298-
# ...despite possible minor failures that allow further analyzis.
312+
# ...despite possible minor failures that allow further analysis.
299313
if stmt.type is None or hasattr(stmt, "new_syntax") and not stmt.new_syntax:
300314
self.fail(TPDICT_CLASS_ERROR, stmt)
301315
elif not isinstance(stmt.rvalue, TempNode):
@@ -317,7 +331,7 @@ def analyze_typeddict_classdef_fields(
317331
t.item if isinstance(t, RequiredType) else t for t in types
318332
]
319333

320-
return fields, types, required_keys
334+
return fields, types, statements, required_keys
321335

322336
def check_typeddict(
323337
self, node: Expression, var_name: str | None, is_func_scope: bool

test-data/unit/check-typeddict.test

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,19 @@ reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'y': builtins.in
221221
[builtins fixtures/dict.pyi]
222222
[typing fixtures/typing-typeddict.pyi]
223223

224+
[case testCannotCreateTypedDictWithDecoratedFunction]
225+
# flags: --disallow-any-expr
226+
# https://github.com/python/mypy/issues/13066
227+
from typing import TypedDict
228+
class D(TypedDict):
229+
@classmethod # E: Invalid statement in TypedDict definition; expected "field_name: field_type"
230+
def m(self) -> D:
231+
pass
232+
d = D()
233+
reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {})"
234+
[builtins fixtures/dict.pyi]
235+
[typing fixtures/typing-typeddict.pyi]
236+
224237
[case testTypedDictWithClassmethodAlternativeConstructorDoesNotCrash]
225238
# https://github.com/python/mypy/issues/5653
226239
from typing import TypedDict

0 commit comments

Comments
 (0)