Skip to content
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
28 changes: 8 additions & 20 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -4352,22 +4351,17 @@ 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):
alias_node.normalized = rvalue.node.normalized
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:
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
41 changes: 41 additions & 0 deletions test-data/unit/check-recursive-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading