8000 Hack to type check keyword arguments to dict() better · python/mypy@f777c01 · GitHub
[go: up one dir, main page]

Skip to content

Commit f777c01

Browse files
committed
Hack to type check keyword arguments to dict() better
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).
1 parent 8787567 commit f777c01

File tree

7 files changed

+120
-23
lines changed

7 files changed

+120
-23
lines changed

mypy/checkexpr.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
ListComprehension, GeneratorExpr, SetExpr, MypyFile, Decorator,
1616
ConditionalExpr, ComparisonExpr, TempNode, SetComprehension,
1717
DictionaryComprehension, ComplexExpr, EllipsisExpr,
18-
TypeAliasExpr, BackquoteExpr, ARG_POS
18+
TypeAliasExpr, BackquoteExpr, ARG_POS, ARG_NAMED, ARG_STAR2
1919
)
2020
from mypy.nodes import function_type
2121
from mypy import nodes
@@ -422,6 +422,17 @@ def infer_function_type_arguments(self, callee_type: CallableType,
422422
inferred_args) = self.infer_function_type_arguments_pass2(
423423
callee_type, args, arg_kinds, formal_to_actual,
424424
inferred_args, context)
425+
426+
if inferred_args and callee_type.special_sig == 'dict' and (
427+
ARG_NAMED in arg_kinds or ARG_STAR2 in arg_kinds):
428+
# HACK: Infer str key type for dict(...) with keyword args. The type system
429+
# can't represent this so we special case it, as this is a pretty common
430+
# thing.
431+
if isinstance(inferred_args[0], NoneTyp):
432+
inferred_args[0] = self.named_type('builtins.str')
433+
elif not is_subtype(self.named_type('builtins.str'), inferred_args[0]):
434+
self.msg.fail(messages.KEYWORD_ARGUMENT_REQUIRES_STR_KEY_TYPE,
435+
context)
425436
else:
426437
# In dynamically typed functions use implicit 'Any' types for
427438
# type variables.

mypy/checkmember.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Type checking of attribute access"""
22

3-
from typing import cast, Callable, List
3+
from typing import cast, Callable, List, Optional
44

55
from mypy.types import (
66
Type, Instance, AnyType, TupleType, CallableType, FunctionLike, TypeVarDef,
@@ -350,7 +350,7 @@ def type_object_type(info: TypeInfo, builtin_type: Callable[[str], Instance]) ->
350350
arg_names=["_args", "_kwds"],
351351
ret_type=AnyType(),
352352
fallback=builtin_type('builtins.function'))
353-
return class_callable(sig, info, fallback)
353+
return class_callable(sig, info, fallback, None)
354354
# Construct callable type based on signature of __init__. Adjust
355355
# return type and insert type arguments.
356356
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,
372372
signature = cast(FunctionLike,
373373
map_type_from_supertype(signature, info, init_or_new.info))
374374

375+
if init_or_new.info.fullname() == 'builtins.dict':
376+
# Special signature!
377+
special_sig = 'dict'
378+
else:
379+
special_sig = None
380+
375381
if isinstance(signature, CallableType):
376-
return class_callable(signature, info, fallback)
382+
return class_callable(signature, info, fallback, special_sig)
377383
else:
378384
# Overloaded __init__/__new__.
379385
items = [] # type: List[CallableType]
380386
for item in cast(Overloaded, signature).items():
381-
items.append(class_callable(item, info, fallback))
387+
items.append(class_callable(item, info, fallback, special_sig))
382388
return Overloaded(items)
383389

384390

385-
def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance) -> CallableType:
391+
def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance,
392+
special_sig: Optional[str]) -> CallableType:
386393
"""Create a type object type based on the signature of __init__."""
387394
variables = [] # type: List[TypeVarDef]
388395
for i, tvar in enumerate(info.defn.type_vars):
@@ -393,7 +400,8 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance)
393400
variables.extend(initvars)
394401

395402
callable_type = init_type.copy_modified(
396-
ret_type=self_type(info), fallback=type_type, name=None, variables=variables)
403+
ret_type=self_type(info), fallback=type_type, name=None, variables=variables,
404+
special_sig=special_sig)
397405
c = callable_type.with_name('"{}"'.format(info.name()))
398406
cc = convert_class_tvars_to_func_tvars(c, len(initvars))
399407
cc.is_classmethod_class = True

mypy/constraints.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ def infer_constraints_for_callable(
4545
4646
Return a list of constraints.
4747
"""
48-
4948
constraints = [] # type: List[Constraint]
5049
tuple_counter = [0]
5150

mypy/messages.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
FUNCTION_TYPE_EXPECTED = "Function is missing a type annotation"
8080
RETURN_TYPE_EXPECTED = "Function is missing a return type annotation"
8181
ARGUMENT_TYPE_EXPECTED = "Function is missing a type annotation for one or more arguments"
82+
KEYWORD_ARGUMENT_REQUIRES_STR_KEY_TYPE = \
83+
'Keyword argument only valid with "str" key type in call to "dict"'
8284

8385

8486
class MessageBuilder:

mypy/test/data/check-expressions.test

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,7 +1340,6 @@ def f(x: int) -> Iterator[None]:
13401340
-- ----------------
13411341

13421342

1343-
13441343
[case testYieldFromIteratorHasNoValue]
13451344
from typing import Iterator
13461345
def f() -> Iterator[int]:
@@ -1362,12 +1361,14 @@ def g() -> Iterator[int]:
13621361
[out]
13631362

13641363

1364+
-- dict(...)
1365+
-- ---------
13651366

1366-
-- dict(x=y, ...)
1367-
-- --------------
13681367

1368+
-- Note that the stub used in unit tests does not have all overload
1369+
-- variants, but it should not matter.
13691370

1370-
[case testDictWithKeywordArgs]
1371+
[case testDictWithKeywordArgsOnly]
13711372
from typing import Dict, Any
13721373
d1 = dict(a=1, b=2) # type: Dict[str, int]
13731374
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]
13781379
[builtins fixtures/dict.py]
13791380

13801381
[case testDictWithoutKeywordArgs]
1381-
dict(undefined)
1382+
from typing import Dict
1383+
d = dict() # E: Need type annotation for variable
1384+
d2 = dict() # type: Dict[int, str]
1385+
dict(undefined) # E: Name 'undefined' is not defined
1386+
[builtins fixtures/dict.py]
1387+
1388+
[case testDictFromList]
1389+
from typing import Dict
1390+
d = dict([(1, 'x'), (2, 'y')])
1391+
d() # E: Dict[int, str] not callable
1392+
d2 = dict([(1, 'x')]) # type: Dict[str, str] # E: List item 0 has incompatible type "Tuple[int, str]"
1393+
[builtins fixtures/dict.py]
1394+
1395+
[case testDictFromIterableAndKeywordArg]
1396+
from typing import Dict
1397+
it = [('x', 1)]
1398+
1399+
d = dict(it, x=1)
1400+
d() # E: Dict[str, int] not callable
1401+
1402+
d2 = dict(it, x='') # E: Cannot infer type argument 2 of "dict"
1403+
d2() # E: Dict[Any, Any] not callable
1404+
1405+
d3 = dict(it, x='') # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type "str"; expected "int"
1406+
[builtins fixtures/dict.py]
1407+
1408+
[case testDictFromIterableAndKeywordArg2]
1409+
it = [(1, 'x')]
1410+
dict(it, x='y') # E: Keyword argument only valid with "str" key type in call to "dict"
1411+
[builtins fixtures/dict.py]
1412+
1413+
[case testDictFromIterableAndKeywordArg3]
1414+
d = dict([], x=1)
1415+
d() # E: Dict[str, int] not callable
1416+
[builtins fixtures/dict.py]
1417+
1418+
[case testDictFromIterableAndStarStarArgs]
1419+
from typing import Dict
1420+
it = [('x', 1)]
1421+
1422+
kw = {'x': 1}
1423+
d = dict(it, **kw)
1424+
d() # E: Dict[str, int] not callable
1425+
1426+
kw2 = {'x': ''}
1427+
d2 = dict(it, **kw2) # E: Cannot infer type argument 2 of "dict"
1428+
d2() # E: Dict[Any, Any] not callable
1429+
1430+
d3 = dict(it, **kw2) # type: Dict[str, int] # E: Argument 2 to "dict" has incompatible type **Dict[str, str]; expected "int"
1431+
[builtins fixtures/dict.py]
1432+
1433+
[case testDictFromIterableAndStarStarArgs2]
1434+
it = [(1, 'x')]
1435+
kw = {'x': 'y'}
1436+
d = dict(it, **kw) # E: Keyword argument only valid with "str" key type in call to "dict"
1437+
d() # E: Dict[int, str] not callable
1438+
[builtins fixtures/dict.py]
1439+
1440+
[case testUserDefinedClassNamedDict]
1441+
from typing import Generic, TypeVar
1442+
T = TypeVar('T')
1443+
S = TypeVar('S')
1444+
class dict(Generic[T, S]):
1445+
def __init__(self, x: T, **kwargs: T) -> None: pass
1446+
dict(1, y=1)
13821447
[builtins fixtures/dict.py]
1383-
[out]
1384-
main:1: error: Name 'undefined' is not defined

mypy/test/data/fixtures/dict.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
# Builtins stub used in dictionary-related test cases.
22

3-
from typing import TypeVar, Generic, Iterable, Iterator, Tuple
3+
from typing import TypeVar, Generic, Iterable, Iterator, Tuple, overload
44

55
T = TypeVar('T')
6-
S = TypeVar('S')
6+
KT = TypeVar('KT')
7+
VT = TypeVar('VT')
78

89
class object:
910
def __init__(self) -> None: pass
1011

1112
class type: pass
1213

13-
class dict(Iterable[T], Generic[T, S]):
14-
def __init__(self, arg: Iterable[Tuple[T, S]] = None) -> None: pass
15-
def __setitem__(self, k: T, v: S) -> None: pass
16-
def __iter__(self) -> Iterator[T]: pass
17-
def update(self, a: 'dict[T, S]') -> None: pass
14+
class dict(Iterable[KT], Generic[KT, VT]):
15+
@overload
16+
def __init__(self, **kwargs: VT) -> None: pass
17+
@overload
18+
def __init__(self, arg: Iterable[Tuple[KT, VT]], **kwargs: VT) -> None: pass
19+
def __setitem__(self, k: KT, v: VT) -> None: pass
20+
def __iter__(self) -> Iterator[KT]: pass
21+
def update(self, a: 'dict[KT, VT]') -> None: pass
22+
1823
class int: pass # for convenience
24+
1925
class str: pass # for keyword argument key type
26+
2027
class list(Iterable[T], Generic[T]): # needed by some test cases
2128
def __iter__(self) -> Iterator[T]: pass
2229
def __mul__(self, x: int) -> list[T]: pass

mypy/types.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,9 @@ class CallableType(FunctionLike):
405405
is_classmethod_class = False
406406
# Was this type implicitly generated instead of explicitly specified by the user?
407407
implicit = False
408+
# Defined for signatures that require special handling (currently only value is 'dict'
409+
# for a signature similar to 'dict')
410+
special_sig = None # type: Optional[str]
408411

409412
def __init__(self,
410413
arg_types: List[Type],
@@ -419,6 +422,7 @@ def __init__(self,
419422
is_ellipsis_args: bool = False,
420423
implicit=False,
421424
is_classmethod_class=False,
425+
special_sig=None,
422426
) -> None:
423427
if variables is None:
424428
variables = []
@@ -435,6 +439,7 @@ def __init__(self,
435439
self.variables = variables
436440
self.is_ellipsis_args = is_ellipsis_args
437441
self.implicit = implicit
442+
self.special_sig = special_sig
438443
super().__init__(line)
439444

440445
def copy_modified(self,
@@ -447,7 +452,8 @@ def copy_modified(self,
447452
definition: SymbolNode = _dummy,
448453
variables: List[TypeVarDef] = _dummy,
449454
line: int = _dummy,
450-
is_ellipsis_args: bool = _dummy) -> 'CallableType':
455+
is_ellipsis_args: bool = _dummy,
456+
special_sig: Optional[str] = _dummy) -> 'CallableType':
451457
return CallableType(
452458
arg_types=arg_types if arg_types is not _dummy else self.arg_types,
453459
arg_kinds=arg_kinds if arg_kinds is not _dummy else self.arg_kinds,
@@ -462,6 +468,7 @@ def copy_modified(self,
462468
is_ellipsis_args if is_ellipsis_args is not _dummy else self.is_ellipsis_args),
463469
implicit=self.implicit,
464470
is_classmethod_class=self.is_classmethod_class,
471+
special_sig=special_sig if special_sig is not _dummy else self.special_sig,
465472
)
466473

467474
def is_type_obj(self) -> bool:

0 commit comments

Comments
 (0)
0