Skip to content

Commit 56258a6

Browse files
authored
Fix minor issues with keywords Unpack (#13854)
This fixes couple issues discovered in #13790: * A crash on empty `Unpack` * Wrong behavior with implicit generic `Any` The latter was actually caused by somewhat reckless handling of generic `TypedDict`s, wrong argument count was handled inconsistently there.
1 parent 0c4b763 commit 56258a6

File tree

3 files changed

+82
-14
lines changed

3 files changed

+82
-14
lines changed

mypy/messages.py

+11
Original file line numberDiff line numberDiff line change
@@ -2704,6 +2704,17 @@ def for_function(callee: CallableType) -> str:
27042704
return ""
27052705

27062706

2707+
def wrong_type_arg_count(n: int, act: str, name: str) -> str:
2708+
s = f"{n} type arguments"
2709+
if n == 0:
2710+
s = "no type arguments"
2711+
elif n == 1:
2712+
s = "1 type argument"
2713+
if act == "0":
2714+
act = "none"
2715+
return f'"{name}" expects {s}, but {act} given'
2716+
2717+
27072718
def find_defining_module(modules: dict[str, MypyFile], typ: CallableType) -> MypyFile | None:
27082719
if not typ.definition:
27092720
return None

mypy/typeanal.py

+33-14
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from mypy import errorcodes as codes, message_registry, nodes
1212
from mypy.errorcodes import ErrorCode
1313
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
14-
from mypy.messages import MessageBuilder, format_type_bare, quote_type_string
14+
from mypy.messages import MessageBuilder, format_type_bare, quote_type_string, wrong_type_arg_count
1515
from mypy.nodes import (
1616
ARG_NAMED,
1717
ARG_NAMED_OPT,
@@ -571,6 +571,9 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
571571
elif fullname in ("typing.Unpack", "typing_extensions.Unpack"):
572572
if not self.api.incomplete_feature_enabled(UNPACK, t):
573573
return AnyType(TypeOfAny.from_error)
574+
if len(t.args) != 1:
575+
self.fail("Unpack[...] requires exactly one type argument", t)
576+
return AnyType(TypeOfAny.from_error)
574577
return UnpackType(self.anal_type(t.args[0]), line=t.line, column=t.column)
575578
return None
576579

@@ -644,14 +647,28 @@ def analyze_type_with_type_info(
644647
# The class has a Tuple[...] base class so it will be
645648
# represented as a tuple type.
646649
if info.special_alias:
647-
return TypeAliasType(info.special_alias, self.anal_array(args))
650+
return expand_type_alias(
651+
info.special_alias,
652+
self.anal_array(args),
653+
self.fail,
654+
False,
655+
ctx,
656+
use_standard_error=True,
657+
)
648658
return tup.copy_modified(items=self.anal_array(tup.items), fallback=instance)
649659
td = info.typeddict_type
650660
if td is not None:
651661
# The class has a TypedDict[...] base class so it will be
652662
# represented as a typeddict type.
653663
if info.special_alias:
654-
return TypeAliasType(info.special_alias, self.anal_array(args))
664+
return expand_type_alias(
665+
info.special_alias,
666+
self.anal_array(args),
667+
self.fail,
668+
False,
669+
ctx,
670+
use_standard_error=True,
671+
)
655672
# Create a named TypedDictType
656673
return td.copy_modified(
657674
item_types=self.anal_array(list(td.items.values())), fallback=instance
@@ -1535,16 +1552,11 @@ def fix_instance(
15351552
t.args = (any_type,) * len(t.type.type_vars)
15361553
return
15371554
# Invalid number of type parameters.
1538-
n = len(t.type.type_vars)
1539-
s = f"{n} type arguments"
1540-
if n == 0:
1541-
s = "no type arguments"
1542-
elif n == 1:
1543-
s = "1 type argument"
1544-
act = str(len(t.args))
1545-
if act == "0":
1546-
act = "none"
1547-
fail(f'"{t.type.name}" expects {s}, but {act} given', t, code=codes.TYPE_ARG)
1555+
fail(
1556+
wrong_type_arg_count(len(t.type.type_vars), str(len(t.args)), t.type.name),
1557+
t,
1558+
code=codes.TYPE_ARG,
1559+
)
15481560
# Construct the correct number of type arguments, as
15491561
# otherwise the type checker may crash as it expects
15501562
# things to be right.
@@ -1561,6 +1573,7 @@ def expand_type_alias(
15611573
*,
15621574
unexpanded_type: Type | None = None,
15631575
disallow_any: bool = False,
1576+
use_standard_error: bool = False,
15641577
) -> Type:
15651578
"""Expand a (generic) type alias target following the rules outlined in TypeAlias docstring.
15661579
@@ -1602,7 +1615,13 @@ def expand_type_alias(
16021615
tp.column = ctx.column
16031616
return tp
16041617
if act_len != exp_len:
1605-
fail(f"Bad number of arguments for type alias, expected: {exp_len}, given: {act_len}", ctx)
1618+
if use_standard_error:
1619+
# This is used if type alias is an internal representation of another type,
1620+
# for example a generic TypedDict or NamedTuple.
1621+
msg = wrong_type_arg_count(exp_len, str(act_len), node.name)
1622+
else:
1623+
msg = f"Bad number of arguments for type alias, expected: {exp_len}, given: {act_len}"
1624+
fail(msg, ctx, code=codes.TYPE_ARG)
16061625
return set_any_tvars(node, ctx.line, ctx.column, from_error=True)
16071626
typ = TypeAliasType(node, args, ctx.line, ctx.column)
16081627
assert typ.alias is not None

test-data/unit/check-varargs.test

+38
Original file line numberDiff line numberDiff line change
@@ -1043,3 +1043,41 @@ def g(**kwargs: Unpack[Person]) -> int: ...
10431043

10441044
reveal_type(g) # N: Revealed type is "def (*, name: builtins.str, age: builtins.int) -> builtins.list[builtins.int]"
10451045
[builtins fixtures/dict.pyi]
1046+
1047+
[case testUnpackGenericTypedDictImplicitAnyEnabled]
1048+
from typing import Generic, TypeVar
1049+
from typing_extensions import Unpack, TypedDict
1050+
1051+
T = TypeVar("T")
1052+
class TD(TypedDict, Generic[T]):
1053+
key: str
1054+
value: T
1055+
1056+
def foo(**kwds: Unpack[TD]) -> None: ... # Same as `TD[Any]`
1057+
foo(key="yes", value=42)
1058+
foo(key="yes", value="ok")
1059+
[builtins fixtures/dict.pyi]
1060+
1061+
[case testUnpackGenericTypedDictImplicitAnyDisabled]
1062+
# flags: --disallow-any-generics
1063+
from typing import Generic, TypeVar
1064+
from typing_extensions import Unpack, TypedDict
1065+
1066+
T = TypeVar("T")
1067+
class TD(TypedDict, Generic[T]):
1068+
key: str
1069+
value: T
1070+
1071+
def foo(**kwds: Unpack[TD]) -> None: ... # E: Missing type parameters for generic type "TD"
1072+
foo(key="yes", value=42)
1073+
foo(key="yes", value="ok")
1074+
[builtins fixtures/dict.pyi]
1075+
1076+
[case testUnpackNoCrashOnEmpty]
1077+
from typing_extensions import Unpack
1078+
1079+
class C:
1080+
def __init__(self, **kwds: Unpack) -> None: ... # E: Unpack[...] requires exactly one type argument
1081+
class D:
1082+
def __init__(self, **kwds: Unpack[int, str]) -> None: ... # E: Unpack[...] requires exactly one type argument
1083+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)