Skip to content

Commit 29abf39

Browse files
authored
Support PEP 646 syntax for Callable (#15951)
Fixes #15412 Two new things here as specified by PEP 646: * Using star for an (explicit) type unpaking in callables, like `Callable[[str, *tuple[int, ...]], None]` * Allowing suffix items after a variadic item, like `Callable[[X, Unpack[Ys], Z], bool]` Implementation is straightforward. Btw while working in this I accidentally fixed a nasty bug, tuple types were often not given any line/column numbers, so if such type becomes a location of an error, it is impossible to ignore.
1 parent 7f65cc7 commit 29abf39

File tree

4 files changed

+123
-32
lines changed

4 files changed

+123
-32
lines changed

mypy/exprtotype.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
NameExpr,
1818
OpExpr,
1919
RefExpr,
20+
StarExpr,
2021
StrExpr,
2122
TupleExpr,
2223
UnaryExpr,
@@ -35,6 +36,7 @@
3536
TypeOfAny,
3637
UnboundType,
3738
UnionType,
39+
UnpackType,
3840
)
3941

4042

@@ -56,6 +58,7 @@ def expr_to_unanalyzed_type(
5658
options: Options | None = None,
5759
allow_new_syntax: bool = False,
5860
_parent: Expression | None = None,
61+
allow_unpack: bool = False,
5962
) -> ProperType:
6063
"""Translate an expression to the corresponding type.
6164
@@ -163,7 +166,10 @@ def expr_to_unanalyzed_type(
163166
return CallableArgument(typ, name, arg_const, expr.line, expr.column)
164167
elif isinstance(expr, ListExpr):
165168
return TypeList(
166-
[expr_to_unanalyzed_type(t, options, allow_new_syntax, expr) for t in expr.items],
169+
[
170+
expr_to_unanalyzed_type(t, options, allow_new_syntax, expr, allow_unpack=True)
171+
for t in expr.items
172+
],
167173
line=expr.line,
168174
column=expr.column,
169175
)
@@ -189,5 +195,7 @@ def expr_to_unanalyzed_type(
189195
return RawExpressionType(None, "builtins.complex", line=expr.line, column=expr.column)
190196
elif isinstance(expr, EllipsisExpr):
191197
return EllipsisType(expr.line)
198+
elif allow_unpack and isinstance(expr, StarExpr):
199+
return UnpackType(expr_to_unanalyzed_type(expr.expr, options, allow_new_syntax))
192200
else:
193201
raise TypeTranslationError()

mypy/fastparse.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
TypeOfAny,
116116
UnboundType,
117117
UnionType,
118+
UnpackType,
118119
)
119120
from mypy.util import bytes_to_human_readable_repr, unnamed_function
120121

@@ -1730,6 +1731,7 @@ def __init__(
17301731
self.override_column = override_column
17311732
self.node_stack: list[AST] = []
17321733
self.is_evaluated = is_evaluated
1734+
self.allow_unpack = False
17331735

17341736
def convert_column(self, column: int) -> int:
17351737
"""Apply column override if defined; otherwise return column.
@@ -2006,10 +2008,20 @@ def visit_Attribute(self, n: Attribute) -> Type:
20062008
else:
20072009
return self.invalid_type(n)
20082010

2011+
# Used for Callable[[X *Ys, Z], R]
2012+
def visit_Starred(self, n: ast3.Starred) -> Type:
2013+
return UnpackType(self.visit(n.value))
2014+
20092015
# List(expr* elts, expr_context ctx)
20102016
def visit_List(self, n: ast3.List) -> Type:
20112017
assert isinstance(n.ctx, ast3.Load)
2012-
return self.translate_argument_list(n.elts)
2018+
old_allow_unpack = self.allow_unpack
2019+
# We specifically only allow starred expressions in a list to avoid
2020+
# confusing errors for top-level unpacks (e.g. in base classes).
2021+
self.allow_unpack = True
2022+
result = self.translate_argument_list(n.elts)
2023+
self.allow_unpack = old_allow_unpack
2024+
return result
20132025

20142026

20152027
def stringify_name(n: AST) -> str | None:

mypy/typeanal.py

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,9 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
568568
instance = self.named_type("builtins.tuple", [self.anal_type(t.args[0])])
569569
instance.line = t.line
570570
return instance
571-
return self.tuple_type(self.anal_array(t.args, allow_unpack=True))
571+
return self.tuple_type(
572+
self.anal_array(t.args, allow_unpack=True), line=t.line, column=t.column
573+
)
572574
elif fullname == "typing.Union":
573575
items = self.anal_array(t.args)
574576
return UnionType.make_union(items)
@@ -968,7 +970,10 @@ def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
968970
return t
969971

970972
def visit_unpack_type(self, t: UnpackType) -> Type:
971-
raise NotImplementedError
973+
if not self.allow_unpack:
974+
self.fail(message_registry.INVALID_UNPACK_POSITION, t.type, code=codes.VALID_TYPE)
975+
return AnyType(TypeOfAny.from_error)
976+
return UnpackType(self.anal_type(t.type))
972977

973978
def visit_parameters(self, t: Parameters) -> Type:
974979
raise NotImplementedError("ParamSpec literals cannot have unbound TypeVars")
@@ -1364,12 +1369,22 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
13641369
assert isinstance(ret, CallableType)
13651370
return ret.accept(self)
13661371

1372+
def refers_to_full_names(self, arg: UnboundType, names: Sequence[str]) -> bool:
1373+
sym = self.lookup_qualified(arg.name, arg)
1374+
if sym is not None:
1375+
if sym.fullname in names:
1376+
return True
1377+
return False
1378+
13671379
def analyze_callable_args(
13681380
self, arglist: TypeList
13691381
) -> tuple[list[Type], list[ArgKind], list[str | None]] | None:
13701382
args: list[Type] = []
13711383
kinds: list[ArgKind] = []
13721384
names: list[str | None] = []
1385+
seen_unpack = False
1386+
unpack_types: list[Type] = []
1387+
invalid_unpacks = []
13731388
for arg in arglist.items:
13741389
if isinstance(arg, CallableArgument):
13751390
args.append(arg.typ)
@@ -1390,20 +1405,42 @@ def analyze_callable_args(
13901405
if arg.name is not None and kind.is_star():
13911406
self.fail(f"{arg.constructor} arguments should not have names", arg)
13921407
return None
1393-
elif isinstance(arg, UnboundType):
1394-
kind = ARG_POS
1395-
# Potentially a unpack.
1396-
sym = self.lookup_qualified(arg.name, arg)
1397-
if sym is not None:
1398-
if sym.fullname in ("typing_extensions.Unpack", "typing.Unpack"):
1399-
kind = ARG_STAR
1400-
args.append(arg)
1401-
kinds.append(kind)
1402-
names.append(None)
1408+
elif (
1409+
isinstance(arg, UnboundType)
1410+
and self.refers_to_full_names(arg, ("typing_extensions.Unpack", "typing.Unpack"))
1411+
or isinstance(arg, UnpackType)
1412+
):
1413+
if seen_unpack:
1414+
# Multiple unpacks, preserve them, so we can give an error later.
1415+
invalid_unpacks.append(arg)
1416+
continue
1417+
seen_unpack = True
1418+
unpack_types.append(arg)
1419+
else:
1420+
if seen_unpack:
1421+
unpack_types.append(arg)
1422+
else:
1423+
args.append(arg)
1424+
kinds.append(ARG_POS)
1425+
names.append(None)
1426+
if seen_unpack:
1427+
if len(unpack_types) == 1:
1428+
args.append(unpack_types[0])
14031429
else:
1404-
args.append(arg)
1405-
kinds.append(ARG_POS)
1406-
names.append(None)
1430+
first = unpack_types[0]
1431+
if isinstance(first, UnpackType):
1432+
# UnpackType doesn't have its own line/column numbers,
1433+
# so use the unpacked type for error messages.
1434+
first = first.type
1435+
args.append(
1436+
UnpackType(self.tuple_type(unpack_types, line=first.line, column=first.column))
1437+
)
1438+
kinds.append(ARG_STAR)
1439+
names.append(None)
1440+
for arg in invalid_unpacks:
1441+
args.append(arg)
1442+
kinds.append(ARG_STAR)
1443+
names.append(None)
14071444
# Note that arglist below is only used for error context.
14081445
check_arg_names(names, [arglist] * len(args), self.fail, "Callable")
14091446
check_arg_kinds(kinds, [arglist] * len(args), self.fail)
@@ -1713,9 +1750,11 @@ def check_unpacks_in_list(self, items: list[Type]) -> list[Type]:
17131750
self.fail("More than one Unpack in a type is not allowed", final_unpack)
17141751
return new_items
17151752

1716-
def tuple_type(self, items: list[Type]) -> TupleType:
1753+
def tuple_type(self, items: list[Type], line: int, column: int) -> TupleType:
17171754
any_type = AnyType(TypeOfAny.special_form)
1718-
return TupleType(items, fallback=self.named_type("builtins.tuple", [any_type]))
1755+
return TupleType(
1756+
items, fallback=self.named_type("builtins.tuple", [any_type]), line=line, column=column
1757+
)
17191758

17201759

17211760
TypeVarLikeList = List[Tuple[str, TypeVarLikeExpr]]

test-data/unit/check-typevar-tuple.test

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,51 @@ call_prefix(target=func_prefix, args=(0, 'foo'))
509509
call_prefix(target=func2_prefix, args=(0, 'foo')) # E: Argument "target" to "call_prefix" has incompatible type "Callable[[str, int, str], None]"; expected "Callable[[bytes, int, str], None]"
510510
[builtins fixtures/tuple.pyi]
511511

512+
[case testTypeVarTuplePep646CallableSuffixSyntax]
513+
from typing import Callable, Tuple, TypeVar
514+
from typing_extensions import Unpack, TypeVarTuple
515+
516+
x: Callable[[str, Unpack[Tuple[int, ...]], bool], None]
517+
reveal_type(x) # N: Revealed type is "def (builtins.str, *Unpack[Tuple[Unpack[builtins.tuple[builtins.int, ...]], builtins.bool]])"
518+
519+
T = TypeVar("T")
520+
S = TypeVar("S")
521+
Ts = TypeVarTuple("Ts")
522+
A = Callable[[T, Unpack[Ts], S], int]
523+
y: A[int, str, bool]
524+
reveal_type(y) # N: Revealed type is "def (builtins.int, builtins.str, builtins.bool) -> builtins.int"
525+
z: A[Unpack[Tuple[int, ...]]]
526+
reveal_type(z) # N: Revealed type is "def (builtins.int, *Unpack[Tuple[Unpack[builtins.tuple[builtins.int, ...]], builtins.int]]) -> builtins.int"
527+
[builtins fixtures/tuple.pyi]
528+
529+
[case testTypeVarTuplePep646CallableInvalidSyntax]
530+
from typing import Callable, Tuple, TypeVar
531+
from typing_extensions import Unpack, TypeVarTuple
532+
533+
Ts = TypeVarTuple("Ts")
534+
Us = TypeVarTuple("Us")
535+
a: Callable[[Unpack[Ts], Unpack[Us]], int] # E: Var args may not appear after named or var args \
536+
# E: More than one Unpack in a type is not allowed
537+
reveal_type(a) # N: Revealed type is "def [Ts, Us] (*Unpack[Ts`-1]) -> builtins.int"
538+
b: Callable[[Unpack], int] # E: Unpack[...] requires exactly one type argument
539+
reveal_type(b) # N: Revealed type is "def (*Any) -> builtins.int"
540+
[builtins fixtures/tuple.pyi]
541+
542+
[case testTypeVarTuplePep646CallableNewSyntax]
543+
from typing import Callable, Generic, Tuple
544+
from typing_extensions import ParamSpec
545+
546+
x: Callable[[str, *Tuple[int, ...]], None]
547+
reveal_type(x) # N: Revealed type is "def (builtins.str, *builtins.int)"
548+
y: Callable[[str, *Tuple[int, ...], bool], None]
549+
reveal_type(y) # N: Revealed type is "def (builtins.str, *Unpack[Tuple[Unpack[builtins.tuple[builtins.int, ...]], builtins.bool]])"
550+
551+
P = ParamSpec("P")
552+
class C(Generic[P]): ...
553+
bad: C[[int, *Tuple[int, ...], int]] # E: Unpack is only valid in a variadic position
554+
reveal_type(bad) # N: Revealed type is "__main__.C[[builtins.int, *Any]]"
555+
[builtins fixtures/tuple.pyi]
556+
512557
[case testTypeVarTuplePep646UnspecifiedParameters]
513558
from typing import Tuple, Generic, TypeVar
514559
from typing_extensions import Unpack, TypeVarTuple
@@ -635,19 +680,6 @@ x: A[str, str]
635680
reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.int, builtins.str, builtins.str]"
636681
[builtins fixtures/tuple.pyi]
637682

638-
[case testVariadicAliasWrongCallable]
639-
from typing import TypeVar, Callable
640-
from typing_extensions import Unpack, TypeVarTuple
641-
642-
T = TypeVar("T")
643-
S = TypeVar("S")
644-
Ts = TypeVarTuple("Ts")
645-
646-
A = Callable[[T, Unpack[Ts], S], int] # E: Required positional args may not appear after default, named or var args
647-
x: A[int, str, int, str]
648-
reveal_type(x) # N: Revealed type is "def (builtins.int, builtins.str, builtins.int, builtins.str) -> builtins.int"
649-
[builtins fixtures/tuple.pyi]
650-
651683
[case testVariadicAliasMultipleUnpacks]
652684
from typing import Tuple, Generic, Callable
653685
from typing_extensions import Unpack, TypeVarTuple

0 commit comments

Comments
 (0)