diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index be56ee3cf63b5..878f386cb8728 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -15,7 +15,7 @@ ListComprehension, GeneratorExpr, SetExpr, MypyFile, Decorator, ConditionalExpr, ComparisonExpr, TempNode, SetComprehension, DictionaryComprehension, ComplexExpr, EllipsisExpr, - TypeAliasExpr, BackquoteExpr, ARG_POS + TypeAliasExpr, BackquoteExpr, ARG_POS, ARG_NAMED, ARG_STAR2 ) from mypy.nodes import function_type from mypy import nodes @@ -422,6 +422,17 @@ def infer_function_type_arguments(self, callee_type: CallableType, inferred_args) = self.infer_function_type_arguments_pass2( callee_type, args, arg_kinds, formal_to_actual, inferred_args, context) + + if inferred_args and callee_type.special_sig == 'dict' and ( + ARG_NAMED in arg_kinds or ARG_STAR2 in arg_kinds): + # HACK: Infer str key type for dict(...) with keyword args. The type system + # can't represent this so we special case it, as this is a pretty common + # thing. + if isinstance(inferred_args[0], NoneTyp): + inferred_args[0] = self.named_type('builtins.str') + elif not is_subtype(self.named_type('builtins.str'), inferred_args[0]): + self.msg.fail(messages.KEYWORD_ARGUMENT_REQUIRES_STR_KEY_TYPE, + context) else: # In dynamically typed functions use implicit 'Any' types for # type variables. diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 21b71f37b08cb..152728ea64269 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1,6 +1,6 @@ """Type checking of attribute access""" -from typing import cast, Callable, List +from typing import cast, Callable, List, Optional from mypy.types import ( Type, Instance, AnyType, TupleType, CallableType, FunctionLike, TypeVarDef, @@ -350,7 +350,7 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) -> arg_names=["_args", "_kwds"], ret_type=AnyType(), fallback=builtin_type('builtins.function')) - return class_callable(sig, info, fallback) + return class_callable(sig, info, fallback, None) # Construct callable type based on signature of __init__. Adjust # return type and insert type arguments. return type_object_type_from_function(init_method, info, fallback) @@ -372,17 +372,24 @@ def type_object_type_from_function(init_or_new: FuncBase, info: TypeInfo, signature = cast(FunctionLike, map_type_from_supertype(signature, info, init_or_new.info)) + if init_or_new.info.fullname() == 'builtins.dict': + # Special signature! + special_sig = 'dict' + else: + special_sig = None + if isinstance(signature, CallableType): - return class_callable(signature, info, fallback) + return class_callable(signature, info, fallback, special_sig) else: # Overloaded __init__/__new__. items = [] # type: List[CallableType] for item in cast(Overloaded, signature).items(): - items.append(class_callable(item, info, fallback)) + items.append(class_callable(item, info, fallback, special_sig)) return Overloaded(items) -def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance) -> CallableType: +def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance, + special_sig: Optional[str]) -> CallableType: """Create a type object type based on the signature of __init__.""" variables = [] # type: List[TypeVarDef] for i, tvar in enumerate(info.defn.type_vars): @@ -393,7 +400,8 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance) variables.extend(initvars) callable_type = init_type.copy_modified( - ret_type=self_type(info), fallback=type_type, name=None, variables=variables) + ret_type=self_type(info), fallback=type_type, name=None, variables=variables, + special_sig=special_sig) c = callable_type.with_name('"{}"'.format(info.name())) cc = convert_class_tvars_to_func_tvars(c, len(initvars)) cc.is_classmethod_class = True diff --git a/mypy/constraints.py b/mypy/constraints.py index 95068373eab30..5cb65d09ac853 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -45,7 +45,6 @@ def infer_constraints_for_callable( Return a list of constraints. """ - constraints = [] # type: List[Constraint] tuple_counter = [0] diff --git a/mypy/messages.py b/mypy/messages.py index 54d1131843cce..151c9ecdd38c4 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -79,6 +79,8 @@ FUNCTION_TYPE_EXPECTED = "Function is missing a type annotation" RETURN_TYPE_EXPECTED = "Function is missing a return type annotation" ARGUMENT_TYPE_EXPECTED = "Function is missing a type annotation for one or more arguments" +KEYWORD_ARGUMENT_REQUIRES_STR_KEY_TYPE = \ + 'Keyword argument only valid with "str" key type in call to "dict"' class MessageBuilder: diff --git a/mypy/test/data/check-expressions.test b/mypy/test/data/check-expressions.test index 80803e1751cf8..79bbfe4c5f784 100644 --- a/mypy/test/data/check-expressions.test +++ b/mypy/test/data/check-expressions.test @@ -1340,7 +1340,6 @@ def f(x: int) -> Iterator[None]: -- ---------------- - [case testYieldFromIteratorHasNoValue] from typing import Iterator def f() -> Iterator[int]: @@ -1362,12 +1361,14 @@ def g() -> Iterator[int]: [out] +-- dict(...) +-- --------- --- dict(x=y, ...) --- -------------- +-- Note that the stub used in unit tests does not have all overload +-- variants, but it should not matter. -[case testDictWithKeywordArgs] +[case testDictWithKeywordArgsOnly] from typing import Dict, Any d1 = dict(a=1, b=2) # type: Dict[str, int] d2 = dict(a=1, b='') # type: Dict[str, int] # E: List item 1 has incompatible type "Tuple[str, str]" @@ -1378,7 +1379,69 @@ d5 = dict(a=1, b='') # type: Dict[str, Any] [builtins fixtures/dict.py] [case testDictWithoutKeywordArgs] -dict(undefined) +from typing import Dict +d = dict() # E: Need type annotation for variable +d2 = dict() # type: Dict[int, str] +dict(undefined) # E: Name 'undefined' is not defined +[builtins fixtures/dict.py] + +[case testDictFromList] +from typing import Dict +d = dict([(1, 'x'), (2, 'y')]) +d() # E: Dict[int, str] not callable +d2 = dict([(1, 'x')]) # type: Dict[str, str] # E: List item 0 has incompatible type "Tuple[int, str]" +[builtins fixtures/dict.py] + +[case testDictFromIterableAndKeywordArg] +from typing import Dict +it = [('x', 1)] + +d = dict(it, x=1) +d() # E: Dict[str, int] not callable + +d2 = dict(it, x='') # E: Cannot infer type argument 2 of "dict" +d2() # E: Dict[Any, Any] not callable + +d3 = dict(it, x='') # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type "str"; expected "int" +[builtins fixtures/dict.py] + +[case testDictFromIterableAndKeywordArg2] +it = [(1, 'x')] +dict(it, x='y') # E: Keyword argument only valid with "str" key type in call to "dict" +[builtins fixtures/dict.py] + +[case testDictFromIterableAndKeywordArg3] +d = dict([], x=1) +d() # E: Dict[str, int] not callable +[builtins fixtures/dict.py] + +[case testDictFromIterableAndStarStarArgs] +from typing import Dict +it = [('x', 1)] + +kw = {'x': 1} +d = dict(it, **kw) +d() # E: Dict[str, int] not callable + +kw2 = {'x': ''} +d2 = dict(it, **kw2) # E: Cannot infer type argument 2 of "dict" +d2() # E: Dict[Any, Any] not callable + +d3 = dict(it, **kw2) # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type **Dict[str, str]; expected "int" +[builtins fixtures/dict.py] + +[case testDictFromIterableAndStarStarArgs2] +it = [(1, 'x')] +kw = {'x': 'y'} +d = dict(it, **kw) # E: Keyword argument only valid with "str" key type in call to "dict" +d() # E: Dict[int, str] not callable +[builtins fixtures/dict.py] + +[case testUserDefinedClassNamedDict] +from typing import Generic, TypeVar +T = TypeVar('T') +S = TypeVar('S') +class dict(Generic[T, S]): + def __init__(self, x: T, **kwargs: T) -> None: pass +dict(1, y=1) [builtins fixtures/dict.py] -[out] -main:1: error: Name 'undefined' is not defined diff --git a/mypy/test/data/fixtures/dict.py b/mypy/test/data/fixtures/dict.py index 715a8e2b4d24b..86ad7f5c8dd05 100644 --- a/mypy/test/data/fixtures/dict.py +++ b/mypy/test/data/fixtures/dict.py @@ -1,22 +1,29 @@ # Builtins stub used in dictionary-related test cases. -from typing import TypeVar, Generic, Iterable, Iterator, Tuple +from typing import TypeVar, Generic, Iterable, Iterator, Tuple, overload T = TypeVar('T') -S = TypeVar('S') +KT = TypeVar('KT') +VT = TypeVar('VT') class object: def __init__(self) -> None: pass class type: pass -class dict(Iterable[T], Generic[T, S]): - def __init__(self, arg: Iterable[Tuple[T, S]] = None) -> None: pass - def __setitem__(self, k: T, v: S) -> None: pass - def __iter__(self) -> Iterator[T]: pass - def update(self, a: 'dict[T, S]') -> None: pass +class dict(Iterable[KT], Generic[KT, VT]): + @overload + def __init__(self, **kwargs: VT) -> None: pass + @overload + def __init__(self, arg: Iterable[Tuple[KT, VT]], **kwargs: VT) -> None: pass + def __setitem__(self, k: KT, v: VT) -> None: pass + def __iter__(self) -> Iterator[KT]: pass + def update(self, a: 'dict[KT, VT]') -> None: pass + class int: pass # for convenience + class str: pass # for keyword argument key type + class list(Iterable[T], Generic[T]): # needed by some test cases def __iter__(self) -> Iterator[T]: pass def __mul__(self, x: int) -> list[T]: pass diff --git a/mypy/types.py b/mypy/types.py index 3726cb38a675b..fb1ea3eb1024c 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -405,6 +405,9 @@ class CallableType(FunctionLike): is_classmethod_class = False # Was this type implicitly generated instead of explicitly specified by the user? implicit = False + # Defined for signatures that require special handling (currently only value is 'dict' + # for a signature similar to 'dict') + special_sig = None # type: Optional[str] def __init__(self, arg_types: List[Type], @@ -419,6 +422,7 @@ def __init__(self, is_ellipsis_args: bool = False, implicit=False, is_classmethod_class=False, + special_sig=None, ) -> None: if variables is None: variables = [] @@ -435,6 +439,7 @@ def __init__(self, self.variables = variables self.is_ellipsis_args = is_ellipsis_args self.implicit = implicit + self.special_sig = special_sig super().__init__(line) def copy_modified(self, @@ -447,7 +452,8 @@ def copy_modified(self, definition: SymbolNode = _dummy, variables: List[TypeVarDef] = _dummy, line: int = _dummy, - is_ellipsis_args: bool = _dummy) -> 'CallableType': + is_ellipsis_args: bool = _dummy, + special_sig: Optional[str] = _dummy) -> 'CallableType': return CallableType( arg_types=arg_types if arg_types is not _dummy else self.arg_types, arg_kinds=arg_kinds if arg_kinds is not _dummy else self.arg_kinds, @@ -462,6 +468,7 @@ def copy_modified(self, is_ellipsis_args if is_ellipsis_args is not _dummy else self.is_ellipsis_args), implicit=self.implicit, is_classmethod_class=self.is_classmethod_class, + special_sig=special_sig if special_sig is not _dummy else self.special_sig, ) def is_type_obj(self) -> bool: