From 800345831769a379e343faaf5d27db8d9d19ce72 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Mon, 15 Dec 2025 19:55:47 +0100 Subject: [PATCH 1/5] added utility for mapping types to instances --- mypy/checkexpr.py | 63 ++++++++++++++++++++------------ mypy/maptype.py | 51 +++++++++++++++++++++++++- test-data/unit/check-kwargs.test | 16 ++++---- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9990caaeb7a1..b26f486a98de 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -19,6 +19,7 @@ from mypy.checkmember import analyze_member_access, has_operator from mypy.checkstrformat import StringFormatterChecker from mypy.constant_fold import constant_fold_expr +from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error from mypy.expandtype import ( @@ -30,7 +31,7 @@ from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments from mypy.literals import literal -from mypy.maptype import map_instance_to_supertype +from mypy.maptype import map_instance_to_supertype, map_type_to_instance from mypy.meet import is_overlapping_types, narrow_declared_type from mypy.message_registry import ErrorMessage from mypy.messages import MessageBuilder, format_type @@ -118,6 +119,7 @@ Plugin, ) from mypy.semanal_enum import ENUM_BASES +from mypy.solve import solve_constraints from mypy.state import state from mypy.subtypes import ( covers_at_runtime, @@ -6143,28 +6145,43 @@ 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 ( - ( - # 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", - [self.named_type("builtins.str"), AnyType(TypeOfAny.special_form)], - ), - ) - or is_subtype( - typ, - self.chk.named_generic_type( - "_typeshed.SupportsKeysAndGetItem", [UninhabitedType(), UninhabitedType()] - ), - ) + if isinstance(typ, ParamSpecType | AnyType): + return True + + # Check if 'typ' is a SupportsKeysAndGetItem[T, Any] for some T <: str + # Note: is_subtype(typ, SupportsKeysAndGetItem[str, Any])` is too harsh + # since SupportsKeysAndGetItem is invariant in the key type parameter. + + # create a TypeVar and template type + T = TypeVarType( + "T", + "T", + id=TypeVarId(-1, namespace=""), + values=[], + upper_bound=self.named_type("builtins.str"), + default=AnyType(TypeOfAny.from_omitted_generics), + ) + template = self.chk.named_generic_type( + "_typeshed.SupportsKeysAndGetItem", [T, AnyType(TypeOfAny.special_form)] + ) + + return map_type_to_instance(typ, template) is not None + + # infer constraints and solve + constraints: list[Constraint] = [ + # solve_constraints seems to completely ignore upper bounds. + # So we need to include it manually. + Constraint(T, SUBTYPE_OF, T.upper_bound), + *infer_constraints(template, typ, SUPERTYPE_OF), + ] + solution, _ = solve_constraints([T], constraints) + assert len(solution) == 1 + + return solution[0] is not None and is_subtype( + typ, + self.chk.named_generic_type( + "_typeshed.SupportsKeysAndGetItem", [solution[0], AnyType(TypeOfAny.special_form)] + ), ) def not_ready_callback(self, name: str, context: Context) -> None: diff --git a/mypy/maptype.py b/mypy/maptype.py index 59ecb2bc9993..7471923e8e02 100644 --- a/mypy/maptype.py +++ b/mypy/maptype.py @@ -1,8 +1,55 @@ from __future__ import annotations -from mypy.expandtype import expand_type_by_instance +from mypy.expandtype import expand_type, expand_type_by_instance from mypy.nodes import TypeInfo -from mypy.types import AnyType, Instance, TupleType, TypeOfAny, has_type_vars +from mypy.types import AnyType, Instance, TupleType, Type, TypeOfAny, has_type_vars + + +def map_type_to_instance(typ: Type, target: Instance) -> Instance | None: + """Attempt to map `typ` to an Instance of the same class as `target` + + Examples: + (list[int], Iterable[T]) -> Iterable[int] + (list[list[int]], Iterable[list[T]]) -> Iterable[list[int]] + (dict[str, int], Mapping[K, int]) -> Mapping[str, int] + (list[int], Mapping[K, V]) -> None + + Args: + typ: The type to map from. + target: The target instance type to map to. + + Returns: + None: if the mapping is not possible. + Instance: the mapped instance type if the mapping is possible. + """ + from mypy.subtypes import is_subtype + from mypy.typeops import get_all_type_vars + + # 1. get type vars of target + tvars = get_all_type_vars(target) + + # fast path: if no type vars, + if not tvars: + return target if is_subtype(typ, target) else None + + from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints + from mypy.solve import solve_constraints + + # 2. determine constraints + constraints: list[Constraint] = infer_constraints(target, typ, SUPERTYPE_OF) + for tvar in tvars: + constraints.append(Constraint(tvar, SUBTYPE_OF, tvar.upper_bound)) + + # 3. solve constraints + solution, _ = solve_constraints(tvars, constraints) + + if None in solution: + return None + + # 4. build resulting Instance by substituting typevars with solution + env = {tvar.id: sol for tvar, sol in zip(tvars, solution)} + target = expand_type(target, env) + return target if is_subtype(typ, target) else None def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Instance: diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 4099716bcf6b..55a80d426c7e 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -572,11 +572,13 @@ main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; ex [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 +from typing import Literal +def func(a: int, b: int) -> None: ... + +def test( + good_kw: dict[Literal["a", "b"], int], + bad_kw: dict[Literal["one", 1], int], +) -> None: + func(**good_kw) + func(**bad_kw) # E: Argument after ** must have string keys [builtins fixtures/dict.pyi] From 1ea4d1e17cada93e9fd006c7565fd5a84ff81b88 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Mon, 15 Dec 2025 20:10:52 +0100 Subject: [PATCH 2/5] introduced helper function to map type to protocol --- mypy/checkexpr.py | 24 +++--------------------- mypy/maptype.py | 34 ++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b26f486a98de..afb02e804113 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -19,7 +19,7 @@ from mypy.checkmember import analyze_member_access, has_operator from mypy.checkstrformat import StringFormatterChecker from mypy.constant_fold import constant_fold_expr -from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints +from mypy.constraints import SUBTYPE_OF from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error from mypy.expandtype import ( @@ -31,7 +31,7 @@ from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments from mypy.literals import literal -from mypy.maptype import map_instance_to_supertype, map_type_to_instance +from mypy.maptype import as_type, map_instance_to_supertype from mypy.meet import is_overlapping_types, narrow_declared_type from mypy.message_registry import ErrorMessage from mypy.messages import MessageBuilder, format_type @@ -119,7 +119,6 @@ Plugin, ) from mypy.semanal_enum import ENUM_BASES -from mypy.solve import solve_constraints from mypy.state import state from mypy.subtypes import ( covers_at_runtime, @@ -6165,24 +6164,7 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: "_typeshed.SupportsKeysAndGetItem", [T, AnyType(TypeOfAny.special_form)] ) - return map_type_to_instance(typ, template) is not None - - # infer constraints and solve - constraints: list[Constraint] = [ - # solve_constraints seems to completely ignore upper bounds. - # So we need to include it manually. - Constraint(T, SUBTYPE_OF, T.upper_bound), - *infer_constraints(template, typ, SUPERTYPE_OF), - ] - solution, _ = solve_constraints([T], constraints) - assert len(solution) == 1 - - return solution[0] is not None and is_subtype( - typ, - self.chk.named_generic_type( - "_typeshed.SupportsKeysAndGetItem", [solution[0], AnyType(TypeOfAny.special_form)] - ), - ) + return as_type(typ, SUBTYPE_OF, template) is not None def not_ready_callback(self, name: str, context: Context) -> None: """Called when we can't infer the type of a variable because it's not ready yet. diff --git a/mypy/maptype.py b/mypy/maptype.py index 7471923e8e02..f595a478a240 100644 --- a/mypy/maptype.py +++ b/mypy/maptype.py @@ -1,26 +1,30 @@ from __future__ import annotations +from typing import cast + from mypy.expandtype import expand_type, expand_type_by_instance from mypy.nodes import TypeInfo from mypy.types import AnyType, Instance, TupleType, Type, TypeOfAny, has_type_vars -def map_type_to_instance(typ: Type, target: Instance) -> Instance | None: - """Attempt to map `typ` to an Instance of the same class as `target` - - Examples: - (list[int], Iterable[T]) -> Iterable[int] - (list[list[int]], Iterable[list[T]]) -> Iterable[list[int]] - (dict[str, int], Mapping[K, int]) -> Mapping[str, int] - (list[int], Mapping[K, V]) -> None +def as_type(typ: Type, direction: int, target: Type) -> Type | None: + """Attempts to solve type variables in `target` so that `typ` is a subtype/supertype of + the resulting type. Args: typ: The type to map from. + direction: One of SUBTYPE_OF or SUPERTYPE_OF target: The target instance type to map to. Returns: None: if the mapping is not possible. - Instance: the mapped instance type if the mapping is possible. + Type: the mapped type if the mapping is possible. + + Examples: + (list[int], Iterable[T]) -> Iterable[int] + (list[list[int]], Iterable[list[T]]) -> Iterable[list[int]] + (dict[str, int], Mapping[K, int]) -> Mapping[str, int] + (list[int], Mapping[K, V]) -> None """ from mypy.subtypes import is_subtype from mypy.typeops import get_all_type_vars @@ -28,16 +32,18 @@ def map_type_to_instance(typ: Type, target: Instance) -> Instance | None: # 1. get type vars of target tvars = get_all_type_vars(target) - # fast path: if no type vars, + # fast path: if no type vars, just check subtype if not tvars: return target if is_subtype(typ, target) else None - from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints + from mypy.constraints import SUBTYPE_OF, Constraint, infer_constraints from mypy.solve import solve_constraints # 2. determine constraints - constraints: list[Constraint] = infer_constraints(target, typ, SUPERTYPE_OF) + constraints: list[Constraint] = infer_constraints(target, typ, not direction) for tvar in tvars: + # need to manually include these because solve_constraints ignores them + # apparently constraints.append(Constraint(tvar, SUBTYPE_OF, tvar.upper_bound)) # 3. solve constraints @@ -46,8 +52,8 @@ def map_type_to_instance(typ: Type, target: Instance) -> Instance | None: if None in solution: return None - # 4. build resulting Instance by substituting typevars with solution - env = {tvar.id: sol for tvar, sol in zip(tvars, solution)} + # 4. build resulting Type by substituting type vars with solution + env = {tvar.id: s for tvar, s in zip(tvars, cast("list[Type]", solution))} target = expand_type(target, env) return target if is_subtype(typ, target) else None From 5482d98a40ca875f7c2f229f3a17031323b89c9b Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Tue, 16 Dec 2025 10:03:52 +0100 Subject: [PATCH 3/5] test also custom key+getitem class --- test-data/unit/check-kwargs.test | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 55a80d426c7e..bbc189628f60 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -572,13 +572,29 @@ main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; ex [builtins fixtures/dict.pyi] [case testLiteralKwargs] -from typing import Literal +from typing import Literal, Mapping, Iterable def func(a: int, b: int) -> None: ... +class GOOD_KW: + def keys(self) -> Iterable[Literal["a", "b"]]: ... + def __getitem__(self, key: str) -> int: ... + +class BAD_KW: + def keys(self) -> Iterable[Literal["one", 1]]: ... + def __getitem__(self, key: str) -> int: ... + def test( - good_kw: dict[Literal["a", "b"], int], - bad_kw: dict[Literal["one", 1], int], + good_kw: GOOD_KW, + bad_kw: BAD_KW, + good_dict: dict[Literal["a", "b"], int], + bad_dict: dict[Literal["one", 1], int], + good_mapping: Mapping[Literal["a", "b"], int], + bad_mapping: Mapping[Literal["one", 1], int], ) -> None: func(**good_kw) func(**bad_kw) # E: Argument after ** must have string keys + func(**good_dict) + func(**bad_dict) # E: Argument after ** must have string keys + func(**good_mapping) + func(**bad_mapping) # E: Argument after ** must have string keys [builtins fixtures/dict.pyi] From 8cc1c298fd1e18c6e873c2b75cdc6b6183b6e90e Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 19 Dec 2025 15:54:52 +0100 Subject: [PATCH 4/5] renamed and moved function to subtypes --- mypy/checkexpr.py | 6 ++--- mypy/maptype.py | 57 ++--------------------------------------------- mypy/subtypes.py | 44 ++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 58 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index afb02e804113..e973f31fc6c4 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -19,7 +19,6 @@ from mypy.checkmember import analyze_member_access, has_operator from mypy.checkstrformat import StringFormatterChecker from mypy.constant_fold import constant_fold_expr -from mypy.constraints import SUBTYPE_OF from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error from mypy.expandtype import ( @@ -31,7 +30,7 @@ from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments from mypy.literals import literal -from mypy.maptype import as_type, map_instance_to_supertype +from mypy.maptype import map_instance_to_supertype from mypy.meet import is_overlapping_types, narrow_declared_type from mypy.message_registry import ErrorMessage from mypy.messages import MessageBuilder, format_type @@ -127,6 +126,7 @@ is_same_type, is_subtype, non_method_protocol_members, + solve_as_subtype, ) from mypy.traverser import ( all_name_and_member_expressions, @@ -6164,7 +6164,7 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: "_typeshed.SupportsKeysAndGetItem", [T, AnyType(TypeOfAny.special_form)] ) - return as_type(typ, SUBTYPE_OF, template) is not None + return solve_as_subtype(typ, template) is not None def not_ready_callback(self, name: str, context: Context) -> None: """Called when we can't infer the type of a variable because it's not ready yet. diff --git a/mypy/maptype.py b/mypy/maptype.py index f595a478a240..59ecb2bc9993 100644 --- a/mypy/maptype.py +++ b/mypy/maptype.py @@ -1,61 +1,8 @@ from __future__ import annotations -from typing import cast - -from mypy.expandtype import expand_type, expand_type_by_instance +from mypy.expandtype import expand_type_by_instance from mypy.nodes import TypeInfo -from mypy.types import AnyType, Instance, TupleType, Type, TypeOfAny, has_type_vars - - -def as_type(typ: Type, direction: int, target: Type) -> Type | None: - """Attempts to solve type variables in `target` so that `typ` is a subtype/supertype of - the resulting type. - - Args: - typ: The type to map from. - direction: One of SUBTYPE_OF or SUPERTYPE_OF - target: The target instance type to map to. - - Returns: - None: if the mapping is not possible. - Type: the mapped type if the mapping is possible. - - Examples: - (list[int], Iterable[T]) -> Iterable[int] - (list[list[int]], Iterable[list[T]]) -> Iterable[list[int]] - (dict[str, int], Mapping[K, int]) -> Mapping[str, int] - (list[int], Mapping[K, V]) -> None - """ - from mypy.subtypes import is_subtype - from mypy.typeops import get_all_type_vars - - # 1. get type vars of target - tvars = get_all_type_vars(target) - - # fast path: if no type vars, just check subtype - if not tvars: - return target if is_subtype(typ, target) else None - - from mypy.constraints import SUBTYPE_OF, Constraint, infer_constraints - from mypy.solve import solve_constraints - - # 2. determine constraints - constraints: list[Constraint] = infer_constraints(target, typ, not direction) - for tvar in tvars: - # need to manually include these because solve_constraints ignores them - # apparently - constraints.append(Constraint(tvar, SUBTYPE_OF, tvar.upper_bound)) - - # 3. solve constraints - solution, _ = solve_constraints(tvars, constraints) - - if None in solution: - return None - - # 4. build resulting Type by substituting type vars with solution - env = {tvar.id: s for tvar, s in zip(tvars, cast("list[Type]", solution))} - target = expand_type(target, env) - return target if is_subtype(typ, target) else None +from mypy.types import AnyType, Instance, TupleType, TypeOfAny, has_type_vars def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Instance: diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 350d57a7e4ad..66b25785b4a3 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -8,6 +8,7 @@ import mypy.constraints import mypy.typeops from mypy.checker_state import checker_state +from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints from mypy.erasetype import erase_type from mypy.expandtype import ( expand_self_type, @@ -35,6 +36,7 @@ ) from mypy.options import Options from mypy.state import state +from mypy.typeops import get_all_type_vars from mypy.types import ( MYPYC_NATIVE_INT_NAMES, TUPLE_LIKE_INSTANCE_NAMES, @@ -2311,3 +2313,45 @@ def is_erased_instance(t: Instance) -> bool: elif not isinstance(get_proper_type(arg), AnyType): return False return True + + +def solve_as_subtype(typ: Type, target: Type) -> Type | None: + """Solves type variables in `target` so that `typ` becomes a subtype of `target`. + + Returns: + None: if the mapping is not possible. + Type: the mapped type if the mapping is possible. + + Examples: + (list[int], Iterable[T]) -> Iterable[int] + (list[list[int]], Iterable[list[T]]) -> Iterable[list[int]] + (dict[str, int], Mapping[K, int]) -> Mapping[str, int] + (list[int], Mapping[K, V]) -> None + """ + + # 1. get type vars of target + tvars = get_all_type_vars(target) + + # fast path: if no type vars, just check subtype + if not tvars: + return target if is_subtype(typ, target) else None + + from mypy.solve import solve_constraints + + # 2. determine constraints + constraints: list[Constraint] = infer_constraints(target, typ, SUPERTYPE_OF) + for tvar in tvars: + # need to manually include these because solve_constraints ignores them + # apparently + constraints.append(Constraint(tvar, SUBTYPE_OF, tvar.upper_bound)) + + # 3. solve constraints + solution, _ = solve_constraints(tvars, constraints) + + if None in solution: + return None + + # 4. build resulting Type by substituting type vars with solution + env = {tvar.id: s for tvar, s in zip(tvars, cast("list[Type]", solution))} + target = expand_type(target, env) + return target if is_subtype(typ, target) else None From 364b0686b0b7d9468d1380b627a753b3c5456332 Mon Sep 17 00:00:00 2001 From: Randolf Scholz Date: Fri, 19 Dec 2025 17:54:17 +0100 Subject: [PATCH 5/5] reworked is_valid_keyword_var_arg --- mypy/checkexpr.py | 28 +++++++++++++++---- mypy/messages.py | 2 +- test-data/unit/check-kwargs.test | 17 ++++++++++- .../unit/check-parameter-specification.test | 12 ++++++-- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e973f31fc6c4..c8746602922a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -6144,9 +6144,29 @@ 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) - if isinstance(typ, ParamSpecType | AnyType): + + # factorize over unions + if isinstance(typ, UnionType): + return all(self.is_valid_keyword_var_arg(item) for item in typ.items) + + if isinstance(typ, AnyType): return True + if isinstance(typ, ParamSpecType): + return typ.flavor == ParamSpecFlavor.KWARGS + + # fast path for builtins.dict + if isinstance(typ, Instance) and typ.type.fullname == "builtins.dict": + return is_subtype(typ.args[0], self.named_type("builtins.str")) + + # fast fail if not SupportsKeysAndGetItem[Any, Any] + any_type = AnyType(TypeOfAny.from_omitted_generics) + if not is_subtype( + typ, + self.chk.named_generic_type("_typeshed.SupportsKeysAndGetItem", [any_type, any_type]), + ): + return False + # Check if 'typ' is a SupportsKeysAndGetItem[T, Any] for some T <: str # Note: is_subtype(typ, SupportsKeysAndGetItem[str, Any])` is too harsh # since SupportsKeysAndGetItem is invariant in the key type parameter. @@ -6158,11 +6178,9 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: id=TypeVarId(-1, namespace=""), values=[], upper_bound=self.named_type("builtins.str"), - default=AnyType(TypeOfAny.from_omitted_generics), - ) - template = self.chk.named_generic_type( - "_typeshed.SupportsKeysAndGetItem", [T, AnyType(TypeOfAny.special_form)] + default=any_type, ) + template = self.chk.named_generic_type("_typeshed.SupportsKeysAndGetItem", [T, any_type]) return solve_as_subtype(typ, template) is not None diff --git a/mypy/messages.py b/mypy/messages.py index bbcc93ebfb25..3fc876dbbc07 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: + if is_mapping: self.fail("Argument after ** must have string keys", context, code=codes.ARG_TYPE) else: self.fail( diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index bbc189628f60..3caba651f2a1 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -572,7 +572,7 @@ main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; ex [builtins fixtures/dict.pyi] [case testLiteralKwargs] -from typing import Literal, Mapping, Iterable +from typing import Literal, Mapping, Iterable, Union def func(a: int, b: int) -> None: ... class GOOD_KW: @@ -597,4 +597,19 @@ def test( func(**bad_dict) # E: Argument after ** must have string keys func(**good_mapping) func(**bad_mapping) # E: Argument after ** must have string keys + +def test_union( + good_kw: Union[GOOD_KW, dict[str, int]], + bad_kw: Union[BAD_KW, dict[str, int]], + good_dict: Union[dict[Literal["a", "b"], int], dict[str, int]], + bad_dict: Union[dict[Literal["one", 1], int], dict[str, int]], + good_mapping: Union[Mapping[Literal["a", "b"], int], dict[str, int]], + bad_mapping: Union[Mapping[Literal["one", 1], int], dict[str, int]], +) -> None: + func(**good_kw) + func(**bad_kw) # E: Argument after ** must have string keys + func(**good_dict) + func(**bad_dict) # E: Argument after ** must have string keys + func(**good_mapping) + func(**bad_mapping) # E: Argument after ** must have string keys [builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 41a6c5b33cb9..9c11f4041092 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -461,14 +461,16 @@ class C(Generic[P, P2]): self.m1(*args, **kwargs) self.m2(*args, **kwargs) # E: Argument 1 to "m2" of "C" has incompatible type "*P.args"; expected "P2.args" \ # E: Argument 2 to "m2" of "C" has incompatible type "**P.kwargs"; expected "P2.kwargs" - self.m1(*kwargs, **args) # E: Argument 1 to "m1" of "C" has incompatible type "*P.kwargs"; expected "P.args" \ - # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" + self.m1(*kwargs, **args) # E: Argument after ** must be a mapping, not "P.args" \ + # E: Argument 1 to "m1" of "C" has incompatible type "*P.kwargs"; expected "P.args" \ + # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" self.m3(*args, **kwargs) # E: Argument 1 to "m3" of "C" has incompatible type "*P.args"; expected "int" \ # E: Argument 2 to "m3" of "C" has incompatible type "**P.kwargs"; expected "int" self.m4(*args, **kwargs) # E: Argument 1 to "m4" of "C" has incompatible type "*P.args"; expected "int" \ # E: Argument 2 to "m4" of "C" has incompatible type "**P.kwargs"; expected "int" - self.m1(*args, **args) # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" + self.m1(*args, **args) # E: Argument after ** must be a mapping, not "P.args" \ + # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" self.m1(*kwargs, **kwargs) # E: Argument 1 to "m1" of "C" has incompatible type "*P.kwargs"; expected "P.args" def m2(self, *args: P2.args, **kwargs: P2.kwargs) -> None: @@ -479,6 +481,10 @@ class C(Generic[P, P2]): def m4(self, x: int) -> None: pass + + + + [builtins fixtures/dict.pyi] [case testParamSpecOverUnannotatedDecorator]