From b0ca5c1820a8d3b3cb437428ea58824f16c55736 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 21 Nov 2018 16:07:40 +0000 Subject: [PATCH 01/25] WIP --- mypy/plugin.py | 51 +++++++++++++++++++++ mypy/semanal_typeddict.py | 4 +- test-data/unit/check-typeddict.test | 26 +++++++++++ test-data/unit/lib-stub/mypy_extensions.pyi | 12 ++++- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index cea4c64b456b..5bb457435cd7 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -400,6 +400,8 @@ def get_method_signature_hook(self, fullname: str if fullname == 'typing.Mapping.get': return typed_dict_get_signature_callback + elif fullname == 'mypy_extensions._TypedDict.setdefault': + return typed_dict_setdefault_signature_callback elif fullname == 'ctypes.Array.__setitem__': return ctypes.array_setitem_callback return None @@ -412,6 +414,8 @@ def get_method_hook(self, fullname: str return typed_dict_get_callback elif fullname == 'builtins.int.__pow__': return int_pow_callback + elif fullname == 'mypy_extensions._TypedDict.setdefault': + return typed_dict_setdefault_callback elif fullname == 'ctypes.Array.__getitem__': return ctypes.array_getitem_callback elif fullname == 'ctypes.Array.__iter__': @@ -544,6 +548,53 @@ def typed_dict_get_callback(ctx: MethodContext) -> Type: return ctx.default_return_type +def typed_dict_setdefault_signature_callback(ctx: MethodSigContext) -> CallableType: + """Try to infer a better signature type for TypedDict.setdefault. + + This is used to get better type context for the second argument that + depends on a TypedDict value type. + """ + signature = ctx.default_signature + if (isinstance(ctx.type, TypedDictType) + and len(ctx.args) == 2 + and len(ctx.args[0]) == 1 + and isinstance(ctx.args[0][0], StrExpr) + and len(signature.arg_types) == 2 + and len(ctx.args[1]) == 1): + key = ctx.args[0][0].value + value_type = ctx.type.items.get(key) + ret_type = signature.ret_type + if value_type: + default_arg = ctx.args[1][0] + if (isinstance(value_type, TypedDictType) + and isinstance(default_arg, DictExpr) + and len(default_arg.items) == 0): + # Caller has empty dict {} as default for typed dict. + value_type = value_type.copy_modified(required_keys=set()) + # Tweak the signature to include the value type as context. + print(value_type) + return signature.copy_modified( + arg_types=[signature.arg_types[0], value_type], + ret_type=ret_type) + return signature + + +def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: + """Infer a precise return type for TypedDict.setdefault with literal first argument.""" + if (isinstance(ctx.type, TypedDictType) + and len(ctx.arg_types) == 2 + and len(ctx.arg_types[0]) == 1): + if isinstance(ctx.args[0][0], StrExpr): + key = ctx.args[0][0].value + value_type = ctx.type.items.get(key) + if value_type: + return value_type + else: + ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context) + return AnyType(TypeOfAny.from_error) + return ctx.default_return_type + + def int_pow_callback(ctx: MethodContext) -> Type: """Infer a more precise return type for int.__pow__.""" if (len(ctx.arg_types) == 1 diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index ae377b21bb02..6af1d001a96b 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -276,9 +276,7 @@ def fail_typeddict_arg(self, message: str, def build_typeddict_typeinfo(self, name: str, items: List[str], types: List[Type], required_keys: Set[str]) -> TypeInfo: - fallback = (self.api.named_type_or_none('typing.Mapping', - [self.api.named_type('__builtins__.str'), - self.api.named_type('__builtins__.object')]) + fallback = (self.api.named_type_or_none('mypy_extensions._TypedDict', []) or self.api.named_type('__builtins__.object')) info = self.api.basic_new_typeinfo(name, fallback) info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), required_keys, diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index c9f935eeb5bd..567117871794 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1481,3 +1481,29 @@ f1(**a, **c) # E: "f1" gets multiple values for keyword argument "x" \ # E: Argument "x" to "f1" has incompatible type "str"; expected "int" f1(**c, **a) # E: "f1" gets multiple values for keyword argument "x" \ # E: Argument "x" to "f1" has incompatible type "str"; expected "int" + +[case testTypedDictNonMappingMethods] +from typing import List +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': List[int]}) +a: A + +reveal_type(a.copy()) # E: Revealed type is 'TypedDict('__main__.A', {'x': builtins.int, 'y': builtins.list[builtins.int]})' +a.has_key() # E: "A" has no attribute "has_key" +a.clear() # E: "A" has no attribute "clear" + +a.setdefault('invalid', 1) # E: TypedDict "A" has no key 'invalid' +reveal_type(a.setdefault('x', 1)) # E: Revealed type is 'builtins.int' +reveal_type(a.setdefault('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' +a.setdefault('y', '') # E: Argument 2 to "setdefault" of "_TypedDict" has incompatible type "str"; expected "List[int]" +[builtins fixtures/dict.pyi] + +[case testTypedDictNonMappingMethods_python2] +from mypy_extensions import TypedDict +A = TypedDict('A', {'x': int}) +a = A(x=1) +reveal_type(a.copy()) # E: Revealed type is 'TypedDict('__main__.A', {'x': builtins.int})' +reveal_type(a.has_key()) # E: Revealed type is 'builtins.bool' +a.clear() # E: "A" has no attribute "clear" +[builtins_py2 fixtures/dict.pyi] diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 791ff9b2d7ea..4a021334953b 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -1,5 +1,6 @@ # NOTE: Requires fixtures/dict.pyi -from typing import Dict, Type, TypeVar, Optional, Any, Generic +from typing import Dict, Type, TypeVar, Optional, Any, Generic, Mapping +import sys _T = TypeVar('_T') _U = TypeVar('_U') @@ -18,6 +19,15 @@ def VarArg(type: _T = ...) -> _T: ... def KwArg(type: _T = ...) -> _T: ... +# Fallback type for all typed dicts (does not exist at runtime) +class _TypedDict(Mapping[str, object]): + def copy(self: _T) -> _T: ... + def setdefault(self, k: str, default: object) -> object: ... + def pop(self, k: str, default: object = ...) -> object: ... + def update(self, __m: Mapping[str, object], **kwargs: object) -> None: ... + if sys.version_info[0] == 2: + def has_key(self) -> bool: ... + def TypedDict(typename: str, fields: Dict[str, Type[_T]], *, total: Any = ...) -> Type[dict]: ... # This is intended as a class decorator, but mypy rejects abstract classes From 8dcbfcf57337e52715c5f3d2a1a1850e105615f9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 21 Nov 2018 16:56:39 +0000 Subject: [PATCH 02/25] More WIP --- test-data/unit/check-typeddict.test | 13 +++++++++++++ test-data/unit/lib-stub/mypy_extensions.pyi | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 567117871794..bd801cc89263 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1507,3 +1507,16 @@ reveal_type(a.copy()) # E: Revealed type is 'TypedDict('__main__.A', {'x': built reveal_type(a.has_key()) # E: Revealed type is 'builtins.bool' a.clear() # E: "A" has no attribute "clear" [builtins_py2 fixtures/dict.pyi] + +[case testTypedDictNonMappingMethodsXXX] +from typing import List +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': List[int]}, total=False) +a: A + +reveal_type(a.pop('x')) # E: Revealed type is 'builtins.int' +reveal_type(a.pop('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' +a.pop('x', '') # E: Argument 2 to "pop" of "_TypedDict" has incompatible type "str"; expected "int" +a.pop('invalid', '') # E: x +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 4a021334953b..641fa03c0f15 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -23,7 +23,7 @@ def KwArg(type: _T = ...) -> _T: ... class _TypedDict(Mapping[str, object]): def copy(self: _T) -> _T: ... def setdefault(self, k: str, default: object) -> object: ... - def pop(self, k: str, default: object = ...) -> object: ... + def pop(self, k: str, default: _T = ...) -> object: ... def update(self, __m: Mapping[str, object], **kwargs: object) -> None: ... if sys.version_info[0] == 2: def has_key(self) -> bool: ... From 3df440165177837c50f26606ef139fd1208cebfe Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 21 Nov 2018 17:22:40 +0000 Subject: [PATCH 03/25] More WIP --- mypy/messages.py | 12 ++++++ mypy/plugin.py | 62 +++++++++++++++++++++++++++++ test-data/unit/check-typeddict.test | 9 +++-- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 43934d39756a..9c0d64b79c8e 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1185,6 +1185,18 @@ def typeddict_key_not_found( else: self.fail("TypedDict {} has no key '{}'".format(self.format(typ), item_name), context) + def typeddict_key_cannot_be_deleted( + self, + typ: TypedDictType, + item_name: str, + context: Context) -> None: + if typ.is_anonymous(): + self.fail("TypedDict key '{}' cannot be deleted".format(item_name), + context) + else: + self.fail("Key '{}' of TypedDict {} cannot be deleted".format( + item_name, self.format(typ)), context) + def type_arguments_not_allowed(self, context: Context) -> None: self.fail('Parameterized generics cannot be used with class or instance checks', context) diff --git a/mypy/plugin.py b/mypy/plugin.py index 5bb457435cd7..14bc021edb34 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -402,6 +402,8 @@ def get_method_signature_hook(self, fullname: str return typed_dict_get_signature_callback elif fullname == 'mypy_extensions._TypedDict.setdefault': return typed_dict_setdefault_signature_callback + elif fullname == 'mypy_extensions._TypedDict.pop': + return typed_dict_pop_signature_callback elif fullname == 'ctypes.Array.__setitem__': return ctypes.array_setitem_callback return None @@ -416,6 +418,8 @@ def get_method_hook(self, fullname: str return int_pow_callback elif fullname == 'mypy_extensions._TypedDict.setdefault': return typed_dict_setdefault_callback + elif fullname == 'mypy_extensions._TypedDict.pop': + return typed_dict_pop_callback elif fullname == 'ctypes.Array.__getitem__': return ctypes.array_getitem_callback elif fullname == 'ctypes.Array.__iter__': @@ -548,6 +552,64 @@ def typed_dict_get_callback(ctx: MethodContext) -> Type: return ctx.default_return_type +def typed_dict_pop_signature_callback(ctx: MethodSigContext) -> CallableType: + """Try to infer a better signature type for TypedDict.pop. + + This is used to get better type context for the second argument that + depends on a TypedDict value type. + """ + signature = ctx.default_signature + if (isinstance(ctx.type, TypedDictType) + and len(ctx.args) == 2 + and len(ctx.args[0]) == 1 + and isinstance(ctx.args[0][0], StrExpr) + and len(signature.arg_types) == 2 + and len(signature.variables) == 1 + and len(ctx.args[1]) == 1): + key = ctx.args[0][0].value + value_type = ctx.type.items.get(key) + if value_type: + default_arg = ctx.args[1][0] + if (isinstance(value_type, TypedDictType) + and isinstance(default_arg, DictExpr) + and len(default_arg.items) == 0): + # Caller has empty dict {} as default for typed dict. + value_type = value_type.copy_modified(required_keys=set()) + # Tweak the signature to include the value type as context. It's + # only needed for type inference since there's a union with a type + # variable that accepts everything. + tv = TypeVarType(signature.variables[0]) + ret_type = UnionType.make_simplified_union([value_type, tv]) + return signature.copy_modified( + arg_types=[signature.arg_types[0], + UnionType.make_simplified_union([value_type, tv])], + ret_type=ret_type) + return signature + + +def typed_dict_pop_callback(ctx: MethodContext) -> Type: + """Infer a precise return type for TypedDict.get with literal first argument.""" + if (isinstance(ctx.type, TypedDictType) + and len(ctx.arg_types) >= 1 + and len(ctx.arg_types[0]) == 1): + if isinstance(ctx.args[0][0], StrExpr): + key = ctx.args[0][0].value + if key in ctx.type.required_keys: + ctx.api.msg.typeddict_key_cannot_be_deleted(ctx.type, key, ctx.context) + value_type = ctx.type.items.get(key) + if value_type: + if len(ctx.args[1]) == 0: + return value_type + elif (len(ctx.arg_types) == 2 and len(ctx.arg_types[1]) == 1 + and len(ctx.args[1]) == 1): + default_arg = ctx.args[1][0] + return UnionType.make_simplified_union([value_type, ctx.arg_types[1][0]]) + else: + ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context) + return AnyType(TypeOfAny.from_error) + return ctx.default_return_type + + def typed_dict_setdefault_signature_callback(ctx: MethodSigContext) -> CallableType: """Try to infer a better signature type for TypedDict.setdefault. diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index bd801cc89263..f6dc6e095b4b 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1508,15 +1508,18 @@ reveal_type(a.has_key()) # E: Revealed type is 'builtins.bool' a.clear() # E: "A" has no attribute "clear" [builtins_py2 fixtures/dict.pyi] -[case testTypedDictNonMappingMethodsXXX] +[case testTypedDictPopMethod] from typing import List from mypy_extensions import TypedDict A = TypedDict('A', {'x': int, 'y': List[int]}, total=False) +B = TypedDict('B', {'x': int}) a: A +b: B reveal_type(a.pop('x')) # E: Revealed type is 'builtins.int' reveal_type(a.pop('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' -a.pop('x', '') # E: Argument 2 to "pop" of "_TypedDict" has incompatible type "str"; expected "int" -a.pop('invalid', '') # E: x +reveal_type(a.pop('x', '')) # E: Revealed type is 'Union[builtins.int, builtins.str]' +a.pop('invalid', '') # E: TypedDict "A" has no key 'invalid' +b.pop('x') # E: Key 'x' of TypedDict "B" cannot be deleted [builtins fixtures/dict.pyi] From de0de967f0e33c828eb0721ed15a22f5135fc960 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 21 Nov 2018 17:44:20 +0000 Subject: [PATCH 04/25] Support del x['key'] with TypedDict --- mypy/plugin.py | 16 ++++++++++++++++ test-data/unit/check-typeddict.test | 14 ++++++++++++++ test-data/unit/lib-stub/mypy_extensions.pyi | 1 + 3 files changed, 31 insertions(+) diff --git a/mypy/plugin.py b/mypy/plugin.py index 14bc021edb34..84d34ace081c 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -420,6 +420,8 @@ def get_method_hook(self, fullname: str return typed_dict_setdefault_callback elif fullname == 'mypy_extensions._TypedDict.pop': return typed_dict_pop_callback + elif fullname == 'mypy_extensions._TypedDict.__delitem__': + return typed_dict_del_callback elif fullname == 'ctypes.Array.__getitem__': return ctypes.array_getitem_callback elif fullname == 'ctypes.Array.__iter__': @@ -657,6 +659,20 @@ def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: return ctx.default_return_type +def typed_dict_del_callback(ctx: MethodContext) -> Type: + """Type check .__del__.""" + if (isinstance(ctx.type, TypedDictType) + and len(ctx.arg_types) == 1 + and len(ctx.arg_types[0]) == 1): + if isinstance(ctx.args[0][0], StrExpr): + key = ctx.args[0][0].value + if key in ctx.type.required_keys: + ctx.api.msg.typeddict_key_cannot_be_deleted(ctx.type, key, ctx.context) + elif key not in ctx.type.items: + ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context) + return ctx.default_return_type + + def int_pow_callback(ctx: MethodContext) -> Type: """Infer a more precise return type for int.__pow__.""" if (len(ctx.arg_types) == 1 diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index f6dc6e095b4b..ac72bcae5e38 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1523,3 +1523,17 @@ reveal_type(a.pop('x', '')) # E: Revealed type is 'Union[builtins.int, builtins. a.pop('invalid', '') # E: TypedDict "A" has no key 'invalid' b.pop('x') # E: Key 'x' of TypedDict "B" cannot be deleted [builtins fixtures/dict.pyi] + +[case testTypedDictDel] +from typing import List +from mypy_extensions import TypedDict + +A = TypedDict('A', {'x': int, 'y': List[int]}, total=False) +B = TypedDict('B', {'x': int}) +a: A +b: B + +del a['x'] +del a['invalid'] # E: TypedDict "A" has no key 'invalid' +del b['x'] # E: Key 'x' of TypedDict "B" cannot be deleted +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 641fa03c0f15..d504e3bccea4 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -27,6 +27,7 @@ class _TypedDict(Mapping[str, object]): def update(self, __m: Mapping[str, object], **kwargs: object) -> None: ... if sys.version_info[0] == 2: def has_key(self) -> bool: ... + def __delitem__(self, k: str) -> None: ... def TypedDict(typename: str, fields: Dict[str, Type[_T]], *, total: Any = ...) -> Type[dict]: ... From 4e655ccb363c495dbed55e4146ed7f500ad277d7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 11:58:16 +0000 Subject: [PATCH 05/25] Remove **kwargs from stub due to stub issues --- test-data/unit/lib-stub/mypy_extensions.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index d504e3bccea4..2d0eb64b7e36 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -24,7 +24,7 @@ class _TypedDict(Mapping[str, object]): def copy(self: _T) -> _T: ... def setdefault(self, k: str, default: object) -> object: ... def pop(self, k: str, default: _T = ...) -> object: ... - def update(self, __m: Mapping[str, object], **kwargs: object) -> None: ... + def update(self, __m: Mapping[str, object]) -> None: ... if sys.version_info[0] == 2: def has_key(self) -> bool: ... def __delitem__(self, k: str) -> None: ... From fabbc6baed68ded46d317675c684d7ea7068f353 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 13:29:22 +0000 Subject: [PATCH 06/25] Update docstring --- mypy/types.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index 201c40b30045..cc33047c863d 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1172,12 +1172,20 @@ def slice(self, begin: Optional[int], stride: Optional[int], class TypedDictType(Type): - """The type of a TypedDict instance. TypedDict(K1=VT1, ..., Kn=VTn) + """Type of TypedDict object {'k1': v1, ..., 'kn': vn}. - A TypedDictType can be either named or anonymous. - If it is anonymous then its fallback will be an Instance of Mapping[str, V]. - If it is named then its fallback will be an Instance of the named type (ex: "Point") - whose TypeInfo has a typeddict_type that is anonymous. + A TypedDict object is a dictionary with specific string (literal) keys. Each + key has a value with a distinct type that depends on the key. TypedDict objects + are normal dict objects at runtime. + + A TypedDictType can be either named or anonymous. If it's anonymous, its + fallback will mypy_extensions._TypedDict (Instance). _TypedDict is a subclass + of Mapping[str, object] and defines all non-mapping dict methods that TypedDict + supports. Some dict methods are unsafe and not supported. _TypedDict isn't defined + at runtime. + + If a TypedDict is named, its fallback will be an Instance of the named type + (ex: "Point") whose TypeInfo has a typeddict_type that is anonymous. """ items = None # type: OrderedDict[str, Type] # item_name -> item_type From 59395de81aca12dad0b3465013d2698d247f5eef Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 13:30:23 +0000 Subject: [PATCH 07/25] Filter out some TypedDict fine-grained dependencies Update related tests. --- mypy/server/deps.py | 4 ++-- test-data/unit/deps-types.test | 4 ++-- test-data/unit/deps.test | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index e0580ae3ef83..944cb8bf8d69 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -790,8 +790,8 @@ def add_dependency(self, trigger: str, target: Optional[str] = None) -> None: If the target is not given explicitly, use the current target. """ - if trigger.startswith((' -> m -> m -> , m - -> m [case testAliasDepsTypedDictFunctional] # __dump_all__ @@ -872,7 +871,8 @@ class I: pass -> a -> a -> , a, mod.I - -> m + -> sys + -> sys [case testAliasDepsClassInFunction] from mod import I diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 5021da063e0a..2a377e85ab79 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -624,7 +624,6 @@ def foo(x: Point) -> int: [out] -> , , m, m.foo -> m - -> m [case testTypedDict2] from mypy_extensions import TypedDict @@ -640,7 +639,6 @@ def foo(x: Point) -> int: -> , , , m, m.A, m.foo -> , , m, m.foo -> m - -> m [case testTypedDict3] from mypy_extensions import TypedDict @@ -658,7 +656,6 @@ def foo(x: Point) -> int: -> , , , m, m.A, m.foo -> , , m, m.Point, m.foo -> m - -> m [case testImportStar] from a import * From 50fc0db28853553bfb69922a75686138823b05d7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 13:30:49 +0000 Subject: [PATCH 08/25] Fix anonymous TypedDict types --- mypy/types.py | 14 ++++---------- test-data/unit/check-typeddict.test | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index cc33047c863d..ea820f22d4f4 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1235,7 +1235,7 @@ def deserialize(cls, data: JsonDict) -> 'TypedDictType': Instance.deserialize(data['fallback'])) def is_anonymous(self) -> bool: - return self.fallback.type.fullname() == 'typing.Mapping' + return self.fallback.type.fullname() == 'mypy_extensions._TypedDict' def as_anonymous(self) -> 'TypedDictType': if self.is_anonymous(): @@ -1258,10 +1258,7 @@ def copy_modified(self, *, fallback: Optional[Instance] = None, def create_anonymous_fallback(self, *, value_type: Type) -> Instance: anonymous = self.as_anonymous() - return anonymous.fallback.copy_modified(args=[ # i.e. Mapping - anonymous.fallback.args[0], # i.e. str - value_type - ]) + return anonymous.fallback def names_are_wider_than(self, other: 'TypedDictType') -> bool: return len(other.items.keys() - self.items.keys()) == 0 @@ -1830,13 +1827,10 @@ def item_str(name: str, typ: str) -> str: s = '{' + ', '.join(item_str(name, typ.accept(self)) for name, typ in t.items.items()) + '}' prefix = '' - suffix = '' if t.fallback and t.fallback.type: - if t.fallback.type.fullname() != 'typing.Mapping': + if t.fallback.type.fullname() != 'mypy_extensions._TypedDict': prefix = repr(t.fallback.type.fullname()) + ', ' - else: - suffix = ', fallback={}'.format(t.fallback.accept(self)) - return 'TypedDict({}{}{})'.format(prefix, s, suffix) + return 'TypedDict({}{})'.format(prefix, s) def visit_raw_literal_type(self, t: RawLiteralType) -> str: return repr(t.value) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index ac72bcae5e38..b60554187bcb 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -435,7 +435,7 @@ p2 = Point3D(x=1, y=1, z=1) joined_points = [p1, p2][0] reveal_type(p1.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]' reveal_type(p2.values()) # E: Revealed type is 'typing.Iterable[builtins.object*]' -reveal_type(joined_points) # E: Revealed type is 'TypedDict({'x': builtins.int, 'y': builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(joined_points) # E: Revealed type is 'TypedDict({'x': builtins.int, 'y': builtins.int})' [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] @@ -448,7 +448,7 @@ c2 = CellWithObject(value=2, meta='turtle doves') joined_cells = [c1, c2] reveal_type(c1) # E: Revealed type is 'TypedDict('__main__.CellWithInt', {'value': builtins.object, 'meta': builtins.int})' reveal_type(c2) # E: Revealed type is 'TypedDict('__main__.CellWithObject', {'value': builtins.object, 'meta': builtins.object})' -reveal_type(joined_cells) # E: Revealed type is 'builtins.list[TypedDict({'value': builtins.object}, fallback=typing.Mapping[builtins.str, builtins.object])]' +reveal_type(joined_cells) # E: Revealed type is 'builtins.list[TypedDict({'value': builtins.object})]' [builtins fixtures/dict.pyi] [case testJoinOfDisjointTypedDictsIsEmptyTypedDict] @@ -460,7 +460,7 @@ d2 = Cell(value='pear tree') joined_dicts = [d1, d2] reveal_type(d1) # E: Revealed type is 'TypedDict('__main__.Point', {'x': builtins.int, 'y': builtins.int})' reveal_type(d2) # E: Revealed type is 'TypedDict('__main__.Cell', {'value': builtins.object})' -reveal_type(joined_dicts) # E: Revealed type is 'builtins.list[TypedDict({}, fallback=typing.Mapping[builtins.str, ])]' +reveal_type(joined_dicts) # E: Revealed type is 'builtins.list[TypedDict({})]' [builtins fixtures/dict.pyi] [case testJoinOfTypedDictWithCompatibleMappingIsMapping] @@ -511,7 +511,7 @@ YZ = TypedDict('YZ', {'y': int, 'z': int}) T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass def g(x: XY, y: YZ) -> None: pass -reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x': builtins.int, 'y': builtins.int, 'z': builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x': builtins.int, 'y': builtins.int, 'z': builtins.int})' [builtins fixtures/dict.pyi] [case testMeetOfTypedDictsWithIncompatibleCommonKeysIsUninhabited] @@ -534,7 +534,7 @@ Z = TypedDict('Z', {'z': int}) T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass def g(x: X, y: Z) -> None: pass -reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x': builtins.int, 'z': builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x': builtins.int, 'z': builtins.int})' [builtins fixtures/dict.pyi] # TODO: It would be more accurate for the meet to be TypedDict instead. @@ -583,7 +583,7 @@ YZ = TypedDict('YZ', {'y': int, 'z': int}, total=False) T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass def g(x: XY, y: YZ) -> None: pass -reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x'?: builtins.int, 'y'?: builtins.int, 'z'?: builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x'?: builtins.int, 'y'?: builtins.int, 'z'?: builtins.int})' [builtins fixtures/dict.pyi] [case testMeetOfTypedDictsWithNonTotalAndTotal] @@ -594,7 +594,7 @@ YZ = TypedDict('YZ', {'y': int, 'z': int}) T = TypeVar('T') def f(x: Callable[[T, T], None]) -> T: pass def g(x: XY, y: YZ) -> None: pass -reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x'?: builtins.int, 'y': builtins.int, 'z': builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(f(g)) # E: Revealed type is 'TypedDict({'x'?: builtins.int, 'y': builtins.int, 'z': builtins.int})' [builtins fixtures/dict.pyi] [case testMeetOfTypedDictsWithIncompatibleNonTotalAndTotal] @@ -1037,15 +1037,15 @@ a: A b: B c: C reveal_type(j(a, b)) \ - # E: Revealed type is 'TypedDict({}, fallback=typing.Mapping[builtins.str, ])' + # E: Revealed type is 'TypedDict({})' reveal_type(j(b, b)) \ - # E: Revealed type is 'TypedDict({'x'?: builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' + # E: Revealed type is 'TypedDict({'x'?: builtins.int})' reveal_type(j(c, c)) \ - # E: Revealed type is 'TypedDict({'x'?: builtins.int, 'y'?: builtins.str}, fallback=typing.Mapping[builtins.str, builtins.object])' + # E: Revealed type is 'TypedDict({'x'?: builtins.int, 'y'?: builtins.str})' reveal_type(j(b, c)) \ - # E: Revealed type is 'TypedDict({'x'?: builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' + # E: Revealed type is 'TypedDict({'x'?: builtins.int})' reveal_type(j(c, b)) \ - # E: Revealed type is 'TypedDict({'x'?: builtins.int}, fallback=typing.Mapping[builtins.str, builtins.int])' + # E: Revealed type is 'TypedDict({'x'?: builtins.int})' [builtins fixtures/dict.pyi] [case testTypedDictClassWithTotalArgument] From afd0f1249443adee8bc7d2903b206eb0326a3e79 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 15:43:15 +0000 Subject: [PATCH 09/25] Fix binder test case --- mypy/checker.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f2af5d7a7a7e..1b1aa7934ee1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3830,11 +3830,12 @@ def builtin_item_type(tp: Type) -> Optional[Type]: elif isinstance(tp, TupleType) and all(not isinstance(it, AnyType) for it in tp.items): return UnionType.make_simplified_union(tp.items) # this type is not externally visible elif isinstance(tp, TypedDictType): - # TypedDict always has non-optional string keys. - if tp.fallback.type.fullname() == 'typing.Mapping': - return tp.fallback.args[0] - elif tp.fallback.type.bases[0].type.fullname() == 'typing.Mapping': - return tp.fallback.type.bases[0].args[0] + # TypedDict always has non-optional string keys. Find the key type from the Mapping + # base class. + for base in tp.fallback.type.mro: + if base.fullname() == 'typing.Mapping': + return map_instance_to_supertype(tp.fallback, base).args[0] + assert False, 'No Mapping base class found for TypedDict fallback' return None From fbf9a3b3ec2dce65a18ef0ae90b9c690206705cc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 15:51:59 +0000 Subject: [PATCH 10/25] Fix lint --- mypy/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 84d34ace081c..c62407187205 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -604,7 +604,6 @@ def typed_dict_pop_callback(ctx: MethodContext) -> Type: return value_type elif (len(ctx.arg_types) == 2 and len(ctx.arg_types[1]) == 1 and len(ctx.args[1]) == 1): - default_arg = ctx.args[1][0] return UnionType.make_simplified_union([value_type, ctx.arg_types[1][0]]) else: ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context) From 820b19e6df3314795ee7df4ac9a710647bef44dc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 15:52:05 +0000 Subject: [PATCH 11/25] Fix semantic analyzer tests --- mypy/test/testsemanal.py | 3 ++- test-data/unit/semanal-typeddict.test | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/test/testsemanal.py b/mypy/test/testsemanal.py index e86f645ed87e..096bdd4dacb2 100644 --- a/mypy/test/testsemanal.py +++ b/mypy/test/testsemanal.py @@ -81,7 +81,8 @@ def test_semanal(testcase: DataDrivenTestCase) -> None: 'mypy_extensions.pyi', 'typing_extensions.pyi', 'abc.pyi', - 'collections.pyi')) + 'collections.pyi', + 'sys.pyi')) and not os.path.basename(f.path).startswith('_') and not os.path.splitext( os.path.basename(f.path))[0].endswith('_')): diff --git a/test-data/unit/semanal-typeddict.test b/test-data/unit/semanal-typeddict.test index cff51a6cc983..dad2f74910e4 100644 --- a/test-data/unit/semanal-typeddict.test +++ b/test-data/unit/semanal-typeddict.test @@ -47,7 +47,7 @@ MypyFile:1( ClassDef:2( A BaseType( - typing.Mapping[builtins.str, builtins.object]) + mypy_extensions._TypedDict) ExpressionStmt:3( StrExpr(foo)) AssignmentStmt:4( From bc6836052f48668a6d5ac45f4eba1b371ec67551 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 16:05:26 +0000 Subject: [PATCH 12/25] Add minimal support for TypedDict.update(x) --- mypy/plugin.py | 14 ++++++++++++++ test-data/unit/check-typeddict.test | 8 ++++++++ test-data/unit/lib-stub/mypy_extensions.pyi | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index c62407187205..b26fc79b02f7 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -404,6 +404,8 @@ def get_method_signature_hook(self, fullname: str return typed_dict_setdefault_signature_callback elif fullname == 'mypy_extensions._TypedDict.pop': return typed_dict_pop_signature_callback + elif fullname == 'mypy_extensions._TypedDict.update': + return typed_dict_update_signature_callback elif fullname == 'ctypes.Array.__setitem__': return ctypes.array_setitem_callback return None @@ -672,6 +674,18 @@ def typed_dict_del_callback(ctx: MethodContext) -> Type: return ctx.default_return_type +def typed_dict_update_signature_callback(ctx: MethodSigContext) -> CallableType: + """Try to infer a better signature type for TypedDict.update.""" + signature = ctx.default_signature + if (isinstance(ctx.type, TypedDictType) + and len(signature.arg_types) == 1): + arg_type = signature.arg_types[0] + assert isinstance(arg_type, TypedDictType) + arg_type = arg_type.copy_modified(required_keys=set()) + return signature.copy_modified(arg_types=[arg_type]) + return signature + + def int_pow_callback(ctx: MethodContext) -> Type: """Infer a more precise return type for int.__pow__.""" if (len(ctx.arg_types) == 1 diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index b60554187bcb..7f3c9d430ca5 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1497,6 +1497,14 @@ a.setdefault('invalid', 1) # E: TypedDict "A" has no key 'invalid' reveal_type(a.setdefault('x', 1)) # E: Revealed type is 'builtins.int' reveal_type(a.setdefault('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' a.setdefault('y', '') # E: Argument 2 to "setdefault" of "_TypedDict" has incompatible type "str"; expected "List[int]" + +a.update({}) +a.update({'x': 1}) +a.update({'x': 1, 'y': []}) +a.update({'x': 1, 'y': [1]}) +a.update({'z': 1}) # E: Extra key 'z' for TypedDict "A" +d = {'x': 1} +a.update(d) # E: Argument 1 to "update" of "_TypedDict" has incompatible type "Dict[str, int]"; expected "A" [builtins fixtures/dict.pyi] [case testTypedDictNonMappingMethods_python2] diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 2d0eb64b7e36..46f8ce423d69 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -24,7 +24,7 @@ class _TypedDict(Mapping[str, object]): def copy(self: _T) -> _T: ... def setdefault(self, k: str, default: object) -> object: ... def pop(self, k: str, default: _T = ...) -> object: ... - def update(self, __m: Mapping[str, object]) -> None: ... + def update(self: _T, __m: _T) -> None: ... if sys.version_info[0] == 2: def has_key(self) -> bool: ... def __delitem__(self, k: str) -> None: ... From 91321780b434345987f52e79beeb29acb09c3475 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 16:24:18 +0000 Subject: [PATCH 13/25] Remove references to _TypedDict from error messages --- mypy/semanal_shared.py | 7 ++++++- test-data/unit/check-typeddict.test | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index 359c1a3a0188..c88c9ff98195 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -144,8 +144,13 @@ def create_indirect_imported_name(file_node: MypyFile, def set_callable_name(sig: Type, fdef: FuncDef) -> Type: if isinstance(sig, FunctionLike): if fdef.info: + if fdef.info.fullname() == 'mypy_extensions._TypedDict': + # Avoid exposing the internal _TypedDict name. + class_name = 'TypedDict' + else: + class_name = fdef.info.name() return sig.with_name( - '{} of {}'.format(fdef.name(), fdef.info.name())) + '{} of {}'.format(fdef.name(), class_name)) else: return sig.with_name(fdef.name()) else: diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 7f3c9d430ca5..b8434fa97c90 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1496,7 +1496,7 @@ a.clear() # E: "A" has no attribute "clear" a.setdefault('invalid', 1) # E: TypedDict "A" has no key 'invalid' reveal_type(a.setdefault('x', 1)) # E: Revealed type is 'builtins.int' reveal_type(a.setdefault('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' -a.setdefault('y', '') # E: Argument 2 to "setdefault" of "_TypedDict" has incompatible type "str"; expected "List[int]" +a.setdefault('y', '') # E: Argument 2 to "setdefault" of "TypedDict" has incompatible type "str"; expected "List[int]" a.update({}) a.update({'x': 1}) @@ -1504,7 +1504,7 @@ a.update({'x': 1, 'y': []}) a.update({'x': 1, 'y': [1]}) a.update({'z': 1}) # E: Extra key 'z' for TypedDict "A" d = {'x': 1} -a.update(d) # E: Argument 1 to "update" of "_TypedDict" has incompatible type "Dict[str, int]"; expected "A" +a.update(d) # E: Argument 1 to "update" of "TypedDict" has incompatible type "Dict[str, int]"; expected "A" [builtins fixtures/dict.pyi] [case testTypedDictNonMappingMethods_python2] From 5626a8d8b17bac432c20b9d17b74c8fd6fe590a7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 16:55:29 +0000 Subject: [PATCH 14/25] Reject some unsafe method calls --- mypy/plugin.py | 7 +++++++ test-data/unit/check-typeddict.test | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/mypy/plugin.py b/mypy/plugin.py index b26fc79b02f7..be9791aff14a 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -16,6 +16,7 @@ Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, AnyType, TypeList, UnboundType, TypeOfAny, TypeType, ) +from mypy import messages from mypy.messages import MessageBuilder from mypy.options import Options import mypy.interpreted_plugin @@ -610,6 +611,9 @@ def typed_dict_pop_callback(ctx: MethodContext) -> Type: else: ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context) return AnyType(TypeOfAny.from_error) + else: + ctx.api.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, ctx.context) + return AnyType(TypeOfAny.from_error) return ctx.default_return_type @@ -657,6 +661,9 @@ def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: else: ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context) return AnyType(TypeOfAny.from_error) + else: + ctx.api.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, ctx.context) + return AnyType(TypeOfAny.from_error) return ctx.default_return_type diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index b8434fa97c90..61153a1495b0 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1497,6 +1497,8 @@ a.setdefault('invalid', 1) # E: TypedDict "A" has no key 'invalid' reveal_type(a.setdefault('x', 1)) # E: Revealed type is 'builtins.int' reveal_type(a.setdefault('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' a.setdefault('y', '') # E: Argument 2 to "setdefault" of "TypedDict" has incompatible type "str"; expected "List[int]" +x = '' +a.setdefault(x, 1) # E: Expected TypedDict key to be string literal a.update({}) a.update({'x': 1}) @@ -1530,6 +1532,8 @@ reveal_type(a.pop('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' reveal_type(a.pop('x', '')) # E: Revealed type is 'Union[builtins.int, builtins.str]' a.pop('invalid', '') # E: TypedDict "A" has no key 'invalid' b.pop('x') # E: Key 'x' of TypedDict "B" cannot be deleted +x = '' +b.pop(x) # E: Expected TypedDict key to be string literal [builtins fixtures/dict.pyi] [case testTypedDictDel] From 25c3e6dbbd799168b376091c9333c9cb6501d372 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 17:07:17 +0000 Subject: [PATCH 15/25] Remove debug print --- mypy/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index be9791aff14a..91d8f4d5be65 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -641,7 +641,6 @@ def typed_dict_setdefault_signature_callback(ctx: MethodSigContext) -> CallableT # Caller has empty dict {} as default for typed dict. value_type = value_type.copy_modified(required_keys=set()) # Tweak the signature to include the value type as context. - print(value_type) return signature.copy_modified( arg_types=[signature.arg_types[0], value_type], ret_type=ret_type) From 8dd642c6383e72dd6c0f1315075620a69b659c9a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 17:07:30 +0000 Subject: [PATCH 16/25] Improve some error messages --- mypy/messages.py | 8 ++++---- mypy/plugin.py | 1 + test-data/unit/check-typeddict.test | 7 +++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 9c0d64b79c8e..6554d2dc163d 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1157,11 +1157,11 @@ def unexpected_typeddict_keys( format_key_list(extra, short=True), self.format(typ)), context) return - if not expected_keys: - expected = '(no keys)' - else: - expected = format_key_list(expected_keys) found = format_key_list(actual_keys, short=True) + if not expected_keys: + self.fail('Unexpected TypedDict {}'.format(found), context) + return + expected = format_key_list(expected_keys) if actual_keys and actual_set < expected_set: found = 'only {}'.format(found) self.fail('Expected {} but found {}'.format(expected, found), context) diff --git a/mypy/plugin.py b/mypy/plugin.py index 91d8f4d5be65..378ffd48dc31 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -687,6 +687,7 @@ def typed_dict_update_signature_callback(ctx: MethodSigContext) -> CallableType: and len(signature.arg_types) == 1): arg_type = signature.arg_types[0] assert isinstance(arg_type, TypedDictType) + arg_type = arg_type.as_anonymous() arg_type = arg_type.copy_modified(required_keys=set()) return signature.copy_modified(arg_types=[arg_type]) return signature diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 61153a1495b0..97758be1d463 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1502,11 +1502,14 @@ a.setdefault(x, 1) # E: Expected TypedDict key to be string literal a.update({}) a.update({'x': 1}) +a.update({'x': ''}) # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int") a.update({'x': 1, 'y': []}) a.update({'x': 1, 'y': [1]}) -a.update({'z': 1}) # E: Extra key 'z' for TypedDict "A" +a.update({'z': 1}) # E: Unexpected TypedDict key 'z' +a.update({'z': 1, 'zz': 1}) # E: Unexpected TypedDict keys ('z', 'zz') +a.update({'z': 1, 'x': 1}) # E: Expected TypedDict key 'x' but found keys ('z', 'x') d = {'x': 1} -a.update(d) # E: Argument 1 to "update" of "TypedDict" has incompatible type "Dict[str, int]"; expected "A" +a.update(d) # E: Argument 1 to "update" of "TypedDict" has incompatible type "Dict[str, int]"; expected "TypedDict({'x'?: int, 'y'?: List[int]})" [builtins fixtures/dict.pyi] [case testTypedDictNonMappingMethods_python2] From a793d6f3c30cdbaee2f3e350df6404c99044f791 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 22 Nov 2018 17:15:41 +0000 Subject: [PATCH 17/25] Catch more non-literal keys --- mypy/plugin.py | 3 +++ test-data/unit/check-typeddict.test | 3 +++ 2 files changed, 6 insertions(+) diff --git a/mypy/plugin.py b/mypy/plugin.py index 378ffd48dc31..9d9e36169d64 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -677,6 +677,9 @@ def typed_dict_del_callback(ctx: MethodContext) -> Type: ctx.api.msg.typeddict_key_cannot_be_deleted(ctx.type, key, ctx.context) elif key not in ctx.type.items: ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context) + else: + ctx.api.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, ctx.context) + return AnyType(TypeOfAny.from_error) return ctx.default_return_type diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 97758be1d463..f62937081da5 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1551,4 +1551,7 @@ b: B del a['x'] del a['invalid'] # E: TypedDict "A" has no key 'invalid' del b['x'] # E: Key 'x' of TypedDict "B" cannot be deleted +s = '' +del a[s] # E: Expected TypedDict key to be string literal +del b[s] # E: Expected TypedDict key to be string literal [builtins fixtures/dict.pyi] From 6975fce36fa014063351718e8b9273148901c20d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 23 Nov 2018 10:48:41 +0000 Subject: [PATCH 18/25] Some cleanup --- mypy/plugin.py | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 9d9e36169d64..9ec9c582870e 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -574,26 +574,19 @@ def typed_dict_pop_signature_callback(ctx: MethodSigContext) -> CallableType: key = ctx.args[0][0].value value_type = ctx.type.items.get(key) if value_type: - default_arg = ctx.args[1][0] - if (isinstance(value_type, TypedDictType) - and isinstance(default_arg, DictExpr) - and len(default_arg.items) == 0): - # Caller has empty dict {} as default for typed dict. - value_type = value_type.copy_modified(required_keys=set()) # Tweak the signature to include the value type as context. It's # only needed for type inference since there's a union with a type # variable that accepts everything. tv = TypeVarType(signature.variables[0]) - ret_type = UnionType.make_simplified_union([value_type, tv]) + typ = UnionType.make_simplified_union([value_type, tv]) return signature.copy_modified( - arg_types=[signature.arg_types[0], - UnionType.make_simplified_union([value_type, tv])], - ret_type=ret_type) + arg_types=[signature.arg_types[0], typ], + ret_type=typ) return signature def typed_dict_pop_callback(ctx: MethodContext) -> Type: - """Infer a precise return type for TypedDict.get with literal first argument.""" + """Type check and infer a precise return type for TypedDict.pop.""" if (isinstance(ctx.type, TypedDictType) and len(ctx.arg_types) >= 1 and len(ctx.arg_types[0]) == 1): @@ -632,23 +625,13 @@ def typed_dict_setdefault_signature_callback(ctx: MethodSigContext) -> CallableT and len(ctx.args[1]) == 1): key = ctx.args[0][0].value value_type = ctx.type.items.get(key) - ret_type = signature.ret_type if value_type: - default_arg = ctx.args[1][0] - if (isinstance(value_type, TypedDictType) - and isinstance(default_arg, DictExpr) - and len(default_arg.items) == 0): - # Caller has empty dict {} as default for typed dict. - value_type = value_type.copy_modified(required_keys=set()) - # Tweak the signature to include the value type as context. - return signature.copy_modified( - arg_types=[signature.arg_types[0], value_type], - ret_type=ret_type) + return signature.copy_modified(arg_types=[signature.arg_types[0], value_type]) return signature def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: - """Infer a precise return type for TypedDict.setdefault with literal first argument.""" + """Type check TypedDict.setdefault and infer a precise return type.""" if (isinstance(ctx.type, TypedDictType) and len(ctx.arg_types) == 2 and len(ctx.arg_types[0]) == 1): @@ -667,7 +650,7 @@ def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: def typed_dict_del_callback(ctx: MethodContext) -> Type: - """Type check .__del__.""" + """Type check TypedDict.__del__.""" if (isinstance(ctx.type, TypedDictType) and len(ctx.arg_types) == 1 and len(ctx.arg_types[0]) == 1): From 7b776fa0cf7bdf557f1b5eee452b4117f06861bb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 23 Nov 2018 11:49:40 +0000 Subject: [PATCH 19/25] Update python evalution tests --- test-data/unit/pythoneval.test | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 83c4570897e4..44a7339f3d1c 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1064,8 +1064,16 @@ reveal_type('value' in c) reveal_type(c.keys()) reveal_type(c.items()) reveal_type(c.values()) +reveal_type(c.copy()) +reveal_type(c.setdefault('value', False)) +c.update({'value': 2}) +c.update({'invalid': 2}) +c.pop('value') c == c c != c +Cell2 = TypedDict('Cell2', {'value': int}, total=False) +c2 = Cell2() +reveal_type(c2.pop('value')) [out] _testTypedDictMappingMethods.py:5: error: Revealed type is 'builtins.str*' _testTypedDictMappingMethods.py:6: error: Revealed type is 'typing.Iterator[builtins.str*]' @@ -1074,6 +1082,11 @@ _testTypedDictMappingMethods.py:8: error: Revealed type is 'builtins.bool' _testTypedDictMappingMethods.py:9: error: Revealed type is 'typing.AbstractSet[builtins.str*]' _testTypedDictMappingMethods.py:10: error: Revealed type is 'typing.AbstractSet[Tuple[builtins.str*, builtins.object*]]' _testTypedDictMappingMethods.py:11: error: Revealed type is 'typing.ValuesView[builtins.object*]' +_testTypedDictMappingMethods.py:12: error: Revealed type is 'TypedDict('_testTypedDictMappingMethods.Cell', {'value': builtins.int})' +_testTypedDictMappingMethods.py:13: error: Revealed type is 'builtins.int' +_testTypedDictMappingMethods.py:15: error: Unexpected TypedDict key 'invalid' +_testTypedDictMappingMethods.py:16: error: Key 'value' of TypedDict "Cell" cannot be deleted +_testTypedDictMappingMethods.py:21: error: Revealed type is 'builtins.int' [case testCrashOnComplexCheckWithNamedTupleNext] from typing import NamedTuple From 576f92342f91ba6e61969fe8c1ed5e8eef4881e5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 23 Nov 2018 13:30:54 +0000 Subject: [PATCH 20/25] Fix type check --- mypy/plugin.py | 4 ++++ runtests.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 9ec9c582870e..5164ade792ba 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -62,6 +62,10 @@ class CheckerPluginInterface: msg = None # type: MessageBuilder options = None # type: Options + @abstractmethod + def fail(self, msg: str, ctx: Context) -> None: + raise NotImplementedError + @abstractmethod def named_generic_type(self, name: str, args: List[Type]) -> Instance: raise NotImplementedError diff --git a/runtests.py b/runtests.py index b03a80395b03..f9d9a5ff9e8f 100755 --- a/runtests.py +++ b/runtests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from os import system -from sys import argv, exit, platform, executable, version_info +from sys import argv, exit, platform, executable, version_info, stdout prog, *args = argv From 810815a13a10721d1785c1ad1f2850d05d921f38 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Dec 2018 12:39:41 +0000 Subject: [PATCH 21/25] Add comment --- test-data/unit/lib-stub/mypy_extensions.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 46f8ce423d69..d0e603187fb6 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -23,6 +23,7 @@ def KwArg(type: _T = ...) -> _T: ... class _TypedDict(Mapping[str, object]): def copy(self: _T) -> _T: ... def setdefault(self, k: str, default: object) -> object: ... + # Mypy expects that 'default' has a type variable type def pop(self, k: str, default: _T = ...) -> object: ... def update(self: _T, __m: _T) -> None: ... if sys.version_info[0] == 2: From 02d5f3135026e5e648695b0e59afa107a12f228c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Dec 2018 12:59:40 +0000 Subject: [PATCH 22/25] Fix some unsafety when using bound dict methods --- mypy/plugin.py | 10 ++++++---- test-data/unit/check-typeddict.test | 5 +++++ test-data/unit/lib-stub/mypy_extensions.pyi | 9 +++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 5164ade792ba..16f8341caa28 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -568,6 +568,7 @@ def typed_dict_pop_signature_callback(ctx: MethodSigContext) -> CallableType: depends on a TypedDict value type. """ signature = ctx.default_signature + str_type = ctx.api.named_generic_type('builtins.str', []) if (isinstance(ctx.type, TypedDictType) and len(ctx.args) == 2 and len(ctx.args[0]) == 1 @@ -584,9 +585,9 @@ def typed_dict_pop_signature_callback(ctx: MethodSigContext) -> CallableType: tv = TypeVarType(signature.variables[0]) typ = UnionType.make_simplified_union([value_type, tv]) return signature.copy_modified( - arg_types=[signature.arg_types[0], typ], + arg_types=[str_type, typ], ret_type=typ) - return signature + return signature.copy_modified(arg_types=[str_type, signature.arg_types[1]]) def typed_dict_pop_callback(ctx: MethodContext) -> Type: @@ -621,6 +622,7 @@ def typed_dict_setdefault_signature_callback(ctx: MethodSigContext) -> CallableT depends on a TypedDict value type. """ signature = ctx.default_signature + str_type = ctx.api.named_generic_type('builtins.str', []) if (isinstance(ctx.type, TypedDictType) and len(ctx.args) == 2 and len(ctx.args[0]) == 1 @@ -630,8 +632,8 @@ def typed_dict_setdefault_signature_callback(ctx: MethodSigContext) -> CallableT key = ctx.args[0][0].value value_type = ctx.type.items.get(key) if value_type: - return signature.copy_modified(arg_types=[signature.arg_types[0], value_type]) - return signature + return signature.copy_modified(arg_types=[str_type, value_type]) + return signature.copy_modified(arg_types=[str_type, signature.arg_types[1]]) def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index f62937081da5..68ebe4ac1242 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1499,6 +1499,8 @@ reveal_type(a.setdefault('y', [])) # E: Revealed type is 'builtins.list[builtins a.setdefault('y', '') # E: Argument 2 to "setdefault" of "TypedDict" has incompatible type "str"; expected "List[int]" x = '' a.setdefault(x, 1) # E: Expected TypedDict key to be string literal +alias = a.setdefault +alias(x, 1) # E: Argument 1 has incompatible type "str"; expected "NoReturn" a.update({}) a.update({'x': 1}) @@ -1537,6 +1539,9 @@ a.pop('invalid', '') # E: TypedDict "A" has no key 'invalid' b.pop('x') # E: Key 'x' of TypedDict "B" cannot be deleted x = '' b.pop(x) # E: Expected TypedDict key to be string literal +pop = b.pop +pop('x') # E: Argument 1 has incompatible type "str"; expected "NoReturn" +pop('invalid') # E: Argument 1 has incompatible type "str"; expected "NoReturn" [builtins fixtures/dict.pyi] [case testTypedDictDel] diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index d0e603187fb6..5aba41c724b2 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -1,5 +1,5 @@ # NOTE: Requires fixtures/dict.pyi -from typing import Dict, Type, TypeVar, Optional, Any, Generic, Mapping +from typing import Dict, Type, TypeVar, Optional, Any, Generic, Mapping, NoReturn import sys _T = TypeVar('_T') @@ -22,11 +22,12 @@ def KwArg(type: _T = ...) -> _T: ... # Fallback type for all typed dicts (does not exist at runtime) class _TypedDict(Mapping[str, object]): def copy(self: _T) -> _T: ... - def setdefault(self, k: str, default: object) -> object: ... + # Using NoReturn so that only calls using the plugin hook can go through. + def setdefault(self, k: NoReturn, default: object) -> object: ... # Mypy expects that 'default' has a type variable type - def pop(self, k: str, default: _T = ...) -> object: ... + def pop(self, k: NoReturn, default: _T = ...) -> object: ... def update(self: _T, __m: _T) -> None: ... - if sys.version_info[0] == 2: + if sys.version_info < (3, 0): def has_key(self) -> bool: ... def __delitem__(self, k: str) -> None: ... From 51edafa57bdfd8c4d68fa2b403f0c417e264d35a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Dec 2018 14:01:04 +0000 Subject: [PATCH 23/25] Fix unsafety issues with TypedDict "del x[y]" --- mypy/plugin.py | 14 +++++++++++--- test-data/unit/check-typeddict.test | 3 +++ test-data/unit/lib-stub/mypy_extensions.pyi | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 16f8341caa28..89907247647d 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -411,6 +411,8 @@ def get_method_signature_hook(self, fullname: str return typed_dict_pop_signature_callback elif fullname == 'mypy_extensions._TypedDict.update': return typed_dict_update_signature_callback + elif fullname == 'mypy_extensions._TypedDict.__delitem__': + return typed_dict_delitem_signature_callback elif fullname == 'ctypes.Array.__setitem__': return ctypes.array_setitem_callback return None @@ -428,7 +430,7 @@ def get_method_hook(self, fullname: str elif fullname == 'mypy_extensions._TypedDict.pop': return typed_dict_pop_callback elif fullname == 'mypy_extensions._TypedDict.__delitem__': - return typed_dict_del_callback + return typed_dict_delitem_callback elif fullname == 'ctypes.Array.__getitem__': return ctypes.array_getitem_callback elif fullname == 'ctypes.Array.__iter__': @@ -655,8 +657,14 @@ def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: return ctx.default_return_type -def typed_dict_del_callback(ctx: MethodContext) -> Type: - """Type check TypedDict.__del__.""" +def typed_dict_delitem_signature_callback(ctx: MethodContext) -> Type: + # Replace NoReturn as the argument type. + str_type = ctx.api.named_generic_type('builtins.str', []) + return ctx.default_signature.copy_modified(arg_types=[str_type]) + + +def typed_dict_delitem_callback(ctx: MethodContext) -> Type: + """Type check TypedDict.__delitem__.""" if (isinstance(ctx.type, TypedDictType) and len(ctx.arg_types) == 1 and len(ctx.arg_types[0]) == 1): diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 68ebe4ac1242..fa58fbf2867d 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1559,4 +1559,7 @@ del b['x'] # E: Key 'x' of TypedDict "B" cannot be deleted s = '' del a[s] # E: Expected TypedDict key to be string literal del b[s] # E: Expected TypedDict key to be string literal +alias = b.__delitem__ +alias('x') # E: Argument 1 has incompatible type "str"; expected "NoReturn" +alias(s) # E: Argument 1 has incompatible type "str"; expected "NoReturn" [builtins fixtures/dict.pyi] diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 5aba41c724b2..0d44e3c413d3 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -29,7 +29,7 @@ class _TypedDict(Mapping[str, object]): def update(self: _T, __m: _T) -> None: ... if sys.version_info < (3, 0): def has_key(self) -> bool: ... - def __delitem__(self, k: str) -> None: ... + def __delitem__(self, k: NoReturn) -> None: ... def TypedDict(typename: str, fields: Dict[str, Type[_T]], *, total: Any = ...) -> Type[dict]: ... From ab7a4850fc555e42fb33c47ed256e5eb0834f2bd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Dec 2018 14:03:42 +0000 Subject: [PATCH 24/25] Fix type signature --- mypy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 89907247647d..152a7a61aa42 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -657,7 +657,7 @@ def typed_dict_setdefault_callback(ctx: MethodContext) -> Type: return ctx.default_return_type -def typed_dict_delitem_signature_callback(ctx: MethodContext) -> Type: +def typed_dict_delitem_signature_callback(ctx: MethodSigContext) -> CallableType: # Replace NoReturn as the argument type. str_type = ctx.api.named_generic_type('builtins.str', []) return ctx.default_signature.copy_modified(arg_types=[str_type]) From 82189dddd6abdb2522252fa7b1303c2a2363f548 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 6 Dec 2018 11:44:37 +0000 Subject: [PATCH 25/25] Update based on review --- mypy/semanal_typeddict.py | 4 ++-- mypy/types.py | 5 ++++- runtests.py | 2 +- test-data/unit/check-typeddict.test | 6 ++++-- test-data/unit/lib-stub/mypy_extensions.pyi | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 6af1d001a96b..7be1c3897c34 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -276,8 +276,8 @@ def fail_typeddict_arg(self, message: str, def build_typeddict_typeinfo(self, name: str, items: List[str], types: List[Type], required_keys: Set[str]) -> TypeInfo: - fallback = (self.api.named_type_or_none('mypy_extensions._TypedDict', []) - or self.api.named_type('__builtins__.object')) + fallback = self.api.named_type_or_none('mypy_extensions._TypedDict', []) + assert fallback is not None info = self.api.basic_new_typeinfo(name, fallback) info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), required_keys, fallback) diff --git a/mypy/types.py b/mypy/types.py index ea820f22d4f4..51066e01b8bf 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1185,7 +1185,10 @@ class TypedDictType(Type): at runtime. If a TypedDict is named, its fallback will be an Instance of the named type - (ex: "Point") whose TypeInfo has a typeddict_type that is anonymous. + (ex: "Point") whose TypeInfo has a typeddict_type that is anonymous. This + is similar to how named tuples work. + + TODO: The fallback structure is perhaps overly complicated. """ items = None # type: OrderedDict[str, Type] # item_name -> item_type diff --git a/runtests.py b/runtests.py index f9d9a5ff9e8f..b03a80395b03 100755 --- a/runtests.py +++ b/runtests.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from os import system -from sys import argv, exit, platform, executable, version_info, stdout +from sys import argv, exit, platform, executable, version_info prog, *args = argv diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index fa58fbf2867d..4fabf234d534 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -1490,7 +1490,8 @@ A = TypedDict('A', {'x': int, 'y': List[int]}) a: A reveal_type(a.copy()) # E: Revealed type is 'TypedDict('__main__.A', {'x': builtins.int, 'y': builtins.list[builtins.int]})' -a.has_key() # E: "A" has no attribute "has_key" +a.has_key('x') # E: "A" has no attribute "has_key" +# TODO: Better error message a.clear() # E: "A" has no attribute "clear" a.setdefault('invalid', 1) # E: TypedDict "A" has no key 'invalid' @@ -1519,7 +1520,7 @@ from mypy_extensions import TypedDict A = TypedDict('A', {'x': int}) a = A(x=1) reveal_type(a.copy()) # E: Revealed type is 'TypedDict('__main__.A', {'x': builtins.int})' -reveal_type(a.has_key()) # E: Revealed type is 'builtins.bool' +reveal_type(a.has_key('y')) # E: Revealed type is 'builtins.bool' a.clear() # E: "A" has no attribute "clear" [builtins_py2 fixtures/dict.pyi] @@ -1535,6 +1536,7 @@ b: B reveal_type(a.pop('x')) # E: Revealed type is 'builtins.int' reveal_type(a.pop('y', [])) # E: Revealed type is 'builtins.list[builtins.int]' reveal_type(a.pop('x', '')) # E: Revealed type is 'Union[builtins.int, builtins.str]' +reveal_type(a.pop('x', (1, 2))) # E: Revealed type is 'Union[builtins.int, Tuple[builtins.int, builtins.int]]' a.pop('invalid', '') # E: TypedDict "A" has no key 'invalid' b.pop('x') # E: Key 'x' of TypedDict "B" cannot be deleted x = '' diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 0d44e3c413d3..7b1e61186544 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -28,7 +28,7 @@ class _TypedDict(Mapping[str, object]): def pop(self, k: NoReturn, default: _T = ...) -> object: ... def update(self: _T, __m: _T) -> None: ... if sys.version_info < (3, 0): - def has_key(self) -> bool: ... + def has_key(self, k: str) -> bool: ... def __delitem__(self, k: NoReturn) -> None: ... def TypedDict(typename: str, fields: Dict[str, Type[_T]], *, total: Any = ...) -> Type[dict]: ...