From 713a2b11bc8917add7f5b1b9caddf7386275269d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 17 Sep 2024 14:11:20 +0100 Subject: [PATCH] [PEP 695] Support Annotated[...] in new-style type aliases The rvalue expression isn't semantically analyzed, so we can't rely on the `fullname` attribute to check if there is a reference to `Annotated`. Instead, use a lookup function provided by the caller to determine the fullname. Fixes #17751. --- mypy/exprtotype.py | 39 ++++++++++++++++++++--------- mypy/semanal.py | 4 ++- test-data/unit/check-python312.test | 22 ++++++++++++++++ 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 92316d11926d..c7df851668be 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -2,12 +2,15 @@ from __future__ import annotations +from typing import Callable + from mypy.fastparse import parse_type_string from mypy.nodes import ( MISSING_FALLBACK, BytesExpr, CallExpr, ComplexExpr, + Context, DictExpr, EllipsisExpr, Expression, @@ -21,6 +24,7 @@ RefExpr, StarExpr, StrExpr, + SymbolTableNode, TupleExpr, UnaryExpr, get_member_expr_fullname, @@ -63,12 +67,16 @@ def expr_to_unanalyzed_type( allow_new_syntax: bool = False, _parent: Expression | None = None, allow_unpack: bool = False, + lookup_qualified: Callable[[str, Context], SymbolTableNode | None] | None = None, ) -> ProperType: """Translate an expression to the corresponding type. The result is not semantically analyzed. It can be UnboundType or TypeList. Raise TypeTranslationError if the expression cannot represent a type. + If lookup_qualified is not provided, the expression is expected to be semantically + analyzed. + If allow_new_syntax is True, allow all type syntax independent of the target Python version (used in stubs). @@ -101,19 +109,26 @@ def expr_to_unanalyzed_type( else: args = [expr.index] - if isinstance(expr.base, RefExpr) and expr.base.fullname in ANNOTATED_TYPE_NAMES: - # TODO: this is not the optimal solution as we are basically getting rid - # of the Annotation definition and only returning the type information, - # losing all the annotations. + if isinstance(expr.base, RefExpr): + # Check if the type is Annotated[...]. For this we need the fullname, + # which must be looked up if the expression hasn't been semantically analyzed. + base_fullname = None + if lookup_qualified is not None: + sym = lookup_qualified(base.name, expr) + if sym and sym.node: + base_fullname = sym.node.fullname + else: + base_fullname = expr.base.fullname - return expr_to_unanalyzed_type(args[0], options, allow_new_syntax, expr) - else: - base.args = tuple( - expr_to_unanalyzed_type( - arg, options, allow_new_syntax, expr, allow_unpack=True - ) - for arg in args - ) + if base_fullname is not None and base_fullname in ANNOTATED_TYPE_NAMES: + # TODO: this is not the optimal solution as we are basically getting rid + # of the Annotation definition and only returning the type information, + # losing all the annotations. + return expr_to_unanalyzed_type(args[0], options, allow_new_syntax, expr) + base.args = tuple( + expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr, allow_unpack=True) + for arg in args + ) if not base.args: base.empty_tuple_index = True return base diff --git a/mypy/semanal.py b/mypy/semanal.py index 782985e3fbab..bd1d6e44573d 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3744,7 +3744,9 @@ def analyze_alias( dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic()) global_scope = not self.type and not self.function_stack try: - typ = expr_to_unanalyzed_type(rvalue, self.options, self.is_stub_file) + typ = expr_to_unanalyzed_type( + rvalue, self.options, self.is_stub_file, lookup_qualified=self.lookup_qualified + ) except TypeTranslationError: self.fail( "Invalid type alias: expression is not a valid type", rvalue, code=codes.VALID_TYPE diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index a3f4c87120cd..5c8a840cdde1 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1713,3 +1713,25 @@ type XNested = (1 + (yield 1)) # E: Yield expression cannot be used within a ty type YNested = (1 + (yield from [])) # E: Yield expression cannot be used within a type alias type ZNested = (1 + (a := 1)) # E: Named expression cannot be used within a type alias type KNested = (1 + (await 1)) # E: Await expression cannot be used within a type alias + +[case testPEP695TypeAliasAndAnnotated] +# flags: --enable-incomplete-feature=NewGenericSyntax +from typing_extensions import Annotated, Annotated as _Annotated +import typing_extensions as t + +def ann(*args): ... + +type A = Annotated[int, ann()] +type B = Annotated[int | str, ann((1, 2))] +type C = _Annotated[int, ann()] +type D = t.Annotated[str, ann()] + +x: A +y: B +z: C +zz: D +reveal_type(x) # N: Revealed type is "builtins.int" +reveal_type(y) # N: Revealed type is "Union[builtins.int, builtins.str]" +reveal_type(z) # N: Revealed type is "builtins.int" +reveal_type(zz) # N: Revealed type is "builtins.str" +[builtins fixtures/tuple.pyi]