Skip to content

Commit

Permalink
Hack to type check keyword arguments to dict() better
Browse files Browse the repository at this point in the history
This is a stop-gap solution until we implement a general plugin
system for functions, or similar.

Fix #984. Also mostly fix #1010 (except for some special cases).
  • Loading branch information
JukkaL committed Apr 17, 2016
1 parent 37b6181 commit c545cc4
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 23 deletions.
13 changes: 12 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 14 additions & 6 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def infer_constraints_for_callable(
Return a list of constraints.
"""

constraints = [] # type: List[Constraint]
tuple_counter = [0]

Expand Down
2 changes: 2 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
77 changes: 70 additions & 7 deletions mypy/test/data/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,6 @@ def f(x: int) -> Iterator[None]:
-- ----------------



[case testYieldFromIteratorHasNoValue]
from typing import Iterator
def f() -> Iterator[int]:
Expand All @@ -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]"
Expand All @@ -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
21 changes: 14 additions & 7 deletions mypy/test/data/fixtures/dict.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 8 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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 = []
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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:
Expand Down

0 comments on commit c545cc4

Please sign in to comment.