Skip to content

Commit

Permalink
[PEP 695] Support Annotated[...] in new-style type aliases (#17777)
Browse files Browse the repository at this point in the history
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.

Error reporting in the second argument to `Annotated` is still
inconsistent, but this seems lower priority. I'll create a follow-up
issue about (or update an existing issue if one exists).

Fixes #17751.
  • Loading branch information
JukkaL authored Sep 18, 2024
1 parent f68f76d commit a47f301
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 13 deletions.
39 changes: 27 additions & 12 deletions mypy/exprtotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +24,7 @@
RefExpr,
StarExpr,
StrExpr,
SymbolTableNode,
TupleExpr,
UnaryExpr,
get_member_expr_fullname,
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3749,7 +3749,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
Expand Down
22 changes: 22 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,28 @@ type YNested = (1 + (yield from [])) # E: Yield expression cannot be used within
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]

[case testPEP695NestedGenericClass1]
# flags: --enable-incomplete-feature=NewGenericSyntax
class C[T]:
Expand Down

0 comments on commit a47f301

Please sign in to comment.