diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4480bad7871d..845997ca6206 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6133,8 +6133,17 @@ def is_valid_var_arg(self, typ: Type) -> bool: def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" + typ = get_proper_type(typ) return ( - is_subtype( + ( + # This is a little ad hoc, ideally we would have a map_instance_to_supertype + # that worked for protocols + isinstance(typ, Instance) + and typ.type.fullname == "builtins.dict" + and is_subtype(typ.args[0], self.named_type("builtins.str")) + ) + or isinstance(typ, ParamSpecType) + or is_subtype( typ, self.chk.named_generic_type( "_typeshed.SupportsKeysAndGetItem", @@ -6147,7 +6156,6 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: "_typeshed.SupportsKeysAndGetItem", [UninhabitedType(), UninhabitedType()] ), ) - or isinstance(typ, ParamSpecType) ) def not_ready_callback(self, name: str, context: Context) -> None: diff --git a/mypy/messages.py b/mypy/messages.py index 471b1f65e642..65bcbd4049e2 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1401,7 +1401,7 @@ def invalid_var_arg(self, typ: Type, context: Context) -> None: def invalid_keyword_var_arg(self, typ: Type, is_mapping: bool, context: Context) -> None: typ = get_proper_type(typ) if isinstance(typ, Instance) and is_mapping: - self.fail("Keywords must be strings", context) + self.fail("Argument after ** must have string keys", context, code=codes.ARG_TYPE) else: self.fail( f"Argument after ** must be a mapping, not {format_type(typ, self.options)}", diff --git a/test-data/unit/check-generic-subtyping.test b/test-data/unit/check-generic-subtyping.test index 3dd117826493..7e954a6a6685 100644 --- a/test-data/unit/check-generic-subtyping.test +++ b/test-data/unit/check-generic-subtyping.test @@ -1014,7 +1014,7 @@ func_with_kwargs(**x1) [out] main:12: note: Revealed type is "typing.Iterator[builtins.int]" main:13: note: Revealed type is "builtins.dict[builtins.int, builtins.str]" -main:14: error: Keywords must be strings +main:14: error: Argument after ** must have string keys main:14: error: Argument 1 to "func_with_kwargs" has incompatible type "**X1[str, int]"; expected "int" [builtins fixtures/dict.pyi] [typing fixtures/typing-medium.pyi] diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 7c9af8eeb11b..cf8205471249 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -334,7 +334,7 @@ def f(**kwargs: 'A') -> None: pass b: Mapping d: Mapping[A, A] m: Mapping[str, A] -f(**d) # E: Keywords must be strings +f(**d) # E: Argument after ** must have string keys f(**m) f(**b) class A: pass @@ -354,7 +354,7 @@ from typing import Dict, Any, Optional class A: pass def f(**kwargs: 'A') -> None: pass d = {} # type: Dict[A, A] -f(**d) # E: Keywords must be strings +f(**d) # E: Argument after ** must have string keys f(**A()) # E: Argument after ** must be a mapping, not "A" kwargs: Optional[Any] f(**kwargs) # E: Argument after ** must be a mapping, not "Any | None" @@ -449,7 +449,7 @@ f(b) # E: Argument 1 to "f" has incompatible type "dict[str, str]"; expected "in f(**b) # E: Argument 1 to "f" has incompatible type "**dict[str, str]"; expected "int" c = {0: 0} -f(**c) # E: Keywords must be strings +f(**c) # E: Argument after ** must have string keys [builtins fixtures/dict.pyi] [case testCallStar2WithStar] @@ -567,3 +567,13 @@ main:38: error: Argument after ** must be a mapping, not "C[str, float]" main:39: error: Argument after ** must be a mapping, not "D" main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; expected "float" [builtins fixtures/dict.pyi] + +[case testLiteralKwargs] +from typing import Any, Literal +kw: dict[Literal["a", "b"], Any] +def func(a, b): ... +func(**kw) + +badkw: dict[Literal["one", 1], Any] +func(**badkw) # E: Argument after ** must have string keys +[builtins fixtures/dict.pyi]