Skip to content

Commit 5d40464

Browse files
authored
Support PEP-646 and PEP-692 in the same callable (#16294)
Fixes #16285 I was not sure if it is important to support this, but taking into account the current behavior is a crash, and that implementation is quite simple, I think we should do this. Using this opportunity I also improve related error messages a bit.
1 parent b41c8c1 commit 5d40464

File tree

5 files changed

+142
-32
lines changed

5 files changed

+142
-32
lines changed

mypy/semanal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,7 @@ def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType
950950
return typ
951951
last_type = get_proper_type(last_type.type)
952952
if not isinstance(last_type, TypedDictType):
953-
self.fail("Unpack item in ** argument must be a TypedDict", defn)
953+
self.fail("Unpack item in ** argument must be a TypedDict", last_type)
954954
new_arg_types = typ.arg_types[:-1] + [AnyType(TypeOfAny.from_error)]
955955
return typ.copy_modified(arg_types=new_arg_types)
956956
overlap = set(typ.arg_names) & set(last_type.items)

mypy/typeanal.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -987,33 +987,40 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type:
987987
self.anal_star_arg_type(t.arg_types[-2], ARG_STAR, nested=nested),
988988
self.anal_star_arg_type(t.arg_types[-1], ARG_STAR2, nested=nested),
989989
]
990+
# If nested is True, it means we are analyzing a Callable[...] type, rather
991+
# than a function definition type. We need to "unpack" ** TypedDict annotation
992+
# here (for function definitions it is done in semanal).
993+
if nested and isinstance(arg_types[-1], UnpackType):
994+
# TODO: it would be better to avoid this get_proper_type() call.
995+
unpacked = get_proper_type(arg_types[-1].type)
996+
if isinstance(unpacked, TypedDictType):
997+
arg_types[-1] = unpacked
998+
unpacked_kwargs = True
999+
arg_types = self.check_unpacks_in_list(arg_types)
9901000
else:
991-
arg_types = self.anal_array(t.arg_types, nested=nested, allow_unpack=True)
9921001
star_index = None
9931002
if ARG_STAR in arg_kinds:
9941003
star_index = arg_kinds.index(ARG_STAR)
9951004
star2_index = None
9961005
if ARG_STAR2 in arg_kinds:
9971006
star2_index = arg_kinds.index(ARG_STAR2)
998-
validated_args: list[Type] = []
999-
for i, at in enumerate(arg_types):
1000-
if isinstance(at, UnpackType) and i not in (star_index, star2_index):
1001-
self.fail(
1002-
message_registry.INVALID_UNPACK_POSITION, at, code=codes.VALID_TYPE
1003-
)
1004-
validated_args.append(AnyType(TypeOfAny.from_error))
1005-
else:
1006-
if nested and isinstance(at, UnpackType) and i == star_index:
1007-
# TODO: it would be better to avoid this get_proper_type() call.
1008-
p_at = get_proper_type(at.type)
1009-
if isinstance(p_at, TypedDictType) and not at.from_star_syntax:
1010-
# Automatically detect Unpack[Foo] in Callable as backwards
1011-
# compatible syntax for **Foo, if Foo is a TypedDict.
1012-
at = p_at
1013-
arg_kinds[i] = ARG_STAR2
1014-
unpacked_kwargs = True
1015-
validated_args.append(at)
1016-
arg_types = validated_args
1007+
arg_types = []
1008+
for i, ut in enumerate(t.arg_types):
1009+
at = self.anal_type(
1010+
ut, nested=nested, allow_unpack=i in (star_index, star2_index)
1011+
)
1012+
if nested and isinstance(at, UnpackType) and i == star_index:
1013+
# TODO: it would be better to avoid this get_proper_type() call.
1014+
p_at = get_proper_type(at.type)
1015+
if isinstance(p_at, TypedDictType) and not at.from_star_syntax:
1016+
# Automatically detect Unpack[Foo] in Callable as backwards
1017+
# compatible syntax for **Foo, if Foo is a TypedDict.
1018+
at = p_at
1019+
arg_kinds[i] = ARG_STAR2
1020+
unpacked_kwargs = True
1021+
arg_types.append(at)
1022+
if nested:
1023+
arg_types = self.check_unpacks_in_list(arg_types)
10171024
# If there were multiple (invalid) unpacks, the arg types list will become shorter,
10181025
# we need to trim the kinds/names as well to avoid crashes.
10191026
arg_kinds = t.arg_kinds[: len(arg_types)]
@@ -1387,8 +1394,9 @@ def analyze_callable_args(
13871394
names: list[str | None] = []
13881395
seen_unpack = False
13891396
unpack_types: list[Type] = []
1390-
invalid_unpacks = []
1391-
for arg in arglist.items:
1397+
invalid_unpacks: list[Type] = []
1398+
second_unpack_last = False
1399+
for i, arg in enumerate(arglist.items):
13921400
if isinstance(arg, CallableArgument):
13931401
args.append(arg.typ)
13941402
names.append(arg.name)
@@ -1415,6 +1423,11 @@ def analyze_callable_args(
14151423
):
14161424
if seen_unpack:
14171425
# Multiple unpacks, preserve them, so we can give an error later.
1426+
if i == len(arglist.items) - 1 and not invalid_unpacks:
1427+
# Special case: if there are just two unpacks, and the second one appears
1428+
# as last type argument, it can be still valid, if the second unpacked type
1429+
# is a TypedDict. This should be checked by the caller.
1430+
second_unpack_last = True
14181431
invalid_unpacks.append(arg)
14191432
continue
14201433
seen_unpack = True
@@ -1442,7 +1455,7 @@ def analyze_callable_args(
14421455
names.append(None)
14431456
for arg in invalid_unpacks:
14441457
args.append(arg)
1445-
kinds.append(ARG_STAR)
1458+
kinds.append(ARG_STAR2 if second_unpack_last else ARG_STAR)
14461459
names.append(None)
14471460
# Note that arglist below is only used for error context.
14481461
check_arg_names(names, [arglist] * len(args), self.fail, "Callable")

mypy/types.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3268,15 +3268,16 @@ def visit_callable_type(self, t: CallableType) -> str:
32683268
num_skip = 0
32693269

32703270
s = ""
3271-
bare_asterisk = False
3271+
asterisk = False
32723272
for i in range(len(t.arg_types) - num_skip):
32733273
if s != "":
32743274
s += ", "
3275-
if t.arg_kinds[i].is_named() and not bare_asterisk:
3275+
if t.arg_kinds[i].is_named() and not asterisk:
32763276
s += "*, "
3277-
bare_asterisk = True
3277+
asterisk = True
32783278
if t.arg_kinds[i] == ARG_STAR:
32793279
s += "*"
3280+
asterisk = True
32803281
if t.arg_kinds[i] == ARG_STAR2:
32813282
s += "**"
32823283
name = t.arg_names[i]

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

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,7 @@ from typing_extensions import Unpack, TypeVarTuple
571571

572572
Ts = TypeVarTuple("Ts")
573573
Us = TypeVarTuple("Us")
574-
a: Callable[[Unpack[Ts], Unpack[Us]], int] # E: Var args may not appear after named or var args \
575-
# E: More than one Unpack in a type is not allowed
574+
a: Callable[[Unpack[Ts], Unpack[Us]], int] # E: More than one Unpack in a type is not allowed
576575
reveal_type(a) # N: Revealed type is "def [Ts, Us] (*Unpack[Ts`-1]) -> builtins.int"
577576
b: Callable[[Unpack], int] # E: Unpack[...] requires exactly one type argument
578577
reveal_type(b) # N: Revealed type is "def (*Any) -> builtins.int"
@@ -730,8 +729,7 @@ A = Tuple[Unpack[Ts], Unpack[Us]] # E: More than one Unpack in a type is not al
730729
x: A[int, str]
731730
reveal_type(x) # N: Revealed type is "Tuple[builtins.int, builtins.str]"
732731

733-
B = Callable[[Unpack[Ts], Unpack[Us]], int] # E: Var args may not appear after named or var args \
734-
# E: More than one Unpack in a type is not allowed
732+
B = Callable[[Unpack[Ts], Unpack[Us]], int] # E: More than one Unpack in a type is not allowed
735733
y: B[int, str]
736734
reveal_type(y) # N: Revealed type is "def (builtins.int, builtins.str) -> builtins.int"
737735

@@ -1912,3 +1910,101 @@ reveal_type(y) # N: Revealed type is "__main__.C[builtins.int, Unpack[builtins.
19121910
z = C[int]() # E: Bad number of arguments, expected: at least 2, given: 1
19131911
reveal_type(z) # N: Revealed type is "__main__.C[Any, Unpack[builtins.tuple[Any, ...]], Any]"
19141912
[builtins fixtures/tuple.pyi]
1913+
1914+
[case testTypeVarTupleBothUnpacksSimple]
1915+
from typing import Tuple
1916+
from typing_extensions import Unpack, TypeVarTuple, TypedDict
1917+
1918+
class Keywords(TypedDict):
1919+
a: str
1920+
b: str
1921+
1922+
Ints = Tuple[int, ...]
1923+
1924+
def f(*args: Unpack[Ints], other: str = "no", **kwargs: Unpack[Keywords]) -> None: ...
1925+
reveal_type(f) # N: Revealed type is "def (*args: builtins.int, other: builtins.str =, **kwargs: Unpack[TypedDict('__main__.Keywords', {'a': builtins.str, 'b': builtins.str})])"
1926+
f(1, 2, a="a", b="b") # OK
1927+
f(1, 2, 3) # E: Missing named argument "a" for "f" \
1928+
# E: Missing named argument "b" for "f"
1929+
1930+
Ts = TypeVarTuple("Ts")
1931+
def g(*args: Unpack[Ts], other: str = "no", **kwargs: Unpack[Keywords]) -> None: ...
1932+
reveal_type(g) # N: Revealed type is "def [Ts] (*args: Unpack[Ts`-1], other: builtins.str =, **kwargs: Unpack[TypedDict('__main__.Keywords', {'a': builtins.str, 'b': builtins.str})])"
1933+
g(1, 2, a="a", b="b") # OK
1934+
g(1, 2, 3) # E: Missing named argument "a" for "g" \
1935+
# E: Missing named argument "b" for "g"
1936+
1937+
def bad(
1938+
*args: Unpack[Keywords], # E: "Keywords" cannot be unpacked (must be tuple or TypeVarTuple)
1939+
**kwargs: Unpack[Ints], # E: Unpack item in ** argument must be a TypedDict
1940+
) -> None: ...
1941+
reveal_type(bad) # N: Revealed type is "def (*args: Any, **kwargs: Any)"
1942+
1943+
def bad2(
1944+
one: int,
1945+
*args: Unpack[Keywords], # E: "Keywords" cannot be unpacked (must be tuple or TypeVarTuple)
1946+
other: str = "no",
1947+
**kwargs: Unpack[Ints], # E: Unpack item in ** argument must be a TypedDict
1948+
) -> None: ...
1949+
reveal_type(bad2) # N: Revealed type is "def (one: builtins.int, *args: Any, other: builtins.str =, **kwargs: Any)"
1950+
[builtins fixtures/tuple.pyi]
1951+
1952+
[case testTypeVarTupleBothUnpacksCallable]
1953+
from typing import Callable, Tuple
1954+
from typing_extensions import Unpack, TypedDict
1955+
1956+
class Keywords(TypedDict):
1957+
a: str
1958+
b: str
1959+
Ints = Tuple[int, ...]
1960+
1961+
cb: Callable[[Unpack[Ints], Unpack[Keywords]], None]
1962+
reveal_type(cb) # N: Revealed type is "def (*builtins.int, **Unpack[TypedDict('__main__.Keywords', {'a': builtins.str, 'b': builtins.str})])"
1963+
1964+
cb2: Callable[[int, Unpack[Ints], int, Unpack[Keywords]], None]
1965+
reveal_type(cb2) # N: Revealed type is "def (builtins.int, *Unpack[Tuple[Unpack[builtins.tuple[builtins.int, ...]], builtins.int]], **Unpack[TypedDict('__main__.Keywords', {'a': builtins.str, 'b': builtins.str})])"
1966+
cb2(1, 2, 3, a="a", b="b")
1967+
cb2(1, a="a", b="b") # E: Too few arguments
1968+
cb2(1, 2, 3, a="a") # E: Missing named argument "b"
1969+
1970+
bad1: Callable[[Unpack[Ints], Unpack[Ints]], None] # E: More than one Unpack in a type is not allowed
1971+
reveal_type(bad1) # N: Revealed type is "def (*builtins.int)"
1972+
bad2: Callable[[Unpack[Keywords], Unpack[Keywords]], None] # E: "Keywords" cannot be unpacked (must be tuple or TypeVarTuple)
1973+
reveal_type(bad2) # N: Revealed type is "def (*Any, **Unpack[TypedDict('__main__.Keywords', {'a': builtins.str, 'b': builtins.str})])"
1974+
bad3: Callable[[Unpack[Keywords], Unpack[Ints]], None] # E: "Keywords" cannot be unpacked (must be tuple or TypeVarTuple) \
1975+
# E: More than one Unpack in a type is not allowed
1976+
reveal_type(bad3) # N: Revealed type is "def (*Any)"
1977+
[builtins fixtures/tuple.pyi]
1978+
1979+
[case testTypeVarTupleBothUnpacksApplication]
1980+
from typing import Callable, TypeVar, Optional
1981+
from typing_extensions import Unpack, TypeVarTuple, TypedDict
1982+
1983+
class Keywords(TypedDict):
1984+
a: str
1985+
b: str
1986+
1987+
T = TypeVar("T")
1988+
Ts = TypeVarTuple("Ts")
1989+
def test(
1990+
x: int,
1991+
func: Callable[[Unpack[Ts]], T],
1992+
*args: Unpack[Ts],
1993+
other: Optional[str] = None,
1994+
**kwargs: Unpack[Keywords],
1995+
) -> T:
1996+
if bool():
1997+
func(*args, **kwargs) # E: Extra argument "a" from **args
1998+
return func(*args)
1999+
def test2(
2000+
x: int,
2001+
func: Callable[[Unpack[Ts], Unpack[Keywords]], T],
2002+
*args: Unpack[Ts],
2003+
other: Optional[str] = None,
2004+
**kwargs: Unpack[Keywords],
2005+
) -> T:
2006+
if bool():
2007+
func(*args) # E: Missing named argument "a" \
2008+
# E: Missing named argument "b"
2009+
return func(*args, **kwargs)
2010+
[builtins fixtures/tuple.pyi]

test-data/unit/semanal-types.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ MypyFile:1(
10431043
default(
10441044
Var(y)
10451045
StrExpr()))
1046-
def (*x: builtins.int, *, y: builtins.str =) -> Any
1046+
def (*x: builtins.int, y: builtins.str =) -> Any
10471047
VarArg(
10481048
Var(x))
10491049
Block:1(

0 commit comments

Comments
 (0)