Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix recursive TypedDicts/NamedTuples defined with call syntax #14488

Merged
merged 1 commit into from
Jan 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2648,7 +2648,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
# But we can't use a full visit because it may emit extra incomplete refs (namely
# when analysing any type applications there) thus preventing the further analysis.
# To break the tie, we first analyse rvalue partially, if it can be a type alias.
if self.can_possibly_be_index_alias(s):
if self.can_possibly_be_type_form(s):
old_basic_type_applications = self.basic_type_applications
self.basic_type_applications = True
with self.allow_unbound_tvars_set():
Expand All @@ -2664,7 +2664,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
for expr in names_modified_by_assignment(s):
self.mark_incomplete(expr.name, expr)
return
if self.can_possibly_be_index_alias(s):
if self.can_possibly_be_type_form(s):
# Now re-visit those rvalues that were we skipped type applications above.
# This should be safe as generally semantic analyzer is idempotent.
with self.allow_unbound_tvars_set():
Expand Down Expand Up @@ -2807,16 +2807,19 @@ def can_be_type_alias(self, rv: Expression, allow_none: bool = False) -> bool:
return True
return False

def can_possibly_be_index_alias(self, s: AssignmentStmt) -> bool:
"""Like can_be_type_alias(), but simpler and doesn't require analyzed rvalue.
def can_possibly_be_type_form(self, s: AssignmentStmt) -> bool:
"""Like can_be_type_alias(), but simpler and doesn't require fully analyzed rvalue.

Instead, use lvalues/annotations structure to figure out whether this can
potentially be a type alias definition. Another difference from above function
is that we are only interested IndexExpr and OpExpr rvalues, since only those
Instead, use lvalues/annotations structure to figure out whether this can potentially be
a type alias definition, NamedTuple, or TypedDict. Another difference from above function
is that we are only interested IndexExpr, CallExpr and OpExpr rvalues, since only those
can be potentially recursive (things like `A = A` are never valid).
"""
if len(s.lvalues) > 1:
return False
if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.callee, RefExpr):
ref = s.rvalue.callee.fullname
return ref in TPDICT_NAMES or ref in TYPED_NAMEDTUPLE_NAMES
if not isinstance(s.lvalues[0], NameExpr):
return False
if s.unanalyzed_type is not None and not self.is_pep_613(s):
Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-recursive-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -880,3 +880,20 @@ class InListRecurse(Generic[T], List[InList[T]]): ...
def list_thing(transforming: InList[T]) -> T:
...
reveal_type(list_thing([5])) # N: Revealed type is "builtins.list[builtins.int]"

[case testRecursiveTypedDictWithList]
from typing import List
from typing_extensions import TypedDict

Example = TypedDict("Example", {"rec": List["Example"]})
e: Example
reveal_type(e) # N: Revealed type is "TypedDict('__main__.Example', {'rec': builtins.list[...]})"
[builtins fixtures/dict.pyi]

[case testRecursiveNamedTupleWithList]
from typing import List, NamedTuple

Example = NamedTuple("Example", [("rec", List["Example"])])
e: Example
reveal_type(e) # N: Revealed type is "Tuple[builtins.list[...], fallback=__main__.Example]"
[builtins fixtures/tuple.pyi]