Skip to content

Commit 272c8fd

Browse files
authored
Fix recursive TypedDicts/NamedTuples defined with call syntax (#14488)
Fixes #14460 Recursive TypedDicts/NamedTuples defined with call syntax that have item types that look like type applications suffer the same chicken-and-egg problem that recursive type aliases. Fortunately, there is a very simple way to distinguish them without fully analyzing rvalues, this is what this PR does.
1 parent 8f4da0e commit 272c8fd

File tree

2 files changed

+27
-7
lines changed

2 files changed

+27
-7
lines changed

mypy/semanal.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2648,7 +2648,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
26482648
# But we can't use a full visit because it may emit extra incomplete refs (namely
26492649
# when analysing any type applications there) thus preventing the further analysis.
26502650
# To break the tie, we first analyse rvalue partially, if it can be a type alias.
2651-
if self.can_possibly_be_index_alias(s):
2651+
if self.can_possibly_be_type_form(s):
26522652
old_basic_type_applications = self.basic_type_applications
26532653
self.basic_type_applications = True
26542654
with self.allow_unbound_tvars_set():
@@ -2664,7 +2664,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
26642664
for expr in names_modified_by_assignment(s):
26652665
self.mark_incomplete(expr.name, expr)
26662666
return
2667-
if self.can_possibly_be_index_alias(s):
2667+
if self.can_possibly_be_type_form(s):
26682668
# Now re-visit those rvalues that were we skipped type applications above.
26692669
# This should be safe as generally semantic analyzer is idempotent.
26702670
with self.allow_unbound_tvars_set():
@@ -2807,16 +2807,19 @@ def can_be_type_alias(self, rv: Expression, allow_none: bool = False) -> bool:
28072807
return True
28082808
return False
28092809

2810-
def can_possibly_be_index_alias(self, s: AssignmentStmt) -> bool:
2811-
"""Like can_be_type_alias(), but simpler and doesn't require analyzed rvalue.
2810+
def can_possibly_be_type_form(self, s: AssignmentStmt) -> bool:
2811+
"""Like can_be_type_alias(), but simpler and doesn't require fully analyzed rvalue.
28122812
2813-
Instead, use lvalues/annotations structure to figure out whether this can
2814-
potentially be a type alias definition. Another difference from above function
2815-
is that we are only interested IndexExpr and OpExpr rvalues, since only those
2813+
Instead, use lvalues/annotations structure to figure out whether this can potentially be
2814+
a type alias definition, NamedTuple, or TypedDict. Another difference from above function
2815+
is that we are only interested IndexExpr, CallExpr and OpExpr rvalues, since only those
28162816
can be potentially recursive (things like `A = A` are never valid).
28172817
"""
28182818
if len(s.lvalues) > 1:
28192819
return False
2820+
if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.callee, RefExpr):
2821+
ref = s.rvalue.callee.fullname
2822+
return ref in TPDICT_NAMES or ref in TYPED_NAMEDTUPLE_NAMES
28202823
if not isinstance(s.lvalues[0], NameExpr):
28212824
return False
28222825
if s.unanalyzed_type is not None and not self.is_pep_613(s):

test-data/unit/check-recursive-types.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,3 +880,20 @@ class InListRecurse(Generic[T], List[InList[T]]): ...
880880
def list_thing(transforming: InList[T]) -> T:
881881
...
882882
reveal_type(list_thing([5])) # N: Revealed type is "builtins.list[builtins.int]"
883+
884+
[case testRecursiveTypedDictWithList]
885+
from typing import List
886+
from typing_extensions import TypedDict
887+
888+
Example = TypedDict("Example", {"rec": List["Example"]})
889+
e: Example
890+
reveal_type(e) # N: Revealed type is "TypedDict('__main__.Example', {'rec': builtins.list[...]})"
891+
[builtins fixtures/dict.pyi]
892+
893+
[case testRecursiveNamedTupleWithList]
894+
from typing import List, NamedTuple
895+
896+
Example = NamedTuple("Example", [("rec", List["Example"])])
897+
e: Example
898+
reveal_type(e) # N: Revealed type is "Tuple[builtins.list[...], fallback=__main__.Example]"
899+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)