diff --git a/mypy/semanal.py b/mypy/semanal.py index cab56021c55e..196a18550088 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4334,9 +4334,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: elif isinstance(s.rvalue, RefExpr): s.rvalue.is_alias_rvalue = True + updated = False if existing: - # An alias gets updated. - updated = False if isinstance(existing.node, TypeAlias): # Invalidate recursive status cache in case it was previously set. existing.node._is_recursive = None @@ -4352,15 +4351,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # Otherwise just replace existing placeholder with type alias *in place*. existing._node = alias_node updated = True - # TODO: switch type aliases to if has_placeholder(): process_placeholder() pattern. - # Type aliases are last notable exception from this logic. - if updated: - if self.final_iteration: - self.cannot_resolve_name(lvalue.name, "name", s) - return True - else: - # We need to defer so that this change can get propagated to base classes. - self.defer(s, force_progress=True) else: self.add_symbol(lvalue.name, alias_node, s) if isinstance(rvalue, RefExpr) and isinstance(rvalue.node, TypeAlias): @@ -4368,6 +4358,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: current_node = existing.node if existing else alias_node assert isinstance(current_node, TypeAlias) self.disable_invalid_recursive_aliases(s, current_node, s.rvalue) + # Check for placeholders only after we disable invalid recursive aliases. + # Otherwise, we may get an infinite recursion while visiting the target. + if updated or has_placeholder(res): + self.process_placeholder(lvalue.name, "name", s, force_progress=updated) if self.is_class_scope(): assert self.type is not None if self.type.is_protocol: @@ -5899,12 +5893,12 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: alias_node.default_depends = default_depends s.alias_node = alias_node + updated = False if ( existing and isinstance(existing.node, (PlaceholderNode, TypeAlias)) and existing.node.line == s.line ): - updated = False if isinstance(existing.node, TypeAlias): # Invalidate recursive status cache in case it was previously set. existing.node._is_recursive = None @@ -5922,20 +5916,14 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: # Otherwise just replace existing placeholder with type alias *in place*. existing._node = alias_node updated = True - - if updated: - if self.final_iteration: - self.cannot_resolve_name(s.name.name, "name", s) - return - else: - # We need to defer so that this change can get propagated to base classes. - self.defer(s, force_progress=True) else: self.add_symbol(s.name.name, alias_node, s) current_node = existing.node if existing else alias_node assert isinstance(current_node, TypeAlias) self.disable_invalid_recursive_aliases(s, current_node, s.value) + if updated or has_placeholder(res): + self.process_placeholder(s.name.name, "name", s, force_progress=updated) s.name.accept(self) finally: self.pop_type_args(s.type_args) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index dbee1e06c69d..89f242a00333 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -2332,3 +2332,44 @@ class N(NamedTuple): n = N((1, 2)) reveal_type(n) # N: Revealed type is "tuple[tuple[builtins.int, builtins.int], fallback=__main__.N]" [builtins fixtures/tuple.pyi] + +[case testAliasWithTypeVarDefaultPlaceholderInImportCycleNewSyntax] +import a +from a import Base +reveal_type(Base().inst()) # N: Revealed type is "c.Inst" +reveal_type(Base().group()) # N: Revealed type is "TypedDict('d.Group', {'params': builtins.tuple[a.Base[c.Inst] | (def ()), ...]})" +[file a.py] +from typing import Generic, TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from c import Inst + from d import Group + +T = TypeVar("T", default="Inst") + +class Base(Generic[T]): + def group(self) -> "Group": ... + def inst(self) -> T: ... + +[file b.py] +from typing import Callable, Union +from a import Base + +type Alias = Union[Base, Callable[[], None]] + +[file c.py] +from a import Base + +class Inst: + def make(self) -> Base: ... + +[file d.py] +from typing import TYPE_CHECKING, Tuple +from typing_extensions import TypedDict + +if TYPE_CHECKING: + from b import Alias + +class Group(TypedDict): + params: "Tuple[Alias, ...]" +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 9eff3d98fbe0..5d81e76ba8c6 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -1040,3 +1040,44 @@ def f(obj: OneClass) -> None: else: reveal_type(obj) # N: Revealed type is "builtins.list[...]" [builtins fixtures/isinstancelist.pyi] + +[case testAliasWithTypeVarDefaultPlaceholderInImportCycle] +import a +from a import Base +reveal_type(Base().inst()) # N: Revealed type is "c.Inst" +reveal_type(Base().group()) # N: Revealed type is "TypedDict('d.Group', {'params': builtins.tuple[a.Base[c.Inst] | (def ()), ...]})" +[file a.py] +from typing import Generic, TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from c import Inst + from d import Group + +T = TypeVar("T", default="Inst") + +class Base(Generic[T]): + def group(self) -> "Group": ... + def inst(self) -> T: ... + +[file b.py] +from typing import Callable, Union +from a import Base + +Alias = Union[Base, Callable[[], None]] + +[file c.py] +from a import Base + +class Inst: + def make(self) -> Base: ... + +[file d.py] +from typing import TYPE_CHECKING, Tuple +from typing_extensions import TypedDict + +if TYPE_CHECKING: + from b import Alias + +class Group(TypedDict): + params: "Tuple[Alias, ...]" +[builtins fixtures/tuple.pyi]