From 36f0c2fc5f3b7d43aa6a70fd41391aeedf4bed98 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 20 May 2024 11:37:23 +0100 Subject: [PATCH 1/4] Minimal support for recursive type aliases using new syntax --- mypy/semanal.py | 22 +++++++++++++++++++++- test-data/unit/check-python312.test | 13 +++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 7d6c75b274ee..25bc3b0dda12 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -5329,7 +5329,27 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: and isinstance(existing.node, (PlaceholderNode, TypeAlias)) and existing.node.line == s.line ): - existing.node = alias_node + updated = False + if isinstance(existing.node, TypeAlias): + if existing.node.target != res: + # Copy expansion to the existing alias, this matches how we update base classes + # for a TypeInfo _in place_ if there are nested placeholders. + existing.node.target = res + existing.node.alias_tvars = alias_tvars + # existing.node.no_args = no_args + updated = True + else: + # Otherwise just replace existing placeholder with type alias. + 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) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index cce22634df6d..3ac4bb1a6eea 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1161,3 +1161,16 @@ def decorator(x: str) -> Any: ... @decorator(T) # E: Argument 1 to "decorator" has incompatible type "int"; expected "str" class C[T]: pass + +[case testPEP695RecursiceTypeAlias] +# mypy: enable-incomplete-feature=NewGenericSyntax + +type A = str | list[A] +a: A +reveal_type(a) # N: Revealed type is "Union[builtins.str, builtins.list[...]]" + +class C[T]: pass + +type B[T] = C[T] | list[B[T]] +b: B[int] +reveal_type(b) # N: Revealed type is "Union[__main__.C[builtins.int], builtins.list[...]]" From b33cf3cc2ec1f91ac1c3d3e5063a00ed07480ded Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 20 May 2024 11:54:08 +0100 Subject: [PATCH 2/4] Detect invalid recursive type aliases --- mypy/nodes.py | 4 +++- mypy/semanal.py | 11 ++++++++--- test-data/unit/check-python312.test | 10 ++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 6657ab8cb65f..fd69b21ef6c4 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1640,19 +1640,21 @@ def accept(self, visitor: StatementVisitor[T]) -> T: class TypeAliasStmt(Statement): - __slots__ = ("name", "type_args", "value") + __slots__ = ("name", "type_args", "value", "invalid_recursive_alias") __match_args__ = ("name", "type_args", "value") name: NameExpr type_args: list[TypeParam] value: Expression # Will get translated into a type + invalid_recursive_alias: bool def __init__(self, name: NameExpr, type_args: list[TypeParam], value: Expression) -> None: super().__init__() self.name = name self.type_args = type_args self.value = value + self.invalid_recursive_alias = False def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_type_alias_stmt(self) diff --git a/mypy/semanal.py b/mypy/semanal.py index 25bc3b0dda12..18acaa2dce29 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3921,7 +3921,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: 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) + self.disable_invalid_recursive_aliases(s, current_node, s.rvalue) if self.is_class_scope(): assert self.type is not None if self.type.is_protocol: @@ -4017,7 +4017,7 @@ def analyze_type_alias_type_params( return declared_tvars, all_declared_tvar_names def disable_invalid_recursive_aliases( - self, s: AssignmentStmt, current_node: TypeAlias + self, s: AssignmentStmt | TypeAliasStmt, current_node: TypeAlias, ctx: Context ) -> None: """Prohibit and fix recursive type aliases that are invalid/unsupported.""" messages = [] @@ -4034,7 +4034,7 @@ def disable_invalid_recursive_aliases( current_node.target = AnyType(TypeOfAny.from_error) s.invalid_recursive_alias = True for msg in messages: - self.fail(msg, s.rvalue) + self.fail(msg, ctx) def analyze_lvalue( self, @@ -5264,6 +5264,8 @@ def visit_match_stmt(self, s: MatchStmt) -> None: self.visit_block(s.bodies[i]) def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: + if s.invalid_recursive_alias: + return self.statement = s type_params = self.push_type_args(s.type_args, s) if type_params is None: @@ -5353,6 +5355,9 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: 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) 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 3ac4bb1a6eea..4c18e16e155d 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1174,3 +1174,13 @@ class C[T]: pass type B[T] = C[T] | list[B[T]] b: B[int] reveal_type(b) # N: Revealed type is "Union[__main__.C[builtins.int], builtins.list[...]]" + +[case testPEP695BadRecursiveTypeAlias] +# mypy: enable-incomplete-feature=NewGenericSyntax + +type A = A # E: Cannot resolve name "A" (possible cyclic definition) +type B = B | int # E: Invalid recursive alias: a union item of itself +a: A +reveal_type(a) # N: Revealed type is "Any" +b: B +reveal_type(b) # N: Revealed type is "Any" From 3bbc6bcb93e99d9483e19a0dc0a98787b6acc0bd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 20 May 2024 12:06:23 +0100 Subject: [PATCH 3/4] Test forward references --- test-data/unit/check-python312.test | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 4c18e16e155d..e7ebad16e702 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1184,3 +1184,23 @@ a: A reveal_type(a) # N: Revealed type is "Any" b: B reveal_type(b) # N: Revealed type is "Any" + +[case testPEP695RecursiveTypeAliasForwardReference] +# mypy: enable-incomplete-feature=NewGenericSyntax + +def f(a: A) -> None: + if isinstance(a, str): + reveal_type(a) # N: Revealed type is "builtins.str" + else: + reveal_type(a) # N: Revealed type is "__main__.C[Union[builtins.str, __main__.C[...]]]" + +type A = str | C[A] + +class C[T]: pass + +f('x') +f(C[str]()) +f(C[C[str]]()) +f(1) # E: Argument 1 to "f" has incompatible type "int"; expected "A" +f(C[int]()) # E: Argument 1 to "f" has incompatible type "C[int]"; expected "A" +[builtins fixtures/isinstance.pyi] From e16bea2f613ca3bd71e1fdc6db1d75285775ac6e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 30 May 2024 15:21:41 +0100 Subject: [PATCH 4/4] Address feedback --- mypy/semanal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 18acaa2dce29..4d08940e4bd5 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -5338,7 +5338,6 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: # for a TypeInfo _in place_ if there are nested placeholders. existing.node.target = res existing.node.alias_tvars = alias_tvars - # existing.node.no_args = no_args updated = True else: # Otherwise just replace existing placeholder with type alias.