From 0d6773c4a1bd9b7f263752a4cc867e6c3f23e90d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 9 Oct 2022 22:52:59 +0100 Subject: [PATCH] Fix minor issues with keywords Unpack --- mypy/messages.py | 11 ++++++++ mypy/typeanal.py | 47 ++++++++++++++++++++++--------- test-data/unit/check-varargs.test | 38 +++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 14 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index f3aa1898bfd8..8c85a86b6d80 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2704,6 +2704,17 @@ def for_function(callee: CallableType) -> str: return "" +def wrong_type_arg_count(n: int, act: str, name: str) -> str: + s = f"{n} type arguments" + if n == 0: + s = "no type arguments" + elif n == 1: + s = "1 type argument" + if act == "0": + act = "none" + return f'"{name}" expects {s}, but {act} given' + + def find_defining_module(modules: dict[str, MypyFile], typ: CallableType) -> MypyFile | None: if not typ.definition: return None diff --git a/mypy/typeanal.py b/mypy/typeanal.py index add18deb34a2..adf58a3d7341 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -11,7 +11,7 @@ from mypy import errorcodes as codes, message_registry, nodes from mypy.errorcodes import ErrorCode from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type -from mypy.messages import MessageBuilder, format_type_bare, quote_type_string +from mypy.messages import MessageBuilder, format_type_bare, quote_type_string, wrong_type_arg_count from mypy.nodes import ( ARG_NAMED, ARG_NAMED_OPT, @@ -571,6 +571,9 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ elif fullname in ("typing.Unpack", "typing_extensions.Unpack"): if not self.api.incomplete_feature_enabled(UNPACK, t): return AnyType(TypeOfAny.from_error) + if len(t.args) != 1: + self.fail("Unpack[...] requires exactly one type argument", t) + return AnyType(TypeOfAny.from_error) return UnpackType(self.anal_type(t.args[0]), line=t.line, column=t.column) return None @@ -644,14 +647,28 @@ def analyze_type_with_type_info( # The class has a Tuple[...] base class so it will be # represented as a tuple type. if info.special_alias: - return TypeAliasType(info.special_alias, self.anal_array(args)) + return expand_type_alias( + info.special_alias, + self.anal_array(args), + self.fail, + False, + ctx, + use_standard_error=True, + ) return tup.copy_modified(items=self.anal_array(tup.items), fallback=instance) td = info.typeddict_type if td is not None: # The class has a TypedDict[...] base class so it will be # represented as a typeddict type. if info.special_alias: - return TypeAliasType(info.special_alias, self.anal_array(args)) + return expand_type_alias( + info.special_alias, + self.anal_array(args), + self.fail, + False, + ctx, + use_standard_error=True, + ) # Create a named TypedDictType return td.copy_modified( item_types=self.anal_array(list(td.items.values())), fallback=instance @@ -1535,16 +1552,11 @@ def fix_instance( t.args = (any_type,) * len(t.type.type_vars) return # Invalid number of type parameters. - n = len(t.type.type_vars) - s = f"{n} type arguments" - if n == 0: - s = "no type arguments" - elif n == 1: - s = "1 type argument" - act = str(len(t.args)) - if act == "0": - act = "none" - fail(f'"{t.type.name}" expects {s}, but {act} given', t, code=codes.TYPE_ARG) + fail( + wrong_type_arg_count(len(t.type.type_vars), str(len(t.args)), t.type.name), + t, + code=codes.TYPE_ARG, + ) # Construct the correct number of type arguments, as # otherwise the type checker may crash as it expects # things to be right. @@ -1561,6 +1573,7 @@ def expand_type_alias( *, unexpanded_type: Type | None = None, disallow_any: bool = False, + use_standard_error: bool = False, ) -> Type: """Expand a (generic) type alias target following the rules outlined in TypeAlias docstring. @@ -1602,7 +1615,13 @@ def expand_type_alias( tp.column = ctx.column return tp if act_len != exp_len: - fail(f"Bad number of arguments for type alias, expected: {exp_len}, given: {act_len}", ctx) + if use_standard_error: + # This is used if type alias is an internal representation of another type, + # for example a generic TypedDict or NamedTuple. + msg = wrong_type_arg_count(exp_len, str(act_len), node.name) + else: + msg = f"Bad number of arguments for type alias, expected: {exp_len}, given: {act_len}" + fail(msg, ctx, code=codes.TYPE_ARG) return set_any_tvars(node, ctx.line, ctx.column, from_error=True) typ = TypeAliasType(node, args, ctx.line, ctx.column) assert typ.alias is not None diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index d5c60bcf450e..00ac7df320d2 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -1043,3 +1043,41 @@ def g(**kwargs: Unpack[Person]) -> int: ... reveal_type(g) # N: Revealed type is "def (*, name: builtins.str, age: builtins.int) -> builtins.list[builtins.int]" [builtins fixtures/dict.pyi] + +[case testUnpackGenericTypedDictImplicitAnyEnabled] +from typing import Generic, TypeVar +from typing_extensions import Unpack, TypedDict + +T = TypeVar("T") +class TD(TypedDict, Generic[T]): + key: str + value: T + +def foo(**kwds: Unpack[TD]) -> None: ... # Same as `TD[Any]` +foo(key="yes", value=42) +foo(key="yes", value="ok") +[builtins fixtures/dict.pyi] + +[case testUnpackGenericTypedDictImplicitAnyDisabled] +# flags: --disallow-any-generics +from typing import Generic, TypeVar +from typing_extensions import Unpack, TypedDict + +T = TypeVar("T") +class TD(TypedDict, Generic[T]): + key: str + value: T + +def foo(**kwds: Unpack[TD]) -> None: ... # E: Missing type parameters for generic type "TD" +foo(key="yes", value=42) +foo(key="yes", value="ok") +[builtins fixtures/dict.pyi] + +[case testUnpackNoCrashOnEmpty] +from typing_extensions import Unpack + +class C: + def __init__(self, **kwds: Unpack) -> None: ... # E: Unpack[...] requires exactly one type argument +class D: + def __init__(self, **kwds: Unpack[int, str]) -> None: ... # E: Unpack[...] requires exactly one type argument +[builtins fixtures/dict.pyi]