diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 32cbbcbc34e1..b217dcd104df 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1,10 +1,11 @@ """Expression type checker. This file is conceptually part of TypeChecker.""" +from collections import OrderedDict from typing import cast, Dict, Set, List, Iterable, Tuple, Callable, Union, Optional from mypy.types import ( Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef, - TupleType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType, + TupleType, TypedDictType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType, PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, true_only, false_only, is_named_instance, function_type, get_typ_args, set_typ_args, @@ -169,6 +170,10 @@ def visit_call_expr(self, e: CallExpr) -> Type: if e.analyzed: # It's really a special form that only looks like a call. return self.accept(e.analyzed, self.chk.type_context[-1]) + if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, TypeInfo) and \ + e.callee.node.typeddict_type is not None: + return self.check_typeddict_call(e.callee.node.typeddict_type, + e.arg_kinds, e.arg_names, e.args, e) self.try_infer_partial_type(e) callee_type = self.accept(e.callee) if (self.chk.options.disallow_untyped_calls and @@ -178,6 +183,80 @@ def visit_call_expr(self, e: CallExpr) -> Type: return self.msg.untyped_function_call(callee_type, e) return self.check_call_expr_with_callee_type(callee_type, e) + def check_typeddict_call(self, callee: TypedDictType, + arg_kinds: List[int], + arg_names: List[str], + args: List[Expression], + context: Context) -> Type: + if len(args) >= 1 and all([ak == ARG_NAMED for ak in arg_kinds]): + # ex: Point(x=42, y=1337) + item_names = arg_names + item_args = args + return self.check_typeddict_call_with_kwargs( + callee, OrderedDict(zip(item_names, item_args)), context) + + if len(args) == 1 and arg_kinds[0] == ARG_POS: + unique_arg = args[0] + if isinstance(unique_arg, DictExpr): + # ex: Point({'x': 42, 'y': 1337}) + return self.check_typeddict_call_with_dict(callee, unique_arg, context) + if isinstance(unique_arg, CallExpr) and isinstance(unique_arg.analyzed, DictExpr): + # ex: Point(dict(x=42, y=1337)) + return self.check_typeddict_call_with_dict(callee, unique_arg.analyzed, context) + + if len(args) == 0: + # ex: EmptyDict() + return self.check_typeddict_call_with_kwargs( + callee, OrderedDict(), context) + + self.chk.fail(messages.INVALID_TYPEDDICT_ARGS, context) + return AnyType() + + def check_typeddict_call_with_dict(self, callee: TypedDictType, + kwargs: DictExpr, + context: Context) -> Type: + item_name_exprs = [item[0] for item in kwargs.items] + item_args = [item[1] for item in kwargs.items] + + item_names = [] # List[str] + for item_name_expr in item_name_exprs: + if not isinstance(item_name_expr, StrExpr): + self.chk.fail(messages.TYPEDDICT_ITEM_NAME_MUST_BE_STRING_LITERAL, item_name_expr) + return AnyType() + item_names.append(item_name_expr.value) + + return self.check_typeddict_call_with_kwargs( + callee, OrderedDict(zip(item_names, item_args)), context) + + def check_typeddict_call_with_kwargs(self, callee: TypedDictType, + kwargs: 'OrderedDict[str, Expression]', + context: Context) -> Type: + if callee.items.keys() != kwargs.keys(): + callee_item_names = callee.items.keys() + kwargs_item_names = kwargs.keys() + + self.msg.typeddict_instantiated_with_unexpected_items( + expected_item_names=list(callee_item_names), + actual_item_names=list(kwargs_item_names), + context=context) + return AnyType() + + items = OrderedDict() # type: OrderedDict[str, Type] + for (item_name, item_expected_type) in callee.items.items(): + item_value = kwargs[item_name] + + item_actual_type = self.chk.check_simple_assignment( + lvalue_type=item_expected_type, rvalue=item_value, context=item_value, + msg=messages.INCOMPATIBLE_TYPES, + lvalue_name='TypedDict item "{}"'.format(item_name), + rvalue_name='expression') + items[item_name] = item_actual_type + + mapping_value_type = join.join_type_list(list(items.values())) + fallback = self.chk.named_generic_type('typing.Mapping', + [self.chk.str_type(), mapping_value_type]) + return TypedDictType(items, fallback) + # Types and methods that can be used to infer partial types. item_args = {'builtins.list': ['append'], 'builtins.set': ['add', 'discard'], diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 9a55103f9215..d1e9ab1cc777 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -3,7 +3,7 @@ from typing import cast, Callable, List, Optional, TypeVar from mypy.types import ( - Type, Instance, AnyType, TupleType, CallableType, FunctionLike, TypeVarDef, + Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarDef, Overloaded, TypeVarType, UnionType, PartialType, DeletedType, NoneTyp, TypeType, function_type ) @@ -116,6 +116,11 @@ def analyze_member_access(name: str, return analyze_member_access(name, typ.fallback, node, is_lvalue, is_super, is_operator, builtin_type, not_ready_callback, msg, original_type=original_type, chk=chk) + elif isinstance(typ, TypedDictType): + # Actually look up from the fallback instance type. + return analyze_member_access(name, typ.fallback, node, is_lvalue, is_super, + is_operator, builtin_type, not_ready_callback, msg, + original_type=original_type, chk=chk) elif isinstance(typ, FunctionLike) and typ.is_type_obj(): # Class attribute. # TODO super? diff --git a/mypy/constraints.py b/mypy/constraints.py index e26e583522ab..1d1e1c332872 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -1,11 +1,11 @@ """Type inference constraints.""" -from typing import List, Optional +from typing import Iterable, List, Optional from mypy.types import ( CallableType, Type, TypeVisitor, UnboundType, AnyType, Void, NoneTyp, TypeVarType, - Instance, TupleType, UnionType, Overloaded, ErasedType, PartialType, DeletedType, - UninhabitedType, TypeType, TypeVarId, is_named_instance + Instance, TupleType, TypedDictType, UnionType, Overloaded, ErasedType, PartialType, + DeletedType, UninhabitedType, TypeType, TypeVarId, is_named_instance ) from mypy.maptype import map_instance_to_supertype from mypy import nodes @@ -342,11 +342,27 @@ def visit_tuple_type(self, template: TupleType) -> List[Constraint]: else: return [] + def visit_typeddict_type(self, template: TypedDictType) -> List[Constraint]: + actual = self.actual + if isinstance(actual, TypedDictType): + res = [] # type: List[Constraint] + # NOTE: Non-matching keys are ignored. Compatibility is checked + # elsewhere so this shouldn't be unsafe. + for (item_name, template_item_type, actual_item_type) in template.zip(actual): + res.extend(infer_constraints(template_item_type, + actual_item_type, + self.direction)) + return res + elif isinstance(actual, AnyType): + return self.infer_against_any(template.items.values()) + else: + return [] + def visit_union_type(self, template: UnionType) -> List[Constraint]: assert False, ("Unexpected UnionType in ConstraintBuilderVisitor" " (should have been handled in infer_constraints)") - def infer_against_any(self, types: List[Type]) -> List[Constraint]: + def infer_against_any(self, types: Iterable[Type]) -> List[Constraint]: res = [] # type: List[Constraint] for t in types: res.extend(infer_constraints(t, AnyType(), self.direction)) diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 3f53f75219b2..47a0d7241bb2 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -2,8 +2,8 @@ from mypy.types import ( Type, TypeVisitor, UnboundType, ErrorType, AnyType, Void, NoneTyp, TypeVarId, - Instance, TypeVarType, CallableType, TupleType, UnionType, Overloaded, ErasedType, - PartialType, DeletedType, TypeTranslator, TypeList, UninhabitedType, TypeType + Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded, + ErasedType, PartialType, DeletedType, TypeTranslator, TypeList, UninhabitedType, TypeType ) from mypy import experiments @@ -78,6 +78,9 @@ def visit_overloaded(self, t: Overloaded) -> Type: def visit_tuple_type(self, t: TupleType) -> Type: return t.fallback.accept(self) + def visit_typeddict_type(self, t: TypedDictType) -> Type: + return t.fallback.accept(self) + def visit_union_type(self, t: UnionType) -> Type: return AnyType() # XXX: return underlying type if only one? diff --git a/mypy/expandtype.py b/mypy/expandtype.py index c1ff8088b3c5..785c77d005e9 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -1,9 +1,9 @@ -from typing import Dict, List +from typing import Dict, Iterable, List from mypy.types import ( Type, Instance, CallableType, TypeVisitor, UnboundType, ErrorType, AnyType, - Void, NoneTyp, TypeVarType, Overloaded, TupleType, UnionType, ErasedType, TypeList, - PartialType, DeletedType, UninhabitedType, TypeType, TypeVarId + Void, NoneTyp, TypeVarType, Overloaded, TupleType, TypedDictType, UnionType, + ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, TypeVarId ) @@ -93,6 +93,9 @@ def visit_overloaded(self, t: Overloaded) -> Type: def visit_tuple_type(self, t: TupleType) -> Type: return t.copy_modified(items=self.expand_types(t.items)) + def visit_typeddict_type(self, t: TypedDictType) -> Type: + return t.copy_modified(item_types=self.expand_types(t.items.values())) + def visit_union_type(self, t: UnionType) -> Type: # After substituting for type variables in t.items, # some of the resulting types might be subtypes of others. @@ -108,7 +111,7 @@ def visit_type_type(self, t: TypeType) -> Type: item = t.item.accept(self) return TypeType(item) - def expand_types(self, types: List[Type]) -> List[Type]: + def expand_types(self, types: Iterable[Type]) -> List[Type]: a = [] # type: List[Type] for t in types: a.append(t.accept(self)) diff --git a/mypy/fixup.py b/mypy/fixup.py index 147238db8e43..8375b9fa58ae 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -2,13 +2,17 @@ from typing import Any, Dict, Optional -from mypy.nodes import (MypyFile, SymbolNode, SymbolTable, SymbolTableNode, - TypeInfo, FuncDef, OverloadedFuncDef, Decorator, Var, - TypeVarExpr, ClassDef, - LDEF, MDEF, GDEF) -from mypy.types import (CallableType, EllipsisType, Instance, Overloaded, TupleType, - TypeList, TypeVarType, UnboundType, UnionType, TypeVisitor, - TypeType) +from mypy.nodes import ( + MypyFile, SymbolNode, SymbolTable, SymbolTableNode, + TypeInfo, FuncDef, OverloadedFuncDef, Decorator, Var, + TypeVarExpr, ClassDef, + LDEF, MDEF, GDEF +) +from mypy.types import ( + CallableType, EllipsisType, Instance, Overloaded, TupleType, TypedDictType, + TypeList, TypeVarType, UnboundType, UnionType, TypeVisitor, + TypeType +) from mypy.visitor import NodeVisitor @@ -192,6 +196,13 @@ def visit_tuple_type(self, tt: TupleType) -> None: if tt.fallback is not None: tt.fallback.accept(self) + def visit_typeddict_type(self, tdt: TypedDictType) -> None: + if tdt.items: + for it in tdt.items.values(): + it.accept(self) + if tdt.fallback is not None: + tdt.fallback.accept(self) + def visit_type_list(self, tl: TypeList) -> None: for t in tl.items: t.accept(self) diff --git a/mypy/indirection.py b/mypy/indirection.py index 77c5a59e88e3..b36d999fd19f 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -87,6 +87,9 @@ def visit_overloaded(self, t: types.Overloaded) -> Set[str]: def visit_tuple_type(self, t: types.TupleType) -> Set[str]: return self._visit(*t.items) | self._visit(t.fallback) + def visit_typeddict_type(self, t: types.TypedDictType) -> Set[str]: + return self._visit(*t.items.values()) | self._visit(t.fallback) + def visit_star_type(self, t: types.StarType) -> Set[str]: return set() diff --git a/mypy/join.py b/mypy/join.py index 6d86106be281..d14b83ded9fb 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -1,10 +1,11 @@ """Calculation of the least upper bound types (joins).""" -from typing import List +from collections import OrderedDict +from typing import cast, List from mypy.types import ( Type, AnyType, NoneTyp, Void, TypeVisitor, Instance, UnboundType, - ErrorType, TypeVarType, CallableType, TupleType, ErasedType, TypeList, + ErrorType, TypeVarType, CallableType, TupleType, TypedDictType, ErasedType, TypeList, UnionType, FunctionLike, Overloaded, PartialType, DeletedType, UninhabitedType, TypeType, true_or_false ) @@ -170,6 +171,8 @@ def visit_instance(self, t: Instance) -> Type: return join_types(t, self.s.fallback) elif isinstance(self.s, TypeType): return join_types(t, self.s) + elif isinstance(self.s, TypedDictType): + return join_types(t, self.s) else: return self.default(self.s) @@ -234,13 +237,27 @@ def visit_tuple_type(self, t: TupleType) -> Type: items = [] # type: List[Type] for i in range(t.length()): items.append(self.join(t.items[i], self.s.items[i])) - # join fallback types if they are different fallback = join_instances(self.s.fallback, t.fallback) assert isinstance(fallback, Instance) return TupleType(items, fallback) else: return self.default(self.s) + def visit_typeddict_type(self, t: TypedDictType) -> Type: + if isinstance(self.s, TypedDictType): + items = OrderedDict([ + (item_name, s_item_type) + for (item_name, s_item_type, t_item_type) in self.s.zip(t) + if is_equivalent(s_item_type, t_item_type) + ]) + mapping_value_type = join_type_list(list(items.values())) + fallback = self.s.create_anonymous_fallback(value_type=mapping_value_type) + return TypedDictType(items, fallback) + elif isinstance(self.s, Instance): + return join_instances(self.s, t.fallback) + else: + return self.default(self.s) + def visit_partial_type(self, t: PartialType) -> Type: # We only have partial information so we can't decide the join result. We should # never get here. @@ -266,6 +283,8 @@ def default(self, typ: Type) -> Type: return ErrorType() elif isinstance(typ, TupleType): return self.default(typ.fallback) + elif isinstance(typ, TypedDictType): + return self.default(typ.fallback) elif isinstance(typ, FunctionLike): return self.default(typ.fallback) elif isinstance(typ, TypeVarType): diff --git a/mypy/meet.py b/mypy/meet.py index 18796ae2491c..fe9b41d21209 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -1,12 +1,13 @@ -from typing import List +from collections import OrderedDict +from typing import List, Optional -from mypy.join import is_similar_callables, combine_similar_callables +from mypy.join import is_similar_callables, combine_similar_callables, join_type_list from mypy.types import ( Type, AnyType, TypeVisitor, UnboundType, Void, ErrorType, NoneTyp, TypeVarType, - Instance, CallableType, TupleType, ErasedType, TypeList, UnionType, PartialType, + Instance, CallableType, TupleType, TypedDictType, ErasedType, TypeList, UnionType, PartialType, DeletedType, UninhabitedType, TypeType ) -from mypy.subtypes import is_subtype +from mypy.subtypes import is_equivalent, is_subtype from mypy import experiments @@ -246,6 +247,21 @@ def visit_tuple_type(self, t: TupleType) -> Type: else: return self.default(self.s) + def visit_typeddict_type(self, t: TypedDictType) -> Type: + if isinstance(self.s, TypedDictType): + for (_, l, r) in self.s.zip(t): + if not is_equivalent(l, r): + return self.default(self.s) + items = OrderedDict([ + (item_name, s_item_type or t_item_type) + for (item_name, s_item_type, t_item_type) in self.s.zipall(t) + ]) + mapping_value_type = join_type_list(list(items.values())) + fallback = self.s.create_anonymous_fallback(value_type=mapping_value_type) + return TypedDictType(items, fallback) + else: + return self.default(self.s) + def visit_partial_type(self, t: PartialType) -> Type: # We can't determine the meet of partial types. We should never get here. assert False, 'Internal error' diff --git a/mypy/messages.py b/mypy/messages.py index e3eec25d37c7..0c3e411bc82b 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -10,8 +10,8 @@ from mypy.errors import Errors from mypy.types import ( - Type, CallableType, Instance, TypeVarType, TupleType, UnionType, Void, NoneTyp, AnyType, - Overloaded, FunctionLike, DeletedType, TypeType + Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, + UnionType, Void, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType ) from mypy.nodes import ( TypeInfo, Context, MypyFile, op_methods, FuncDef, reverse_type_aliases, @@ -72,6 +72,10 @@ KEYWORD_ARGUMENT_REQUIRES_STR_KEY_TYPE = \ 'Keyword argument only valid with "str" key type in call to "dict"' ALL_MUST_BE_SEQ_STR = 'Type of __all__ must be {}, not {}' +INVALID_TYPEDDICT_ARGS = \ + 'Expected keyword arguments, {...}, or dict(...) in TypedDict constructor' +TYPEDDICT_ITEM_NAME_MUST_BE_STRING_LITERAL = \ + 'Expected TypedDict item name to be string literal' class MessageBuilder: @@ -204,8 +208,8 @@ def format_simple(self, typ: Type, verbosity: int = 0) -> str: # interpreted as a normal word. return '"{}"'.format(base_str) elif itype.type.fullname() == 'builtins.tuple': - item_type = strip_quotes(self.format(itype.args[0])) - return 'Tuple[{}, ...]'.format(item_type) + item_type_str = strip_quotes(self.format(itype.args[0])) + return 'Tuple[{}, ...]'.format(item_type_str) elif itype.type.fullname() in reverse_type_aliases: alias = reverse_type_aliases[itype.type.fullname()] alias = alias.split('.')[-1] @@ -239,6 +243,15 @@ def format_simple(self, typ: Type, verbosity: int = 0) -> str: return s else: return 'tuple(length {})'.format(len(items)) + elif isinstance(typ, TypedDictType): + # If the TypedDictType is named, return the name + if typ.fallback.type.fullname() != 'typing.Mapping': + return self.format_simple(typ.fallback) + items = [] + for (item_name, item_type) in typ.items.items(): + items.append('{}={}'.format(item_name, strip_quotes(self.format(item_type)))) + s = '"TypedDict({})"'.format(', '.join(items)) + return s elif isinstance(typ, UnionType): # Only print Unions as Optionals if the Optional wouldn't have to contain another Union print_as_optional = (len(typ.items) - @@ -788,6 +801,13 @@ def unsupported_type_type(self, item: Type, context: Context) -> None: def redundant_cast(self, typ: Type, context: Context) -> None: self.note('Redundant cast to {}'.format(self.format(typ)), context) + def typeddict_instantiated_with_unexpected_items(self, + expected_item_names: List[str], + actual_item_names: List[str], + context: Context) -> None: + self.fail('Expected items {} but found {}.'.format( + expected_item_names, actual_item_names), context) + def capitalize(s: str) -> str: """Capitalize the first character of a string.""" diff --git a/mypy/nodes.py b/mypy/nodes.py index 013373cf2344..3719c3f85bfe 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1894,13 +1894,14 @@ class is generic then it will be a type constructor of higher kind. # object used for this class is not an Instance but a TupleType; # the corresponding Instance is set as the fallback type of the # tuple type. - tuple_type = None # type: mypy.types.TupleType + tuple_type = None # type: Optional[mypy.types.TupleType] # Is this a named tuple type? is_named_tuple = False - # Is this a typed dict type? - is_typed_dict = False + # If this class is defined by the TypedDict type constructor, + # then this is not None. + typeddict_type = None # type: Optional[mypy.types.TypedDictType] # Is this a newtype type? is_newtype = False @@ -1910,7 +1911,7 @@ class is generic then it will be a type constructor of higher kind. FLAGS = [ 'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple', - 'is_typed_dict', 'is_newtype' + 'is_newtype' ] def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> None: @@ -2045,6 +2046,8 @@ def serialize(self) -> JsonDict: 'bases': [b.serialize() for b in self.bases], '_promote': None if self._promote is None else self._promote.serialize(), 'tuple_type': None if self.tuple_type is None else self.tuple_type.serialize(), + 'typeddict_type': + None if self.typeddict_type is None else self.typeddict_type.serialize(), 'flags': get_flags(self, TypeInfo.FLAGS), } return data @@ -2065,6 +2068,8 @@ def deserialize(cls, data: JsonDict) -> 'TypeInfo': else mypy.types.Type.deserialize(data['_promote'])) ti.tuple_type = (None if data['tuple_type'] is None else mypy.types.TupleType.deserialize(data['tuple_type'])) + ti.typeddict_type = (None if data['typeddict_type'] is None + else mypy.types.TypedDictType.deserialize(data['typeddict_type'])) set_flags(ti, data['flags']) return ti diff --git a/mypy/sametypes.py b/mypy/sametypes.py index 9e428ae60e15..48782738962a 100644 --- a/mypy/sametypes.py +++ b/mypy/sametypes.py @@ -1,9 +1,9 @@ from typing import Sequence from mypy.types import ( - Type, UnboundType, ErrorType, AnyType, NoneTyp, Void, TupleType, UnionType, CallableType, - TypeVarType, Instance, TypeVisitor, ErasedType, TypeList, Overloaded, PartialType, - DeletedType, UninhabitedType, TypeType + Type, UnboundType, ErrorType, AnyType, NoneTyp, Void, TupleType, TypedDictType, + UnionType, CallableType, TypeVarType, Instance, TypeVisitor, ErasedType, + TypeList, Overloaded, PartialType, DeletedType, UninhabitedType, TypeType ) @@ -111,6 +111,17 @@ def visit_tuple_type(self, left: TupleType) -> bool: else: return False + def visit_typeddict_type(self, left: TypedDictType) -> bool: + if isinstance(self.right, TypedDictType): + if left.items.keys() != self.right.items.keys(): + return False + for (_, left_item_type, right_item_type) in left.zip(self.right): + if not is_same_type(left_item_type, right_item_type): + return False + return True + else: + return False + def visit_union_type(self, left: UnionType) -> bool: # XXX This is a test for syntactic equality, not equivalence if isinstance(self.right, UnionType): diff --git a/mypy/semanal.py b/mypy/semanal.py index 5e213a841c01..7ea1ebd40f7d 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -43,6 +43,7 @@ traverse the entire AST. """ +from collections import OrderedDict from typing import ( List, Dict, Set, Tuple, cast, TypeVar, Union, Optional, Callable ) @@ -72,13 +73,15 @@ from mypy.types import ( NoneTyp, CallableType, Overloaded, Instance, Type, TypeVarType, AnyType, FunctionLike, UnboundType, TypeList, TypeVarDef, TypeType, - TupleType, UnionType, StarType, EllipsisType, function_type) + TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType, +) from mypy.nodes import implicit_module_attrs from mypy.typeanal import TypeAnalyser, TypeAnalyserPass3, analyze_type_alias from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.sametypes import is_same_type from mypy.erasetype import erase_typevars from mypy.options import Options +from mypy import join T = TypeVar('T') @@ -905,6 +908,9 @@ def analyze_metaclass(self, defn: ClassDef) -> None: def object_type(self) -> Instance: return self.named_type('__builtins__.object') + def str_type(self) -> Instance: + return self.named_type('__builtins__.str') + def class_type(self, info: TypeInfo) -> Type: # Construct a function type whose fallback is cls. from mypy import checkmember # To avoid import cycle. @@ -1724,7 +1730,7 @@ def parse_namedtuple_args(self, call: CallExpr, types = [AnyType() for _ in items] underscore = [item for item in items if item.startswith('_')] if underscore: - self.fail("namedtuple() Field names cannot start with an underscore: " + self.fail("namedtuple() field names cannot start with an underscore: " + ', '.join(underscore), call) return items, types, ok @@ -1767,7 +1773,7 @@ def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance) -> TypeI def build_namedtuple_typeinfo(self, name: str, items: List[str], types: List[Type]) -> TypeInfo: - strtype = self.named_type('__builtins__.str') # type: Type + strtype = self.str_type() basetuple_type = self.named_type('__builtins__.tuple', [AnyType()]) dictype = (self.named_type_or_none('builtins.dict', [strtype, AnyType()]) or self.object_type()) @@ -1922,6 +1928,10 @@ def parse_typeddict_args(self, call: CallExpr, "TypedDict() expects a dictionary literal as the second argument", call) dictexpr = args[1] items, types, ok = self.parse_typeddict_fields_with_types(dictexpr.items, call) + underscore = [item for item in items if item.startswith('_')] + if underscore: + self.fail("TypedDict() item names cannot start with an underscore: " + + ', '.join(underscore), call) return items, types, ok def parse_typeddict_fields_with_types(self, dict_items: List[Tuple[Expression, Expression]], @@ -1947,17 +1957,13 @@ def fail_typeddict_arg(self, message: str, def build_typeddict_typeinfo(self, name: str, items: List[str], types: List[Type]) -> TypeInfo: - strtype = self.named_type('__builtins__.str') # type: Type - dictype = (self.named_type_or_none('builtins.dict', [strtype, AnyType()]) - or self.object_type()) - fallback = dictype + mapping_value_type = join.join_type_list(types) + fallback = (self.named_type_or_none('typing.Mapping', + [self.str_type(), mapping_value_type]) + or self.object_type()) info = self.basic_new_typeinfo(name, fallback) - info.is_typed_dict = True - - # (TODO: Store {items, types} inside "info" somewhere for use later. - # Probably inside a new "info.keys" field which - # would be analogous to "info.names".) + info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), fallback) return info diff --git a/mypy/subtypes.py b/mypy/subtypes.py index bfb3c4b58e87..bbc8e3b0b941 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -2,7 +2,7 @@ from mypy.types import ( Type, AnyType, UnboundType, TypeVisitor, ErrorType, Void, NoneTyp, - Instance, TypeVarType, CallableType, TupleType, UnionType, Overloaded, + Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded, ErasedType, TypeList, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance ) import mypy.applytype @@ -188,6 +188,21 @@ def visit_tuple_type(self, left: TupleType) -> bool: else: return False + def visit_typeddict_type(self, left: TypedDictType) -> bool: + right = self.right + if isinstance(right, Instance): + return is_subtype(left.fallback, right, self.check_type_parameter) + elif isinstance(right, TypedDictType): + if not left.names_are_wider_than(right): + return False + for (_, l, r) in left.zip(right): + if not is_equivalent(l, r, self.check_type_parameter): + return False + # (NOTE: Fallbacks don't matter.) + return True + else: + return False + def visit_overloaded(self, left: Overloaded) -> bool: right = self.right if isinstance(right, Instance): diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index fa0019d1a947..885570197568 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -54,6 +54,7 @@ 'check-isinstance.test', 'check-lists.test', 'check-namedtuple.test', + 'check-typeddict.test', 'check-type-aliases.test', 'check-ignore.test', 'check-type-promotion.test', diff --git a/mypy/typeanal.py b/mypy/typeanal.py index fc0d897ec0a5..4d9f1709a9ce 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1,9 +1,10 @@ """Semantic analysis of types""" +from collections import OrderedDict from typing import Callable, cast, List, Optional from mypy.types import ( - Type, UnboundType, TypeVarType, TupleType, UnionType, Instance, + Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args, ) @@ -191,9 +192,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: # Instance with an invalid number of type arguments. instance = Instance(info, self.anal_array(t.args), t.line, t.column) tup = info.tuple_type - if tup is None: - return instance - else: + if tup is not None: # The class has a Tuple[...] base class so it will be # represented as a tuple type. if t.args: @@ -201,6 +200,17 @@ def visit_unbound_type(self, t: UnboundType) -> Type: return AnyType() return tup.copy_modified(items=self.anal_array(tup.items), fallback=instance) + td = info.typeddict_type + if td is not None: + # The class has a TypedDict[...] base class so it will be + # represented as a typeddict type. + if t.args: + self.fail('Generic TypedDict types not supported', t) + return AnyType() + # Create a named TypedDictType + return td.copy_modified(item_types=self.anal_array(list(td.items.values())), + fallback=instance) + return instance else: return AnyType() @@ -294,6 +304,13 @@ def visit_tuple_type(self, t: TupleType) -> Type: fallback = t.fallback if t.fallback else self.builtin_type('builtins.tuple', [AnyType()]) return TupleType(self.anal_array(t.items), fallback, t.line) + def visit_typeddict_type(self, t: TypedDictType) -> Type: + items = OrderedDict([ + (item_name, item_type.accept(self)) + for (item_name, item_type) in t.items.items() + ]) + return TypedDictType(items, t.fallback) + def visit_star_type(self, t: StarType) -> Type: return StarType(t.type.accept(self), t.line) @@ -460,6 +477,10 @@ def visit_tuple_type(self, t: TupleType) -> None: for item in t.items: item.accept(self) + def visit_typeddict_type(self, t: TypedDictType) -> None: + for item_type in t.items.values(): + item_type.accept(self) + def visit_union_type(self, t: UnionType) -> None: for item in t.items: item.accept(self) diff --git a/mypy/types.py b/mypy/types.py index 34c1ff2eb584..a385db829453 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2,8 +2,9 @@ from abc import abstractmethod import copy +from collections import OrderedDict from typing import ( - Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Sequence, Optional, Union + Any, TypeVar, Dict, List, Tuple, cast, Generic, Set, Sequence, Optional, Union, Iterable, ) import mypy.nodes @@ -451,6 +452,9 @@ def deserialize(cls, data: JsonDict) -> 'Instance': inst.type_ref = data['type_ref'] # Will be fixed up by fixup.py later. return inst + def copy_modified(self, *, args: List[Type]) -> 'Instance': + return Instance(self.type, args, self.line, self.column, self.erased) + class TypeVarType(Type): """A type variable type. @@ -799,7 +803,7 @@ def deserialize(cls, data: JsonDict) -> 'TupleType': implicit=data['implicit']) def copy_modified(self, *, fallback: Instance = None, - items: List[Type] = None) -> 'TupleType': + items: List[Type] = None) -> 'TupleType': if fallback is None: fallback = self.fallback if items is None: @@ -811,6 +815,86 @@ def slice(self, begin: int, stride: int, end: int) -> 'TupleType': self.line, self.column, self.implicit) +class TypedDictType(Type): + """The type of a TypedDict instance. TypedDict(K1=VT1, ..., Kn=VTn) + + 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. + """ + + items = None # type: OrderedDict[str, Type] # (item_name, item_type) + fallback = None # type: Instance + + def __init__(self, items: 'OrderedDict[str, Type]', fallback: Instance, + line: int = -1, column: int = -1) -> None: + self.items = items + self.fallback = fallback + self.can_be_true = len(self.items) > 0 + self.can_be_false = len(self.items) == 0 + super().__init__(line, column) + + def accept(self, visitor: 'TypeVisitor[T]') -> T: + return visitor.visit_typeddict_type(self) + + def serialize(self) -> JsonDict: + return {'.class': 'TypedDictType', + 'items': [[n, t.serialize()] for (n, t) in self.items.items()], + 'fallback': self.fallback.serialize(), + } + + @classmethod + def deserialize(cls, data: JsonDict) -> 'TypedDictType': + assert data['.class'] == 'TypedDictType' + return TypedDictType(OrderedDict([(n, Type.deserialize(t)) for (n, t) in data['items']]), + Instance.deserialize(data['fallback'])) + + def as_anonymous(self): + if self.fallback.type.fullname() == 'typing.Mapping': + return self + assert self.fallback.type.typeddict_type is not None + return self.fallback.type.typeddict_type.as_anonymous() + + def copy_modified(self, *, fallback: Instance = None, + item_types: List[Type] = None) -> 'TypedDictType': + if fallback is None: + fallback = self.fallback + if item_types is None: + items = self.items + else: + items = OrderedDict(zip(self.items, item_types)) + return TypedDictType(items, fallback, self.line, self.column) + + 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 + ]) + + def names_are_wider_than(self, other: 'TypedDictType'): + return len(other.items.keys() - self.items.keys()) == 0 + + def zip(self, right: 'TypedDictType') -> Iterable[Tuple[str, Type, Type]]: + left = self + for (item_name, left_item_type) in left.items.items(): + right_item_type = right.items.get(item_name) + if right_item_type is not None: + yield (item_name, left_item_type, right_item_type) + + def zipall(self, right: 'TypedDictType') \ + -> Iterable[Tuple[str, Optional[Type], Optional[Type]]]: + left = self + for (item_name, left_item_type) in left.items.items(): + right_item_type = right.items.get(item_name) + yield (item_name, left_item_type, right_item_type) + for (item_name, right_item_type) in right.items.items(): + if item_name in left.items: + continue + yield (item_name, None, right_item_type) + + class StarType(Type): """The star type *type_parameter. @@ -1080,6 +1164,10 @@ def visit_overloaded(self, t: Overloaded) -> T: def visit_tuple_type(self, t: TupleType) -> T: pass + @abstractmethod + def visit_typeddict_type(self, t: TypedDictType) -> T: + pass + def visit_star_type(self, t: StarType) -> T: raise self._notimplemented_helper('star_type') @@ -1149,9 +1237,20 @@ def visit_callable_type(self, t: CallableType) -> Type: def visit_tuple_type(self, t: TupleType) -> Type: return TupleType(self.translate_types(t.items), + # TODO: This appears to be unsafe. cast(Any, t.fallback.accept(self)), t.line, t.column) + def visit_typeddict_type(self, t: TypedDictType) -> Type: + items = OrderedDict([ + (item_name, item_type.accept(self)) + for (item_name, item_type) in t.items.items() + ]) + return TypedDictType(items, + # TODO: This appears to be unsafe. + cast(Any, t.fallback.accept(self)), + t.line, t.column) + def visit_star_type(self, t: StarType) -> Type: return StarType(t.type.accept(self), t.line, t.column) @@ -1287,6 +1386,15 @@ def visit_tuple_type(self, t: TupleType) -> str: return 'Tuple[{}, fallback={}]'.format(s, t.fallback.accept(self)) return 'Tuple[{}]'.format(s) + def visit_typeddict_type(self, t: TypedDictType) -> str: + s = self.keywords_str(t.items.items()) + if t.fallback and t.fallback.type: + if s == '': + return 'TypedDict(_fallback={})'.format(t.fallback.accept(self)) + else: + return 'TypedDict({}, _fallback={})'.format(s, t.fallback.accept(self)) + return 'TypedDict({})'.format(s) + def visit_star_type(self, t: StarType) -> str: s = t.type.accept(self) return '*{}'.format(s) @@ -1320,6 +1428,15 @@ def list_str(self, a: List[Type]) -> str: res.append(str(t)) return ', '.join(res) + def keywords_str(self, a: Iterable[Tuple[str, Type]]) -> str: + """Convert keywords to strings (pretty-print types) + and join the results with commas. + """ + return ', '.join([ + '{}={}'.format(name, t.accept(self)) + for (name, t) in a + ]) + # These constants define the method used by TypeQuery to combine multiple # query results, e.g. for tuple types. The strategy is not used for empty @@ -1391,6 +1508,9 @@ def visit_callable_type(self, t: CallableType) -> bool: def visit_tuple_type(self, t: TupleType) -> bool: return self.query_types(t.items) + def visit_typeddict_type(self, t: TypedDictType) -> bool: + return self.query_types(t.items.values()) + def visit_star_type(self, t: StarType) -> bool: return t.type.accept(self) @@ -1403,7 +1523,7 @@ def visit_overloaded(self, t: Overloaded) -> bool: def visit_type_type(self, t: TypeType) -> bool: return t.item.accept(self) - def query_types(self, types: Sequence[Type]) -> bool: + def query_types(self, types: Iterable[Type]) -> bool: """Perform a query for a list of types. Use the strategy constant to combine the results. diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 650c2101e33e..0eba241b3564 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -23,7 +23,7 @@ x[2] # E: Tuple index out of range [case testNamedTupleNoUnderscoreFields] from collections import namedtuple -X = namedtuple('X', 'x, _y, _z') # E: namedtuple() Field names cannot start with an underscore: _y, _z +X = namedtuple('X', 'x, _y, _z') # E: namedtuple() field names cannot start with an underscore: _y, _z [case testNamedTupleAccessingAttributes] from collections import namedtuple diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test new file mode 100644 index 000000000000..00a17e263232 --- /dev/null +++ b/test-data/unit/check-typeddict.test @@ -0,0 +1,444 @@ +-- Create Instance + +[case testCanCreateTypedDictInstanceWithKeywordArguments] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(x=42, y=1337) +reveal_type(p) # E: Revealed type is 'TypedDict(x=builtins.int, y=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' +[builtins fixtures/dict.pyi] + +[case testCanCreateTypedDictInstanceWithDictCall] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(dict(x=42, y=1337)) +reveal_type(p) # E: Revealed type is 'TypedDict(x=builtins.int, y=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' +[builtins fixtures/dict.pyi] + +[case testCanCreateTypedDictInstanceWithDictLiteral] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point({'x': 42, 'y': 1337}) +reveal_type(p) # E: Revealed type is 'TypedDict(x=builtins.int, y=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' +[builtins fixtures/dict.pyi] + +[case testCanCreateTypedDictInstanceWithNoArguments] +from mypy_extensions import TypedDict +EmptyDict = TypedDict('EmptyDict', {}) +p = EmptyDict() +reveal_type(p) # E: Revealed type is 'TypedDict(_fallback=typing.Mapping[builtins.str, builtins.None])' +[builtins fixtures/dict.pyi] + + +-- Create Instance (Errors) + +[case testCannotCreateTypedDictInstanceWithUnknownArgumentPattern] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(42, 1337) # E: Expected keyword arguments, {...}, or dict(...) in TypedDict constructor +[builtins fixtures/dict.pyi] + +[case testCannotCreateTypedDictInstanceNonLiteralItemName] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +x = 'x' +p = Point({x: 42, 'y': 1337}) # E: Expected TypedDict item name to be string literal +[builtins fixtures/dict.pyi] + +[case testCannotCreateTypedDictInstanceWithExtraItems] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(x=42, y=1337, z=666) # E: Expected items ['x', 'y'] but found ['x', 'y', 'z']. +[builtins fixtures/dict.pyi] + +[case testCannotCreateTypedDictInstanceWithMissingItems] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(x=42) # E: Expected items ['x', 'y'] but found ['x']. +[builtins fixtures/dict.pyi] + +[case testCannotCreateTypedDictInstanceWithIncompatibleItemType] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +p = Point(x='meaning_of_life', y=1337) # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int") +[builtins fixtures/dict.pyi] + + +-- Subtyping + +[case testCanConvertTypedDictToItself] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +def identity(p: Point) -> Point: + return p +[builtins fixtures/dict.pyi] + +[case testCanConvertTypedDictToEquivalentTypedDict] +from mypy_extensions import TypedDict +PointA = TypedDict('PointA', {'x': int, 'y': int}) +PointB = TypedDict('PointB', {'x': int, 'y': int}) +def identity(p: PointA) -> PointB: + return p +[builtins fixtures/dict.pyi] + +[case testCannotConvertTypedDictToSimilarTypedDictWithNarrowerItemTypes] +# flags: --hide-error-context +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +ObjectPoint = TypedDict('ObjectPoint', {'x': object, 'y': object}) +def convert(op: ObjectPoint) -> Point: + return op # E: Incompatible return value type (got "ObjectPoint", expected "Point") +[builtins fixtures/dict.pyi] + +[case testCannotConvertTypedDictToSimilarTypedDictWithWiderItemTypes] +# flags: --hide-error-context +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +ObjectPoint = TypedDict('ObjectPoint', {'x': object, 'y': object}) +def convert(p: Point) -> ObjectPoint: + return p # E: Incompatible return value type (got "Point", expected "ObjectPoint") +[builtins fixtures/dict.pyi] + +[case testCannotConvertTypedDictToSimilarTypedDictWithIncompatibleItemTypes] +# flags: --hide-error-context +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +Chameleon = TypedDict('Chameleon', {'x': str, 'y': str}) +def convert(p: Point) -> Chameleon: + return p # E: Incompatible return value type (got "Point", expected "Chameleon") +[builtins fixtures/dict.pyi] + +[case testCanConvertTypedDictToNarrowerTypedDict] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +Point1D = TypedDict('Point1D', {'x': int}) +def narrow(p: Point) -> Point1D: + return p +[builtins fixtures/dict.pyi] + +[case testCannotConvertTypedDictToWiderTypedDict] +# flags: --hide-error-context +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +Point3D = TypedDict('Point3D', {'x': int, 'y': int, 'z': int}) +def widen(p: Point) -> Point3D: + return p # E: Incompatible return value type (got "Point", expected "Point3D") +[builtins fixtures/dict.pyi] + +[case testCanConvertTypedDictToCompatibleMapping] +from mypy_extensions import TypedDict +from typing import Mapping +Point = TypedDict('Point', {'x': int, 'y': int}) +def as_mapping(p: Point) -> Mapping[str, int]: + return p +[builtins fixtures/dict.pyi] + +[case testCannotConvertTypedDictToCompatibleMapping] +# flags: --hide-error-context +from mypy_extensions import TypedDict +from typing import Mapping +Point = TypedDict('Point', {'x': int, 'y': int}) +def as_mapping(p: Point) -> Mapping[str, str]: + return p # E: Incompatible return value type (got "Point", expected Mapping[str, str]) +[builtins fixtures/dict.pyi] + +-- TODO: Fix mypy stubs so that the following passes in the test suite +--[case testCanConvertTypedDictToAnySuperclassOfMapping] +--from mypy_extensions import TypedDict +--from typing import Sized, Iterable, Container +--Point = TypedDict('Point', {'x': int, 'y': int}) +--def as_sized(p: Point) -> Sized: +-- return p +--def as_iterable(p: Point) -> Iterable[str]: +-- return p +--def as_container(p: Point) -> Container[str]: +-- return p +--def as_object(p: Point) -> object: +-- return p +--[builtins fixtures/dict.pyi] + +[case testCannotConvertTypedDictToDictOrMutableMapping] +# flags: --hide-error-context +from mypy_extensions import TypedDict +from typing import Dict, MutableMapping +Point = TypedDict('Point', {'x': int, 'y': int}) +def as_dict(p: Point) -> Dict[str, int]: + return p # E: Incompatible return value type (got "Point", expected Dict[str, int]) +def as_mutable_mapping(p: Point) -> MutableMapping[str, int]: + return p # E: Incompatible return value type (got "Point", expected MutableMapping[str, int]) +[builtins fixtures/dict.pyi] + +[case testCanConvertTypedDictToAny] +from mypy_extensions import TypedDict +from typing import Any +Point = TypedDict('Point', {'x': int, 'y': int}) +def unprotect(p: Point) -> Any: + return p +[builtins fixtures/dict.pyi] + + +-- Join + +[case testJoinOfTypedDictHasOnlyCommonKeysAndNewFallback] +from mypy_extensions import TypedDict +TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +Point3D = TypedDict('Point3D', {'x': int, 'y': int, 'z': int}) +p1 = TaggedPoint(type='2d', x=0, y=0) +p2 = Point3D(x=1, y=1, z=1) +joined_points = [p1, p2] +reveal_type(p1) # E: Revealed type is 'TypedDict(type=builtins.str, x=builtins.int, y=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.object])' +reveal_type(p2) # E: Revealed type is 'TypedDict(x=builtins.int, y=builtins.int, z=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(joined_points) # E: Revealed type is 'builtins.list[TypedDict(x=builtins.int, y=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])]' +[builtins fixtures/dict.pyi] + +[case testJoinOfTypedDictRemovesNonequivalentKeys] +from mypy_extensions import TypedDict +CellWithInt = TypedDict('CellWithInt', {'value': object, 'meta': int}) +CellWithObject = TypedDict('CellWithObject', {'value': object, 'meta': object}) +c1 = CellWithInt(value=1, meta=42) +c2 = CellWithObject(value=2, meta='turtle doves') +joined_cells = [c1, c2] +reveal_type(c1) # E: Revealed type is 'TypedDict(value=builtins.int, meta=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(c2) # E: Revealed type is 'TypedDict(value=builtins.int, meta=builtins.str, _fallback=typing.Mapping[builtins.str, builtins.object])' +reveal_type(joined_cells) # E: Revealed type is 'builtins.list[TypedDict(value=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])]' +[builtins fixtures/dict.pyi] + +[case testJoinOfDisjointTypedDictsIsEmptyTypedDict] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int}) +Cell = TypedDict('Cell', {'value': object}) +d1 = Point(x=0, y=0) +d2 = Cell(value='pear tree') +joined_dicts = [d1, d2] +reveal_type(d1) # E: Revealed type is 'TypedDict(x=builtins.int, y=builtins.int, _fallback=typing.Mapping[builtins.str, builtins.int])' +reveal_type(d2) # E: Revealed type is 'TypedDict(value=builtins.str, _fallback=typing.Mapping[builtins.str, builtins.str])' +reveal_type(joined_dicts) # E: Revealed type is 'builtins.list[TypedDict(_fallback=typing.Mapping[builtins.str, builtins.None])]' +[builtins fixtures/dict.pyi] + +[case testJoinOfTypedDictWithCompatibleMappingIsMapping] +from mypy_extensions import TypedDict +from typing import Mapping +Cell = TypedDict('Cell', {'value': int}) +left = Cell(value=42) +right = {'score': 999} # type: Mapping[str, int] +joined1 = [left, right] +joined2 = [right, left] +reveal_type(joined1) # E: Revealed type is 'builtins.list[typing.Mapping*[builtins.str, builtins.int]]' +reveal_type(joined2) # E: Revealed type is 'builtins.list[typing.Mapping*[builtins.str, builtins.int]]' +[builtins fixtures/dict.pyi] + +-- TODO: Fix mypy stubs so that the following passes in the test suite +--[case testJoinOfTypedDictWithCompatibleMappingSupertypeIsSupertype] +--from mypy_extensions import TypedDict +--from typing import Sized +--Cell = TypedDict('Cell', {'value': int}) +--left = Cell(value=42) +--right = {'score': 999} # type: Sized +--joined1 = [left, right] +--joined2 = [right, left] +--reveal_type(joined1) # E: Revealed type is 'builtins.list[typing.Sized*]' +--reveal_type(joined2) # E: Revealed type is 'builtins.list[typing.Sized*]' +--[builtins fixtures/dict.pyi] + +[case testJoinOfTypedDictWithIncompatibleMappingIsObject] +from mypy_extensions import TypedDict +from typing import Mapping +Cell = TypedDict('Cell', {'value': int}) +left = Cell(value=42) +right = {'score': 'zero'} # type: Mapping[str, str] +joined1 = [left, right] +joined2 = [right, left] +reveal_type(joined1) # E: Revealed type is 'builtins.list[builtins.object*]' +reveal_type(joined2) # E: Revealed type is 'builtins.list[builtins.object*]' +[builtins fixtures/dict.pyi] + +[case testJoinOfTypedDictWithIncompatibleTypeIsObject] +from mypy_extensions import TypedDict +from typing import Mapping +Cell = TypedDict('Cell', {'value': int}) +left = Cell(value=42) +right = 42 +joined1 = [left, right] +joined2 = [right, left] +reveal_type(joined1) # E: Revealed type is 'builtins.list[builtins.object*]' +reveal_type(joined2) # E: Revealed type is 'builtins.list[builtins.object*]' +[builtins fixtures/dict.pyi] + + +-- Meet + +[case testMeetOfTypedDictsWithCompatibleCommonKeysHasAllKeysAndNewFallback] +from mypy_extensions import TypedDict +from typing import TypeVar, Callable +XY = TypedDict('XY', {'x': int, 'y': int}) +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])' +[builtins fixtures/dict.pyi] + +[case testMeetOfTypedDictsWithIncompatibleCommonKeysIsUninhabited] +# flags: --strict-optional +from mypy_extensions import TypedDict +from typing import TypeVar, Callable +XYa = TypedDict('XYa', {'x': int, 'y': int}) +YbZ = TypedDict('YbZ', {'y': object, 'z': int}) +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: XYa, y: YbZ) -> None: pass +reveal_type(f(g)) # E: Revealed type is '' +[builtins fixtures/dict.pyi] + +[case testMeetOfTypedDictsWithNoCommonKeysHasAllKeysAndNewFallback] +from mypy_extensions import TypedDict +from typing import TypeVar, Callable +X = TypedDict('X', {'x': int}) +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])' +[builtins fixtures/dict.pyi] + +# TODO: It would be more accurate for the meet to be TypedDict instead. +[case testMeetOfTypedDictWithCompatibleMappingIsUninhabitedForNow] +# flags: --strict-optional +from mypy_extensions import TypedDict +from typing import TypeVar, Callable, Mapping +X = TypedDict('X', {'x': int}) +M = Mapping[str, int] +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: X, y: M) -> None: pass +reveal_type(f(g)) # E: Revealed type is '' +[builtins fixtures/dict.pyi] + +[case testMeetOfTypedDictWithIncompatibleMappingIsUninhabited] +# flags: --strict-optional +from mypy_extensions import TypedDict +from typing import TypeVar, Callable, Mapping +X = TypedDict('X', {'x': int}) +M = Mapping[str, str] +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: X, y: M) -> None: pass +reveal_type(f(g)) # E: Revealed type is '' +[builtins fixtures/dict.pyi] + +# TODO: It would be more accurate for the meet to be TypedDict instead. +[case testMeetOfTypedDictWithCompatibleMappingSuperclassIsUninhabitedForNow] +# flags: --strict-optional +from mypy_extensions import TypedDict +from typing import TypeVar, Callable, Iterable +X = TypedDict('X', {'x': int}) +I = Iterable[str] +T = TypeVar('T') +def f(x: Callable[[T, T], None]) -> T: pass +def g(x: X, y: I) -> None: pass +reveal_type(f(g)) # E: Revealed type is '' +[builtins fixtures/dict.pyi] + + +-- Constraint Solver + +-- TODO: Figure out some way to trigger the ConstraintBuilderVisitor.visit_typeddict_type() path. + + +-- Methods + +-- TODO: iter() doesn't accept TypedDictType as an argument type. Figure out why. +--[case testCanCallMappingMethodsOnTypedDict] +--from mypy_extensions import TypedDict +--Cell = TypedDict('Cell', {'value': int}) +--c = Cell(value=42) +--c['value'] +--iter(c) +--len(c) +--'value' in c +--c.keys() +--c.items() +--c.values() +--c.get('value') +--c == c +--c != c +--[builtins fixtures/dict.pyi] + + +-- Special Method: __getitem__ + +-- TODO: Implement support for this case. +--[case testCanGetItemOfTypedDictWithValidStringLiteralKey] +--from mypy_extensions import TypedDict +--TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +--p = TaggedPoint(type='2d', x=42, y=1337) +--def get_x(p: TaggedPoint) -> int: +-- return p['x'] +--[builtins fixtures/dict.pyi] + +-- TODO: Implement support for this case. +--[case testCannotGetItemOfTypedDictWithInvalidStringLiteralKey] +--# flags: --hide-error-context +--from mypy_extensions import TypedDict +--TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +--p = TaggedPoint(type='2d', x=42, y=1337) +--def get_z(p: TaggedPoint) -> int: +-- return p['z'] # E: ... 'z' is not a valid key for Point. Expected one of {'x', 'y'}. +--[builtins fixtures/dict.pyi] + +-- TODO: Implement support for this case. +--[case testCannotGetItemOfTypedDictWithNonLiteralKey] +--# flags: --hide-error-context +--from mypy_extensions import TypedDict +--from typing import Union +--TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +--p = TaggedPoint(type='2d', x=42, y=1337) +--def get_coordinate(p: TaggedPoint, key: str) -> Union[str, int]: +-- return p[key] # E: ... Cannot prove 'key' is a valid key for Point. Expected one of {'x', 'y'} +--[builtins fixtures/dict.pyi] + + +-- Special Method: __setitem__ + +-- TODO: Implement support for this case. +--[case testCanSetItemOfTypedDictWithValidStringLiteralKey] +--from mypy_extensions import TypedDict +--TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +--p = TaggedPoint(type='2d', x=42, y=1337) +--def set_x(p: TaggedPoint, x: int) -> None: +-- p['x'] = x +--[builtins fixtures/dict.pyi] + +-- TODO: Implement support for this case. +--[case testCannotSetItemOfTypedDictWithInvalidStringLiteralKey] +--# flags: --hide-error-context +--from mypy_extensions import TypedDict +--TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +--p = TaggedPoint(type='2d', x=42, y=1337) +--def set_z(p: TaggedPoint, z: int) -> None: +-- p['z'] = z # E: ... 'z' is not a valid key for Point. Expected one of {'x', 'y'}. +--[builtins fixtures/dict.pyi] + +-- TODO: Implement support for this case. +--[case testCannotSetItemOfTypedDictWithNonLiteralKey] +--# flags: --hide-error-context +--from mypy_extensions import TypedDict +--from typing import Union +--TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +--p = TaggedPoint(type='2d', x=42, y=1337) +--def set_coordinate(p: TaggedPoint, key: str, value: Union[str, int]) -> None: +-- p[key] = value # E: ... Cannot prove 'key' is a valid key for Point. Expected one of {'x', 'y'} +--[builtins fixtures/dict.pyi] + + +-- Special Method: get + +-- TODO: Implement support for these cases: +--[case testGetOfTypedDictWithValidStringLiteralKeyReturnsPreciseType] +--[case testGetOfTypedDictWithInvalidStringLiteralKeyIsError] +--[case testGetOfTypedDictWithNonLiteralKeyReturnsImpreciseType] + + +-- isinstance + +-- TODO: Implement support for this case. +--[case testCannotIsInstanceTypedDictType] diff --git a/test-data/unit/lib-stub/mypy_extensions.pyi b/test-data/unit/lib-stub/mypy_extensions.pyi index 6c57954d8a81..2bfc072b940e 100644 --- a/test-data/unit/lib-stub/mypy_extensions.pyi +++ b/test-data/unit/lib-stub/mypy_extensions.pyi @@ -3,4 +3,4 @@ from typing import Dict, Type, TypeVar T = TypeVar('T') -def TypedDict(typename: str, fields: Dict[str, Type[T]]) -> Type[dict]: ... +def TypedDict(typename: str, fields: Dict[str, Type[T]]) -> Type[dict]: pass diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 56a07e8fb47c..77a7b349e4cd 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -80,6 +80,8 @@ class Sequence(Iterable[T], Generic[T]): class Mapping(Generic[T, U]): pass +class MutableMapping(Generic[T, U]): pass + def NewType(name: str, tp: Type[T]) -> Callable[[T], T]: def new_type(x): return x diff --git a/test-data/unit/semanal-namedtuple.test b/test-data/unit/semanal-namedtuple.test index b590ee9bdd7d..a820a07fe745 100644 --- a/test-data/unit/semanal-namedtuple.test +++ b/test-data/unit/semanal-namedtuple.test @@ -154,6 +154,10 @@ N = namedtuple('N', 1) # E: List or tuple literal expected as the second argumen from collections import namedtuple N = namedtuple('N', ['x', 1]) # E: String literal expected as namedtuple() item +[case testNamedTupleWithUnderscoreItemName] +from collections import namedtuple +N = namedtuple('N', ['_fallback']) # E: namedtuple() field names cannot start with an underscore: _fallback + -- NOTE: The following code works at runtime but is not yet supported by mypy. -- Keyword arguments may potentially be supported in the future. [case testNamedTupleWithNonpositionalArgs] diff --git a/test-data/unit/semanal-typeddict.test b/test-data/unit/semanal-typeddict.test index 98aef1a5ef1e..a0229d82a9ed 100644 --- a/test-data/unit/semanal-typeddict.test +++ b/test-data/unit/semanal-typeddict.test @@ -1,6 +1,30 @@ --- Semantic analysis of typed dicts +-- Create Type -[case testCanDefineTypedDictType] +-- TODO: Implement support for this syntax. +--[case testCanCreateTypedDictTypeWithKeywordArguments] +--from mypy_extensions import TypedDict +--Point = TypedDict('Point', x=int, y=int) +--[builtins fixtures/dict.pyi] +--[out] +--MypyFile:1( +-- ImportFrom:1(mypy_extensions, [TypedDict]) +-- AssignmentStmt:2( +-- NameExpr(Point* [__main__.Point]) +-- TypedDictExpr:2(Point))) + +-- TODO: Implement support for this syntax. +--[case testCanCreateTypedDictTypeWithDictCall] +--from mypy_extensions import TypedDict +--Point = TypedDict('Point', dict(x=int, y=int)) +--[builtins fixtures/dict.pyi] +--[out] +--MypyFile:1( +-- ImportFrom:1(mypy_extensions, [TypedDict]) +-- AssignmentStmt:2( +-- NameExpr(Point* [__main__.Point]) +-- TypedDictExpr:2(Point))) + +[case testCanCreateTypedDictTypeWithDictLiteral] from mypy_extensions import TypedDict Point = TypedDict('Point', {'x': int, 'y': int}) [builtins fixtures/dict.pyi] @@ -11,41 +35,47 @@ MypyFile:1( NameExpr(Point* [__main__.Point]) TypedDictExpr:2(Point))) --- Errors -[case testTypedDictWithTooFewArguments] +-- Create Type (Errors) + +[case testCannotCreateTypedDictTypeWithTooFewArguments] from mypy_extensions import TypedDict Point = TypedDict('Point') # E: Too few arguments for TypedDict() [builtins fixtures/dict.pyi] -[case testTypedDictWithTooManyArguments] +[case testCannotCreateTypedDictTypeWithTooManyArguments] from mypy_extensions import TypedDict Point = TypedDict('Point', {'x': int, 'y': int}, dict) # E: Too many arguments for TypedDict() [builtins fixtures/dict.pyi] -[case testTypedDictWithInvalidName] +[case testCannotCreateTypedDictTypeWithInvalidName] from mypy_extensions import TypedDict Point = TypedDict(dict, {'x': int, 'y': int}) # E: TypedDict() expects a string literal as the first argument [builtins fixtures/dict.pyi] -[case testTypedDictWithInvalidItems] +[case testCannotCreateTypedDictTypeWithInvalidItems] from mypy_extensions import TypedDict Point = TypedDict('Point', {'x'}) # E: TypedDict() expects a dictionary literal as the second argument [builtins fixtures/dict.pyi] +[case testCannotCreateTypedDictTypeWithUnderscoreItemName] +from mypy_extensions import TypedDict +Point = TypedDict('Point', {'x': int, 'y': int, '_fallback': object}) # E: TypedDict() item names cannot start with an underscore: _fallback +[builtins fixtures/dict.pyi] + -- NOTE: The following code works at runtime but is not yet supported by mypy. -- Keyword arguments may potentially be supported in the future. -[case testTypedDictWithNonpositionalArgs] +[case testCannotCreateTypedDictTypeWithNonpositionalArgs] from mypy_extensions import TypedDict Point = TypedDict(typename='Point', fields={'x': int, 'y': int}) # E: Unexpected arguments to TypedDict() [builtins fixtures/dict.pyi] -[case testTypedDictWithInvalidItemName] +[case testCannotCreateTypedDictTypeWithInvalidItemName] from mypy_extensions import TypedDict Point = TypedDict('Point', {int: int, int: int}) # E: Invalid TypedDict() field name [builtins fixtures/dict.pyi] -[case testTypedDictWithInvalidItemType] +[case testCannotCreateTypedDictTypeWithInvalidItemType] from mypy_extensions import TypedDict Point = TypedDict('Point', {'x': 1, 'y': 1}) # E: Invalid field type [builtins fixtures/dict.pyi]