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 diff --git a/mypy/messages.py b/mypy/messages.py index 43934d39756a..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) @@ -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 cea4c64b456b..152a7a61aa42 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 @@ -61,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 @@ -400,6 +405,14 @@ 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 == 'mypy_extensions._TypedDict.pop': + 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 @@ -412,6 +425,12 @@ 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 == 'mypy_extensions._TypedDict.pop': + return typed_dict_pop_callback + elif fullname == 'mypy_extensions._TypedDict.__delitem__': + return typed_dict_delitem_callback elif fullname == 'ctypes.Array.__getitem__': return ctypes.array_getitem_callback elif fullname == 'ctypes.Array.__iter__': @@ -544,6 +563,136 @@ 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 + 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 + 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: + # 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]) + typ = UnionType.make_simplified_union([value_type, tv]) + return signature.copy_modified( + arg_types=[str_type, typ], + ret_type=typ) + return signature.copy_modified(arg_types=[str_type, signature.arg_types[1]]) + + +def typed_dict_pop_callback(ctx: MethodContext) -> Type: + """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): + 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): + 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) + else: + ctx.api.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, 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. + + This is used to get better type context for the second argument that + 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 + 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) + if value_type: + 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: + """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): + 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) + else: + ctx.api.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, ctx.context) + return AnyType(TypeOfAny.from_error) + return ctx.default_return_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]) + + +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): + 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) + else: + ctx.api.fail(messages.TYPEDDICT_KEY_MUST_BE_STRING_LITERAL, ctx.context) + return AnyType(TypeOfAny.from_error) + 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.as_anonymous() + 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/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/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index ae377b21bb02..7be1c3897c34 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -276,10 +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('typing.Mapping', - [self.api.named_type('__builtins__.str'), - self.api.named_type('__builtins__.object')]) - 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/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((' 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/mypy/types.py b/mypy/types.py index 201c40b30045..51066e01b8bf 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1172,12 +1172,23 @@ 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. 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 @@ -1227,7 +1238,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(): @@ -1250,10 +1261,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 @@ -1822,13 +1830,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 c9f935eeb5bd..4fabf234d534 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] @@ -1481,3 +1481,87 @@ 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('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' +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 +alias = a.setdefault +alias(x, 1) # E: Argument 1 has incompatible type "str"; expected "NoReturn" + +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: 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 "TypedDict({'x'?: int, 'y'?: 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('y')) # E: Revealed type is 'builtins.bool' +a.clear() # E: "A" has no attribute "clear" +[builtins_py2 fixtures/dict.pyi] + +[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]' +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 = '' +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] +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 +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/deps-types.test b/test-data/unit/deps-types.test index c8ed69d83bf3..8cc859929bc6 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -853,7 +853,6 @@ class I: pass -> 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 * diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 791ff9b2d7ea..7b1e61186544 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, NoReturn +import sys _T = TypeVar('_T') _U = TypeVar('_U') @@ -18,6 +19,18 @@ 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: ... + # 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: NoReturn, default: _T = ...) -> object: ... + def update(self: _T, __m: _T) -> None: ... + if sys.version_info < (3, 0): + 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]: ... # This is intended as a class decorator, but mypy rejects abstract classes 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 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(