diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4cc91f9cc123..686cd2a0ccc6 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4,7 +4,7 @@ from contextlib import contextmanager import itertools from typing import ( - cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator + cast, Dict, Set, List, Tuple, Callable, Union, Optional, Sequence, Iterator, Iterable ) from typing_extensions import ClassVar, Final, overload, TypeAlias as _TypeAlias @@ -69,7 +69,7 @@ try_expanding_sum_type_to_union, tuple_fallback, make_simplified_union, true_only, false_only, erase_to_union_or_bound, function_type, callable_type, try_getting_str_literals, custom_special_method, - is_literal_type_like, simple_literal_type, + is_literal_type_like, simple_literal_type, try_getting_str_literals_from_type ) from mypy.message_registry import ErrorMessage import mypy.errorcodes as codes @@ -1490,6 +1490,27 @@ def check_for_extra_actual_arguments(self, context) is_unexpected_arg_error = True ok = False + elif (isinstance(actual_type, Instance) and + actual_type.type.has_base('typing.Mapping')): + any_type = AnyType(TypeOfAny.special_form) + mapping_info = self.chk.named_generic_type('typing.Mapping', + [any_type, any_type]).type + supertype = map_instance_to_supertype(actual_type, mapping_info) + if messages and supertype.args: + args = try_getting_str_literals_from_type(supertype.args[0]) + if args and nodes.ARG_STAR2 not in callee.arg_kinds: + messages.unexpected_keyword_argument( + callee, args[0], supertype.args[0], context) + is_unexpected_arg_error = True + elif (args and nodes.ARG_POS in callee.arg_kinds and + not all(arg in callee.arg_names for arg in args) and + isinstance(actual_names, Iterable)): + act_names = [name for name, kind in + zip(iter(actual_names), actual_kinds) + if kind != nodes.ARG_STAR2] + messages.too_few_arguments(callee, context, act_names) + ok = False + # *args/**kwargs can be applied even if the function takes a fixed # number of positional arguments. This may succeed at runtime. @@ -4026,12 +4047,22 @@ 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?""" + mapping_type = self.chk.named_generic_type( + 'typing.Mapping', [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)]) + typ = get_proper_type(typ) + ret = ( - is_subtype(typ, self.chk.named_generic_type('typing.Mapping', - [self.named_type('builtins.str'), AnyType(TypeOfAny.special_form)])) or - is_subtype(typ, self.chk.named_generic_type('typing.Mapping', - [UninhabitedType(), UninhabitedType()])) or - isinstance(typ, ParamSpecType) + is_subtype(typ, mapping_type) or + (isinstance(typ, Instance) and + is_subtype(typ, self.chk.named_type('typing.Mapping')) and + try_getting_str_literals_from_type(map_instance_to_supertype( + typ, mapping_type.type).args[0]) is not None) or + # This condition is to avoid false-positive errors when empty dictionaries are + # passed with double-stars (e.g., **{})。The type of empty dicts is inferred to be + # dict[, ], which is not a subtype of Mapping[str, Any]。 + is_subtype(typ, self.chk.named_generic_type('typing.Mapping', + [UninhabitedType(), UninhabitedType()])) or + isinstance(typ, ParamSpecType) ) if self.chk.options.python_version[0] < 3: ret = ret or is_subtype(typ, self.chk.named_generic_type('typing.Mapping', diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index 9f8de1265ee7..5c06d17ac86f 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -135,14 +135,14 @@ class A: pass [case testKeywordArgumentsWithDynamicallyTypedCallable] from typing import Any -f = None # type: Any +f: Any = None f(x=f(), z=None()) # E: "None" not callable f(f, zz=None()) # E: "None" not callable f(x=None) [case testKeywordArgumentWithFunctionObject] from typing import Callable -f = None # type: Callable[[A, B], None] +f: Callable[[A, B], None] = None f(a=A(), b=B()) f(A(), b=B()) class A: pass @@ -212,8 +212,8 @@ class B: pass [case testKwargsAfterBareArgs] from typing import Tuple, Any def f(a, *, b=None) -> None: pass -a = None # type: Any -b = None # type: Any +a: Any = None +b: Any = None f(a, **b) [builtins fixtures/dict.pyi] @@ -237,7 +237,7 @@ class B: pass [case testKeywordArgAfterVarArgsWithBothCallerAndCalleeVarArgs] from typing import List def f(*a: 'A', b: 'B' = None) -> None: pass -a = None # type: List[A] +a: List[A] = None f(*a) f(A(), *a) f(b=B()) @@ -262,22 +262,20 @@ class A: pass [case testKwargsArgumentInFunctionBody] from typing import Dict, Any def f( **kwargs: 'A') -> None: - d1 = kwargs # type: Dict[str, A] - d2 = kwargs # type: Dict[A, Any] # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[A, Any]") - d3 = kwargs # type: Dict[Any, str] # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[Any, str]") + d1: Dict[str, A] = kwargs + d2: Dict[A, Any] = kwargs # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[A, Any]") + d3: Dict[Any, str] = kwargs # E: Incompatible types in assignment (expression has type "Dict[str, A]", variable has type "Dict[Any, str]") class A: pass [builtins fixtures/dict.pyi] -[out] [case testKwargsArgumentInFunctionBodyWithImplicitAny] from typing import Dict, Any def f(**kwargs) -> None: - d1 = kwargs # type: Dict[str, A] - d2 = kwargs # type: Dict[str, str] - d3 = kwargs # type: Dict[A, Any] # E: Incompatible types in assignment (expression has type "Dict[str, Any]", variable has type "Dict[A, Any]") + d1: Dict[str, A] = kwargs + d2: Dict[str, str] = kwargs + d3: Dict[A, Any] = kwargs # E: Incompatible types in assignment (expression has type "Dict[str, Any]", variable has type "Dict[A, Any]") class A: pass [builtins fixtures/dict.pyi] -[out] [case testCallingFunctionThatAcceptsVarKwargs] import typing @@ -295,10 +293,10 @@ class B: pass [case testCallingFunctionWithKeywordVarArgs] from typing import Dict def f( **kwargs: 'A') -> None: pass -d = None # type: Dict[str, A] +d: Dict[str, A] = None f(**d) f(x=A(), **d) -d2 = None # type: Dict[str, B] +d2: Dict[str, B] = None f(**d2) # E: Argument 1 to "f" has incompatible type "**Dict[str, B]"; expected "A" f(x=A(), **d2) # E: Argument 2 to "f" has incompatible type "**Dict[str, B]"; expected "A" f(**{'x': B()}) # E: Argument 1 to "f" has incompatible type "**Dict[str, B]"; expected "A" @@ -331,9 +329,9 @@ reveal_type(formatter.__call__) # N: Revealed type is "def (message: builtins.s [case testPassingMappingForKeywordVarArg] from typing import Mapping def f(**kwargs: 'A') -> None: pass -b = None # type: Mapping -d = None # type: Mapping[A, A] -m = None # type: Mapping[str, A] +b: Mapping = None +d: Mapping[A, A] = None +m: Mapping[str, A] = None f(**d) # E: Keywords must be strings f(**m) f(**b) @@ -344,16 +342,47 @@ class A: pass from typing import Mapping class MappingSubclass(Mapping[str, str]): pass def f(**kwargs: 'A') -> None: pass -d = None # type: MappingSubclass +d: MappingSubclass = None f(**d) class A: pass [builtins fixtures/dict.pyi] +[case testPassingMappingLiteralsForKeywordVarArg] +from typing import Mapping, Any, Union +from typing_extensions import Literal +def f(a=None, b=None, **kwargs) -> None: pass +def g(a: int, b: int) -> None: pass # N: "g" defined here +def h(a: int, b: int, **kwargs) -> None: pass + +s: Mapping[Literal[3], int] = {3: 2} +f(**s) # E: Keywords must be strings + +t: Mapping[Literal['b'], int] = {'b':2} +f(**t) +h(**t) + +u: Mapping[Literal['c'], int] = {'b':2} \ +# E: Dict entry 0 has incompatible type "Literal['b']": "int"; expected "Literal['c']": "int" +f(**u) + +v: Mapping[Literal['a','b'], int] = {'a':2, 'b':1} +f(**v) + +w: Mapping[Literal['d'], int] = {'c':2} \ +# E: Dict entry 0 has incompatible type "Literal['c']": "int"; expected "Literal['d']": "int" +f(**w) + +x: Mapping[Literal['c','d'], int] = {'c':1, 'd': 2} +g(**x) # E: Unexpected keyword argument "c" for "g" +h(**x) # E: Missing positional arguments "a", "b" in call to "h" + +[builtins fixtures/dict.pyi] + [case testInvalidTypeForKeywordVarArg] # flags: --strict-optional from typing import Dict, Any, Optional def f(**kwargs: 'A') -> None: pass -d = {} # type: Dict[A, A] +d: Dict[A, A] = None f(**d) # E: Keywords must be strings f(**A()) # E: Argument after ** must be a mapping, not "A" class A: pass @@ -364,9 +393,9 @@ f(**kwargs) # E: Argument after ** must be a mapping, not "Optional[Any]" [case testPassingKeywordVarArgsToNonVarArgsFunction] from typing import Any, Dict def f(a: 'A', b: 'B') -> None: pass -d = None # type: Dict[str, Any] +d: Dict[str, Any] = None f(**d) -d2 = None # type: Dict[str, A] +d2: Dict[str, A] = None f(**d2) # E: Argument 1 to "f" has incompatible type "**Dict[str, A]"; expected "B" class A: pass class B: pass @@ -375,8 +404,8 @@ class B: pass [case testBothKindsOfVarArgs] from typing import Any, List, Dict def f(a: 'A', b: 'A') -> None: pass -l = None # type: List[Any] -d = None # type: Dict[Any, Any] +l: List[Any] = None +d: Dict[Any, Any] = None f(*l, **d) class A: pass [builtins fixtures/dict.pyi] @@ -387,8 +416,8 @@ def f1(a: 'A', b: 'A') -> None: pass def f2(a: 'A') -> None: pass def f3(a: 'A', **kwargs: 'A') -> None: pass def f4(**kwargs: 'A') -> None: pass -d = None # type: Dict[Any, Any] -d2 = None # type: Dict[Any, Any] +d: Dict[Any, Any] = None +d2: Dict[Any, Any] = None f1(**d, **d2) f2(**d, **d2) f3(**d, **d2) @@ -399,14 +428,14 @@ class A: pass [case testPassingKeywordVarArgsToVarArgsOnlyFunction] from typing import Any, Dict def f(*args: 'A') -> None: pass -d = None # type: Dict[Any, Any] +d: Dict[Any, Any] = None f(**d) class A: pass [builtins fixtures/dict.pyi] [case testKeywordArgumentAndCommentSignature] import typing -def f(x): # type: (int) -> str # N: "f" defined here +def f(x: int) -> str: # N: "f" defined here pass f(x='') # E: Argument "x" to "f" has incompatible type "str"; expected "int" f(x=0) @@ -415,7 +444,7 @@ f(y=0) # E: Unexpected keyword argument "y" for "f" [case testKeywordArgumentAndCommentSignature2] import typing class A: - def f(self, x): # type: (int) -> str # N: "f" of "A" defined here + def f(self, x: int) -> str: # N: "f" of "A" defined here pass A().f(x='') # E: Argument "x" to "f" of "A" has incompatible type "str"; expected "int" A().f(x=0) @@ -423,7 +452,7 @@ A().f(y=0) # E: Unexpected keyword argument "y" for "f" of "A" [case testKeywordVarArgsAndCommentSignature] import typing -def f(**kwargs): # type: (**int) -> None +def f(**kwargs: int): pass f(z=1) f(x=1, y=1) @@ -487,11 +516,11 @@ def f(*vargs: int, **kwargs: object) -> None: def g(arg: int = 0, **kwargs: object) -> None: pass -d = {} # type: Dict[str, object] +d: Dict[str, object] = {} f(**d) g(**d) # E: Argument 1 to "g" has incompatible type "**Dict[str, object]"; expected "int" -m = {} # type: Mapping[str, object] +m: Mapping[str, object] = {} f(**m) g(**m) # E: Argument 1 to "g" has incompatible type "**Mapping[str, object]"; expected "int" [builtins fixtures/dict.pyi]