Skip to content

Commit eb144ce

Browse files
authored
Allow literals as kwargs dict keys (#20416)
Fixes #10023 , fixes #13674 , closes #10237
1 parent e089abc commit eb144ce

File tree

4 files changed

+25
-7
lines changed

4 files changed

+25
-7
lines changed

mypy/checkexpr.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6142,8 +6142,17 @@ def is_valid_var_arg(self, typ: Type) -> bool:
61426142

61436143
def is_valid_keyword_var_arg(self, typ: Type) -> bool:
61446144
"""Is a type valid as a **kwargs argument?"""
6145+
typ = get_proper_type(typ)
61456146
return (
6146-
is_subtype(
6147+
(
6148+
# This is a little ad hoc, ideally we would have a map_instance_to_supertype
6149+
# that worked for protocols
6150+
isinstance(typ, Instance)
6151+
and typ.type.fullname == "builtins.dict"
6152+
and is_subtype(typ.args[0], self.named_type("builtins.str"))
6153+
)
6154+
or isinstance(typ, ParamSpecType)
6155+
or is_subtype(
61476156
typ,
61486157
self.chk.named_generic_type(
61496158
"_typeshed.SupportsKeysAndGetItem",
@@ -6156,7 +6165,6 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool:
61566165
"_typeshed.SupportsKeysAndGetItem", [UninhabitedType(), UninhabitedType()]
61576166
),
61586167
)
6159-
or isinstance(typ, ParamSpecType)
61606168
)
61616169

61626170
def not_ready_callback(self, name: str, context: Context) -> None:

mypy/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1401,7 +1401,7 @@ def invalid_var_arg(self, typ: Type, context: Context) -> None:
14011401
def invalid_keyword_var_arg(self, typ: Type, is_mapping: bool, context: Context) -> None:
14021402
typ = get_proper_type(typ)
14031403
if isinstance(typ, Instance) and is_mapping:
1404-
self.fail("Keywords must be strings", context)
1404+
self.fail("Argument after ** must have string keys", context, code=codes.ARG_TYPE)
14051405
else:
14061406
self.fail(
14071407
f"Argument after ** must be a mapping, not {format_type(typ, self.options)}",

test-data/unit/check-generic-subtyping.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ func_with_kwargs(**x1)
10141014
[out]
10151015
main:12: note: Revealed type is "typing.Iterator[builtins.int]"
10161016
main:13: note: Revealed type is "builtins.dict[builtins.int, builtins.str]"
1017-
main:14: error: Keywords must be strings
1017+
main:14: error: Argument after ** must have string keys
10181018
main:14: error: Argument 1 to "func_with_kwargs" has incompatible type "**X1[str, int]"; expected "int"
10191019
[builtins fixtures/dict.pyi]
10201020
[typing fixtures/typing-medium.pyi]

test-data/unit/check-kwargs.test

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def f(**kwargs: 'A') -> None: pass
334334
b: Mapping
335335
d: Mapping[A, A]
336336
m: Mapping[str, A]
337-
f(**d) # E: Keywords must be strings
337+
f(**d) # E: Argument after ** must have string keys
338338
f(**m)
339339
f(**b)
340340
class A: pass
@@ -354,7 +354,7 @@ from typing import Dict, Any, Optional
354354
class A: pass
355355
def f(**kwargs: 'A') -> None: pass
356356
d = {} # type: Dict[A, A]
357-
f(**d) # E: Keywords must be strings
357+
f(**d) # E: Argument after ** must have string keys
358358
f(**A()) # E: Argument after ** must be a mapping, not "A"
359359
kwargs: Optional[Any]
360360
f(**kwargs) # E: Argument after ** must be a mapping, not "Any | None"
@@ -452,7 +452,7 @@ f(b) # E: Argument 1 to "f" has incompatible type "dict[str, str]"; expected "in
452452
f(**b) # E: Argument 1 to "f" has incompatible type "**dict[str, str]"; expected "int"
453453

454454
c = {0: 0}
455-
f(**c) # E: Keywords must be strings
455+
f(**c) # E: Argument after ** must have string keys
456456
[builtins fixtures/dict.pyi]
457457

458458
[case testCallStar2WithStar]
@@ -570,3 +570,13 @@ main:38: error: Argument after ** must be a mapping, not "C[str, float]"
570570
main:39: error: Argument after ** must be a mapping, not "D"
571571
main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; expected "float"
572572
[builtins fixtures/dict.pyi]
573+
574+
[case testLiteralKwargs]
575+
from typing import Any, Literal
576+
kw: dict[Literal["a", "b"], Any]
577+
def func(a, b): ...
578+
func(**kw)
579+
580+
badkw: dict[Literal["one", 1], Any]
581+
func(**badkw) # E: Argument after ** must have string keys
582+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)