Skip to content

Commit c96d66e

Browse files
fix testLiteralKwargs false positive for good_kw
1 parent 237a853 commit c96d66e

File tree

2 files changed

+46
-29
lines changed

2 files changed

+46
-29
lines changed

mypy/checkexpr.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from mypy.checkmember import analyze_member_access, has_operator
2020
from mypy.checkstrformat import StringFormatterChecker
2121
from mypy.constant_fold import constant_fold_expr
22+
from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints
2223
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars
2324
from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error
2425
from mypy.expandtype import (
@@ -118,6 +119,7 @@
118119
Plugin,
119120
)
120121
from mypy.semanal_enum import ENUM_BASES
122+
from mypy.solve import solve_constraints
121123
from mypy.state import state
122124
from mypy.subtypes import (
123125
covers_at_runtime,
@@ -6143,28 +6145,41 @@ def is_valid_var_arg(self, typ: Type) -> bool:
61436145
def is_valid_keyword_var_arg(self, typ: Type) -> bool:
61446146
"""Is a type valid as a **kwargs argument?"""
61456147
typ = get_proper_type(typ)
6146-
return (
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(
6156-
typ,
6157-
self.chk.named_generic_type(
6158-
"_typeshed.SupportsKeysAndGetItem",
6159-
[self.named_type("builtins.str"), AnyType(TypeOfAny.special_form)],
6160-
),
6161-
)
6162-
or is_subtype(
6163-
typ,
6164-
self.chk.named_generic_type(
6165-
"_typeshed.SupportsKeysAndGetItem", [UninhabitedType(), UninhabitedType()]
6166-
),
6167-
)
6148+
if isinstance(typ, ParamSpecType | AnyType):
6149+
return True
6150+
6151+
# Check if 'typ' is a SupportsKeysAndGetItem[T, Any] for some T <: str
6152+
# Note: is_subtype(typ, SupportsKeysAndGetItem[str, Any])` is too harsh
6153+
# since SupportsKeysAndGetItem is invariant in the key type parameter.
6154+
6155+
# create a TypeVar and template type
6156+
T = TypeVarType(
6157+
"T",
6158+
"T",
6159+
id=TypeVarId(-1, namespace="<kwargs>"),
6160+
values=[],
6161+
upper_bound=self.named_type("builtins.str"),
6162+
default=AnyType(TypeOfAny.from_omitted_generics),
6163+
)
6164+
template = self.chk.named_generic_type(
6165+
"_typeshed.SupportsKeysAndGetItem", [T, AnyType(TypeOfAny.special_form)]
6166+
)
6167+
6168+
# infer constraints and solve
6169+
constraints: list[Constraint] = [
6170+
# solve_constraints seems to completely ignore upper bounds.
6171+
# So we need to include it manually.
6172+
Constraint(T, SUBTYPE_OF, T.upper_bound),
6173+
*infer_constraints(template, typ, SUPERTYPE_OF),
6174+
]
6175+
solution, _ = solve_constraints([T], constraints)
6176+
assert len(solution) == 1
6177+
6178+
return solution[0] is not None and is_subtype(
6179+
typ,
6180+
self.chk.named_generic_type(
6181+
"_typeshed.SupportsKeysAndGetItem", [solution[0], AnyType(TypeOfAny.special_form)]
6182+
),
61686183
)
61696184

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

test-data/unit/check-kwargs.test

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -572,11 +572,13 @@ main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; ex
572572
[builtins fixtures/dict.pyi]
573573

574574
[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-
bad_kw: dict[Literal["one", 1], Any]
581-
func(**bad_kw) # E: Argument after ** must have string keys
575+
from typing import Literal
576+
def func(a: int, b: int) -> None: ...
577+
578+
def test(
579+
good_kw: dict[Literal["a", "b"], int],
580+
bad_kw: dict[Literal["one", 1], int],
581+
) -> None:
582+
func(**good_kw)
583+
func(**bad_kw) # E: Argument after ** must have string keys
582584
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)