diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0101f8eec107..2360b060e37a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -957,7 +957,8 @@ def check_argument_types(self, arg_types: List[Type], arg_kinds: List[int], messages.invalid_var_arg(arg_type, context) if (arg_kinds[actual] == nodes.ARG_STAR2 and not self.is_valid_keyword_var_arg(arg_type)): - messages.invalid_keyword_var_arg(arg_type, context) + is_mapping = is_subtype(arg_type, self.chk.named_type('typing.Mapping')) + messages.invalid_keyword_var_arg(arg_type, is_mapping, context) # Get the type of an individual actual argument (for *args # and **args this is the item type, not the collection type). if (isinstance(arg_type, TupleType) @@ -2260,17 +2261,17 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: """Is a type valid as a **kwargs argument?""" if self.chk.options.python_version[0] >= 3: return is_subtype(typ, self.chk.named_generic_type( - 'builtins.dict', [self.named_type('builtins.str'), - AnyType()])) + 'typing.Mapping', [self.named_type('builtins.str'), + AnyType()])) else: return ( is_subtype(typ, self.chk.named_generic_type( - 'builtins.dict', + 'typing.Mapping', [self.named_type('builtins.str'), AnyType()])) or is_subtype(typ, self.chk.named_generic_type( - 'builtins.dict', + 'typing.Mapping', [self.named_type('builtins.unicode'), AnyType()]))) diff --git a/mypy/messages.py b/mypy/messages.py index c4cb6569aca7..8311794d5528 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -740,12 +740,16 @@ def could_not_infer_type_arguments(self, callee_type: CallableType, n: int, def invalid_var_arg(self, typ: Type, context: Context) -> None: self.fail('List or tuple expected as variable arguments', context) - def invalid_keyword_var_arg(self, typ: Type, context: Context) -> None: - if isinstance(typ, Instance) and (typ.type.fullname() == 'builtins.dict'): + def invalid_keyword_var_arg(self, typ: Type, is_mapping: bool, context: Context) -> None: + if isinstance(typ, Instance) and is_mapping: self.fail('Keywords must be strings', context) else: - self.fail('Argument after ** must be a dictionary', - context) + suffix = '' + if isinstance(typ, Instance): + suffix = ', not {}'.format(self.format(typ)) + self.fail( + 'Argument after ** must be a mapping{}'.format(suffix), + context) def undefined_in_superclass(self, member: str, context: Context) -> None: self.fail('"{}" undefined in superclass'.format(member), context) diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test index de229c76e631..fa35c8002079 100644 --- a/test-data/unit/check-kwargs.test +++ b/test-data/unit/check-kwargs.test @@ -280,12 +280,33 @@ reveal_type(formatter.__call__) # E: Revealed type is 'def (message: builtins.s [builtins fixtures/bool.pyi] [out] +[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] +f(**d) # E: Keywords must be strings +f(**m) +f(**b) +class A: pass +[builtins fixtures/dict.pyi] + +[case testPassingMappingSubclassForKeywordVarArg] +from typing import Mapping +class MappingSubclass(Mapping[str, str]): pass +def f(**kwargs: 'A') -> None: pass +d = None # type: MappingSubclass +f(**d) +class A: pass +[builtins fixtures/dict.pyi] + [case testInvalidTypeForKeywordVarArg] from typing import Dict -def f( **kwargs: 'A') -> None: pass +def f(**kwargs: 'A') -> None: pass d = None # type: Dict[A, A] f(**d) # E: Keywords must be strings -f(**A()) # E: Argument after ** must be a dictionary +f(**A()) # E: Argument after ** must be a mapping, not "A" class A: pass [builtins fixtures/dict.pyi] diff --git a/test-data/unit/fixtures/args.pyi b/test-data/unit/fixtures/args.pyi index b084fc6c68e5..b3d05b5a7f39 100644 --- a/test-data/unit/fixtures/args.pyi +++ b/test-data/unit/fixtures/args.pyi @@ -1,6 +1,6 @@ # Builtins stub used to support *args, **kwargs. -from typing import TypeVar, Generic, Iterable, Tuple, Dict, Any, overload +from typing import TypeVar, Generic, Iterable, Tuple, Dict, Any, overload, Mapping Tco = TypeVar('Tco', covariant=True) T = TypeVar('T') @@ -19,7 +19,8 @@ class type: def __call__(self, *args: Any, **kwargs: Any) -> Any: pass class tuple(Iterable[Tco], Generic[Tco]): pass -class dict(Generic[T, S]): pass + +class dict(Iterable[T], Mapping[T, S], Generic[T, S]): pass class int: def __eq__(self, o: object) -> bool: pass