From 7d3220eda34113d03a12d03b3c29b05e0a94201e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 11 Aug 2022 14:50:17 +0100 Subject: [PATCH 01/15] Basic support for generic TypedDicts --- mypy/checkexpr.py | 65 +++++++++++++++++++++++++++-- mypy/semanal.py | 6 +++ mypy/typeanal.py | 5 +-- mypy/types.py | 2 + test-data/unit/check-serialize.test | 4 +- test-data/unit/check-typeddict.test | 22 +++++++++- 6 files changed, 93 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 565e20b9c243..c746e091c324 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -318,7 +318,14 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: result = node.type elif isinstance(node, TypeInfo): # Reference to a type object. - result = type_object_type(node, self.named_type) + if node.typeddict_type: + # Use alias target to get the correct fallback (not just typing.TypedDict). + assert node.special_alias is not None + target = node.special_alias.target + assert isinstance(target, ProperType) and isinstance(target, TypedDictType) + result = self.typeddict_callable(target) + else: + result = type_object_type(node, self.named_type) if isinstance(result, CallableType) and isinstance( # type: ignore result.ret_type, Instance ): @@ -396,6 +403,20 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> fallback=Instance(e.callee.node, []) ) return self.check_typeddict_call(typeddict_type, e.arg_kinds, e.arg_names, e.args, e) + if ( + isinstance(e.callee, IndexExpr) + and isinstance(e.callee.base, RefExpr) + and isinstance(e.callee.base.node, TypeInfo) + and e.callee.base.node.typeddict_type is not None + ): + # Apply type argument form type application. + typeddict_callable = get_proper_type(self.accept(e.callee)) + if isinstance(typeddict_callable, CallableType): + typeddict_applied = get_proper_type(typeddict_callable.ret_type) + assert isinstance(typeddict_applied, TypedDictType) + return self.check_typeddict_call( + typeddict_applied, e.arg_kinds, e.arg_names, e.args, e + ) if ( isinstance(e.callee, NameExpr) and e.callee.name in ("isinstance", "issubclass") @@ -692,6 +713,24 @@ def check_typeddict_call_with_dict( else: return AnyType(TypeOfAny.from_error) + def typeddict_callable(self, typed_dict: TypedDictType) -> CallableType: + """Contsruct a reasonable type for a TypedDict type in runtime context. + + If it appears as a callee, it will be special-cased anyway, e.g. it is + also allowed to accept a single positional argument if it is a dict literal. + """ + expected_types = list(typed_dict.items.values()) + kinds = [ArgKind.ARG_NAMED] * len(expected_types) + names = list(typed_dict.items.keys()) + return CallableType( + expected_types, + kinds, + names, + typed_dict, + self.named_type("builtins.type"), + variables=typed_dict.fallback.type.defn.type_vars, + ) + def check_typeddict_call_with_kwargs( self, callee: TypedDictType, kwargs: Dict[str, Expression], context: Context ) -> Type: @@ -707,7 +746,27 @@ def check_typeddict_call_with_kwargs( ) return AnyType(TypeOfAny.from_error) - for (item_name, item_expected_type) in callee.items.items(): + # We don't show any errors, just infer types in a generic TypedDict type, + # a custom error message will be given below, if there are errors. + with self.msg.filter_errors(), self.chk.local_type_map(): + orig_ret_type, _ = self.check_callable_call( + self.typeddict_callable(callee), + list(kwargs.values()), + [ArgKind.ARG_NAMED] * len(kwargs), + context, + list(kwargs.keys()), + None, + None, + None, + ) + + ret_type = get_proper_type(orig_ret_type) + if not isinstance(ret_type, TypedDictType): + # If something went really wrong, type-check call with original type, + # this may give a better error message. + ret_type = callee + + for (item_name, item_expected_type) in ret_type.items.items(): if item_name in kwargs: item_value = kwargs[item_name] self.chk.check_simple_assignment( @@ -720,7 +779,7 @@ def check_typeddict_call_with_kwargs( code=codes.TYPEDDICT_ITEM, ) - return callee + return orig_ret_type def get_partial_self_var(self, expr: MemberExpr) -> Optional[Var]: """Get variable node for a partial self attribute. diff --git a/mypy/semanal.py b/mypy/semanal.py index 2a30783d5bdc..2bdc44a55895 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1379,6 +1379,12 @@ def analyze_class(self, defn: ClassDef) -> None: return if self.analyze_typeddict_classdef(defn): + if defn.info: + defn.type_vars = tvar_defs + defn.info.type_vars = [] + defn.info.add_type_vars() + assert defn.info.special_alias is not None + defn.info.special_alias.alias_tvars = list(defn.info.type_vars) return if self.analyze_namedtuple_classdef(defn): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index d797c8306515..567f5e17aabc 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -621,12 +621,9 @@ def analyze_type_with_type_info( if td is not None: # The class has a TypedDict[...] base class so it will be # represented as a typeddict type. - if args: - self.fail("Generic TypedDict types not supported", ctx) - return AnyType(TypeOfAny.from_error) if info.special_alias: # We don't support generic TypedDict types yet. - return TypeAliasType(info.special_alias, []) + return TypeAliasType(info.special_alias, self.anal_array(args)) # Create a named TypedDictType return td.copy_modified( item_types=self.anal_array(list(td.items.values())), fallback=instance diff --git a/mypy/types.py b/mypy/types.py index df0999ed3ca6..0f6e0cace779 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1730,6 +1730,8 @@ def type_object(self) -> mypy.nodes.TypeInfo: ret = get_proper_type(ret.upper_bound) if isinstance(ret, TupleType): ret = ret.partial_fallback + if isinstance(ret, TypedDictType): + ret = ret.fallback assert isinstance(ret, Instance) return ret.type diff --git a/test-data/unit/check-serialize.test b/test-data/unit/check-serialize.test index 0d7e9f74fa75..66d5d879ae68 100644 --- a/test-data/unit/check-serialize.test +++ b/test-data/unit/check-serialize.test @@ -1066,11 +1066,11 @@ class C: [out1] main:2: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" main:3: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" -main:4: note: Revealed type is "def () -> ntcrash.C.A@4" +main:4: note: Revealed type is "def (*, x: builtins.int) -> TypedDict('ntcrash.C.A@4', {'x': builtins.int})" [out2] main:2: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" main:3: note: Revealed type is "TypedDict('ntcrash.C.A@4', {'x': builtins.int})" -main:4: note: Revealed type is "def () -> ntcrash.C.A@4" +main:4: note: Revealed type is "def (*, x: builtins.int) -> TypedDict('ntcrash.C.A@4', {'x': builtins.int})" [case testSerializeNonTotalTypedDict] from m import d diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 62ac5e31da45..a49dcd812735 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -789,7 +789,7 @@ from mypy_extensions import TypedDict D = TypedDict('D', {'x': int}) d: object if isinstance(d, D): # E: Cannot use isinstance() with TypedDict type - reveal_type(d) # N: Revealed type is "__main__.D" + reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'x': builtins.int})" issubclass(object, D) # E: Cannot use issubclass() with TypedDict type [builtins fixtures/isinstancelist.pyi] @@ -1517,7 +1517,7 @@ from b import tp x: tp reveal_type(x['x']) # N: Revealed type is "builtins.int" -reveal_type(tp) # N: Revealed type is "def () -> b.tp" +reveal_type(tp) # N: Revealed type is "def (*, x: builtins.int) -> TypedDict('b.tp', {'x': builtins.int})" tp(x='no') # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int") [file b.py] @@ -2395,3 +2395,21 @@ def func(foo: Union[F1, F2]): # E: Argument 1 to "__setitem__" has incompatible type "int"; expected "str" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testGenericTypedDict] +from typing import TypedDict, Generic, TypeVar + +T = TypeVar("T") + +class TD(TypedDict, Generic[T]): + key: int + value: T + +tds: TD[str] +reveal_type(tds) # N: Revealed type is "TypedDict('__main__.TD', {'key': builtins.int, 'value': builtins.str})" + +tdi = TD(key=0, value=0) +reveal_type(tdi) # N: Revealed type is "TypedDict('__main__.TD', {'key': builtins.int, 'value': builtins.int})" +TD[str](key=0, value=0) # E: Incompatible types (expression has type "int", TypedDict item "value" has type "str") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From 270e82dd2dbe15ec0c0717b5da326f2da66e66a2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 11 Aug 2022 19:46:38 +0100 Subject: [PATCH 02/15] Fix inference; more tests an fixes --- mypy/checkexpr.py | 80 ++++++++++++++++------- mypy/fixup.py | 1 + test-data/unit/check-recursive-types.test | 16 +++++ test-data/unit/check-typeddict.test | 31 ++++++++- test-data/unit/deps.test | 6 ++ 5 files changed, 111 insertions(+), 23 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c746e091c324..fc07944307a2 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -319,11 +319,8 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: elif isinstance(node, TypeInfo): # Reference to a type object. if node.typeddict_type: - # Use alias target to get the correct fallback (not just typing.TypedDict). - assert node.special_alias is not None - target = node.special_alias.target - assert isinstance(target, ProperType) and isinstance(target, TypedDictType) - result = self.typeddict_callable(target) + # We special-case TypedDict, because they don't define any constructor. + result = self.typeddict_callable(node) else: result = type_object_type(node, self.named_type) if isinstance(result, CallableType) and isinstance( # type: ignore @@ -402,7 +399,9 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> typeddict_type = e.callee.node.typeddict_type.copy_modified( fallback=Instance(e.callee.node, []) ) - return self.check_typeddict_call(typeddict_type, e.arg_kinds, e.arg_names, e.args, e) + return self.check_typeddict_call( + typeddict_type, e.arg_kinds, e.arg_names, e.args, e, self.accept(e.callee) + ) if ( isinstance(e.callee, IndexExpr) and isinstance(e.callee.base, RefExpr) @@ -415,7 +414,7 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> typeddict_applied = get_proper_type(typeddict_callable.ret_type) assert isinstance(typeddict_applied, TypedDictType) return self.check_typeddict_call( - typeddict_applied, e.arg_kinds, e.arg_names, e.args, e + typeddict_applied, e.arg_kinds, e.arg_names, e.args, e, self.accept(e.callee) ) if ( isinstance(e.callee, NameExpr) @@ -648,6 +647,7 @@ def check_typeddict_call( arg_names: Sequence[Optional[str]], args: List[Expression], context: Context, + orig_callee: Optional[Type], ) -> Type: if len(args) >= 1 and all([ak == ARG_NAMED for ak in arg_kinds]): # ex: Point(x=42, y=1337) @@ -655,21 +655,25 @@ def check_typeddict_call( item_names = cast(List[str], arg_names) item_args = args return self.check_typeddict_call_with_kwargs( - callee, dict(zip(item_names, item_args)), context + callee, dict(zip(item_names, item_args)), context, orig_callee ) 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) + return self.check_typeddict_call_with_dict( + callee, unique_arg, context, orig_callee + ) 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) + return self.check_typeddict_call_with_dict( + callee, unique_arg.analyzed, context, orig_callee + ) if len(args) == 0: # ex: EmptyDict() - return self.check_typeddict_call_with_kwargs(callee, {}, context) + return self.check_typeddict_call_with_kwargs(callee, {}, context, orig_callee) self.chk.fail(message_registry.INVALID_TYPEDDICT_ARGS, context) return AnyType(TypeOfAny.from_error) @@ -703,36 +707,47 @@ def match_typeddict_call_with_dict( return False def check_typeddict_call_with_dict( - self, callee: TypedDictType, kwargs: DictExpr, context: Context + self, + callee: TypedDictType, + kwargs: DictExpr, + context: Context, + orig_callee: Optional[Type], ) -> Type: validated_kwargs = self.validate_typeddict_kwargs(kwargs=kwargs) if validated_kwargs is not None: return self.check_typeddict_call_with_kwargs( - callee, kwargs=validated_kwargs, context=context + callee, kwargs=validated_kwargs, context=context, orig_callee=orig_callee ) else: return AnyType(TypeOfAny.from_error) - def typeddict_callable(self, typed_dict: TypedDictType) -> CallableType: - """Contsruct a reasonable type for a TypedDict type in runtime context. + def typeddict_callable(self, info: TypeInfo) -> CallableType: + """Construct a reasonable type for a TypedDict type in runtime context. If it appears as a callee, it will be special-cased anyway, e.g. it is also allowed to accept a single positional argument if it is a dict literal. """ - expected_types = list(typed_dict.items.values()) + assert info.special_alias is not None + target = info.special_alias.target + assert isinstance(target, ProperType) and isinstance(target, TypedDictType) + expected_types = list(target.items.values()) kinds = [ArgKind.ARG_NAMED] * len(expected_types) - names = list(typed_dict.items.keys()) + names = list(target.items.keys()) return CallableType( expected_types, kinds, names, - typed_dict, + target, self.named_type("builtins.type"), - variables=typed_dict.fallback.type.defn.type_vars, + variables=info.defn.type_vars, ) def check_typeddict_call_with_kwargs( - self, callee: TypedDictType, kwargs: Dict[str, Expression], context: Context + self, + callee: TypedDictType, + kwargs: Dict[str, Expression], + context: Context, + orig_callee: Optional[Type], ) -> Type: if not (callee.required_keys <= set(kwargs.keys()) <= set(callee.items.keys())): expected_keys = [ @@ -746,11 +761,27 @@ def check_typeddict_call_with_kwargs( ) return AnyType(TypeOfAny.from_error) + orig_callee = get_proper_type(orig_callee) + if isinstance(orig_callee, CallableType): + infer_callee = orig_callee + else: + # Try reconstructing from type context. + if callee.fallback.type.special_alias is not None: + infer_callee = self.typeddict_callable(callee.fallback.type) + else: + infer_callee = CallableType( + list(callee.items.values()), + [ArgKind.ARG_NAMED] * len(callee.items), + list(callee.items.keys()), + callee, + self.named_type("builtins.type"), + ) + # We don't show any errors, just infer types in a generic TypedDict type, # a custom error message will be given below, if there are errors. with self.msg.filter_errors(), self.chk.local_type_map(): orig_ret_type, _ = self.check_callable_call( - self.typeddict_callable(callee), + infer_callee, list(kwargs.values()), [ArgKind.ARG_NAMED] * len(kwargs), context, @@ -3991,7 +4022,12 @@ def visit_dict_expr(self, e: DictExpr) -> Type: # to avoid the second error, we always return TypedDict type that was requested typeddict_context = self.find_typeddict_context(self.type_context[-1], e) if typeddict_context: - self.check_typeddict_call_with_dict(callee=typeddict_context, kwargs=e, context=e) + orig_ret_type = self.check_typeddict_call_with_dict( + callee=typeddict_context, kwargs=e, context=e, orig_callee=None + ) + ret_type = get_proper_type(orig_ret_type) + if isinstance(ret_type, TypedDictType): + return ret_type.copy_modified() return typeddict_context.copy_modified() # fast path attempt diff --git a/mypy/fixup.py b/mypy/fixup.py index 37e651fe05ff..1a0a5f7e67ee 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -78,6 +78,7 @@ def visit_type_info(self, info: TypeInfo) -> None: info.update_tuple_type(info.tuple_type) if info.typeddict_type: info.typeddict_type.accept(self.type_fixer) + info.update_typeddict_type(info.typeddict_type) if info.declared_metaclass: info.declared_metaclass.accept(self.type_fixer) if info.metaclass_type: diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index b5a1fe6838b5..3b5a9569cb82 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -752,3 +752,19 @@ reveal_type(f(tda1, tda2)) # N: Revealed type is "TypedDict({'x': builtins.int, reveal_type(f(tda1, tdb)) # N: Revealed type is "TypedDict({})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testBasicRecursiveGenericTypedDict] +# flags: --enable-recursive-aliases +from typing import TypedDict, TypeVar, Generic, Optional, List + +T = TypeVar("T") +class Tree(TypedDict, Generic[T], total=False): + value: T + left: Tree[T] + right: Tree[T] + +def collect(arg: Tree[T]) -> List[T]: ... + +reveal_type(collect({"left": {"right": {"value": 0}}})) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index a49dcd812735..0c8069e35eb9 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2396,7 +2396,7 @@ def func(foo: Union[F1, F2]): [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[case testGenericTypedDict] +[case testGenericTypedDictCreation] from typing import TypedDict, Generic, TypeVar T = TypeVar("T") @@ -2411,5 +2411,34 @@ reveal_type(tds) # N: Revealed type is "TypedDict('__main__.TD', {'key': builti tdi = TD(key=0, value=0) reveal_type(tdi) # N: Revealed type is "TypedDict('__main__.TD', {'key': builtins.int, 'value': builtins.int})" TD[str](key=0, value=0) # E: Incompatible types (expression has type "int", TypedDict item "value" has type "str") +TD[str]({"key": 0, "value": 0}) # E: Incompatible types (expression has type "int", TypedDict item "value" has type "str") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testGenericTypedDictInference] +from typing import TypedDict, Generic, TypeVar, List + +T = TypeVar("T") + +class TD(TypedDict, Generic[T]): + key: int + value: T + +def foo(x: TD[T]) -> List[T]: ... + +reveal_type(foo(TD(key=1, value=2))) # N: Revealed type is "builtins.list[builtins.int]" +reveal_type(foo({"key": 1, "value": 2})) # N: Revealed type is "builtins.list[builtins.int]" +reveal_type(foo(dict(key=1, value=2))) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testGenericTypedDictExtending] +from typing import TypedDict, Generic, TypeVar, List + +T = TypeVar("T") + +class TD(TypedDict, Generic[T]): + key: int + value: T [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 0714940246e5..28d51f1a4c30 100644 --- a/test-data/unit/deps.test +++ b/test-data/unit/deps.test @@ -650,6 +650,8 @@ def foo(x: Point) -> int: return x['x'] + x['y'] [builtins fixtures/dict.pyi] [out] + -> m + -> m -> , , m, m.foo -> m @@ -665,6 +667,8 @@ def foo(x: Point) -> int: -> m -> m -> , , , m, m.A, m.foo + -> m + -> m -> , , m, m.foo -> m @@ -682,6 +686,8 @@ def foo(x: Point) -> int: -> m -> m -> , , , m, m.A, m.foo + -> m + -> m -> , , m, m.Point, m.foo -> m From 68744d591b0fc9be025016943c63d3e595e5cf1e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 11 Aug 2022 21:53:27 +0100 Subject: [PATCH 03/15] Add support for extending generic TypedDicts --- mypy/semanal.py | 3 + mypy/semanal_typeddict.py | 241 +++++++++++++++------- test-data/unit/check-recursive-types.test | 18 ++ test-data/unit/check-typeddict.test | 14 +- 4 files changed, 206 insertions(+), 70 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 2bdc44a55895..586266de2074 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -268,6 +268,7 @@ TupleType, Type, TypeAliasType, + TypedDictType, TypeOfAny, TypeType, TypeVarLikeType, @@ -1821,6 +1822,8 @@ def configure_base_classes( msg = 'Class cannot subclass value of type "Any"' self.fail(msg, base_expr) info.fallback_to_any = True + elif isinstance(base, TypedDictType): + base_types.append(base.fallback) else: msg = "Invalid base class" name = self.get_name_repr_of_expr(base_expr) diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 2261df76acb3..23e814f9479b 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -1,6 +1,6 @@ """Semantic analysis of TypedDict definitions.""" -from typing import List, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple from typing_extensions import Final from mypy import errorcodes as codes @@ -18,18 +18,28 @@ EllipsisExpr, Expression, ExpressionStmt, + IndexExpr, NameExpr, PassStmt, RefExpr, StrExpr, TempNode, + TupleExpr, TypedDictExpr, TypeInfo, ) from mypy.options import Options from mypy.semanal_shared import SemanticAnalyzerInterface, has_placeholder from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type -from mypy.types import TPDICT_NAMES, AnyType, RequiredType, Type, TypedDictType, TypeOfAny +from mypy.types import ( + TPDICT_NAMES, + AnyType, + RequiredType, + Type, + TypedDictType, + TypeOfAny, + replace_alias_tvars, +) TPDICT_CLASS_ERROR: Final = ( "Invalid statement in TypedDict definition; " 'expected "field_name: field_type"' @@ -61,84 +71,177 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[Typ """ possible = False for base_expr in defn.base_type_exprs: + if isinstance(base_expr, IndexExpr): + base_expr = base_expr.base if isinstance(base_expr, RefExpr): self.api.accept(base_expr) if base_expr.fullname in TPDICT_NAMES or self.is_typeddict(base_expr): possible = True - if possible: - existing_info = None - if isinstance(defn.analyzed, TypedDictExpr): - existing_info = defn.analyzed.info - if ( - len(defn.base_type_exprs) == 1 - and isinstance(defn.base_type_exprs[0], RefExpr) - and defn.base_type_exprs[0].fullname in TPDICT_NAMES - ): - # Building a new TypedDict - fields, types, required_keys = self.analyze_typeddict_classdef_fields(defn) - if fields is None: - return True, None # Defer - info = self.build_typeddict_typeinfo( - defn.name, fields, types, required_keys, defn.line, existing_info - ) - defn.analyzed = TypedDictExpr(info) - defn.analyzed.line = defn.line - defn.analyzed.column = defn.column - return True, info - - # Extending/merging existing TypedDicts - typeddict_bases = [] - typeddict_bases_set = set() - for expr in defn.base_type_exprs: - if isinstance(expr, RefExpr) and expr.fullname in TPDICT_NAMES: - if "TypedDict" not in typeddict_bases_set: - typeddict_bases_set.add("TypedDict") - else: - self.fail('Duplicate base class "TypedDict"', defn) - elif isinstance(expr, RefExpr) and self.is_typeddict(expr): - assert expr.fullname - if expr.fullname not in typeddict_bases_set: - typeddict_bases_set.add(expr.fullname) - typeddict_bases.append(expr) - else: - assert isinstance(expr.node, TypeInfo) - self.fail(f'Duplicate base class "{expr.node.name}"', defn) - else: - self.fail("All bases of a new TypedDict must be TypedDict types", defn) - - keys: List[str] = [] - types = [] - required_keys = set() - # Iterate over bases in reverse order so that leftmost base class' keys take precedence - for base in reversed(typeddict_bases): - assert isinstance(base, RefExpr) - assert isinstance(base.node, TypeInfo) - assert isinstance(base.node.typeddict_type, TypedDictType) - base_typed_dict = base.node.typeddict_type - base_items = base_typed_dict.items - valid_items = base_items.copy() - for key in base_items: - if key in keys: - self.fail(f'Overwriting TypedDict field "{key}" while merging', defn) - keys.extend(valid_items.keys()) - types.extend(valid_items.values()) - required_keys.update(base_typed_dict.required_keys) - new_keys, new_types, new_required_keys = self.analyze_typeddict_classdef_fields( - defn, keys - ) - if new_keys is None: + if not possible: + return False, None + existing_info = None + if isinstance(defn.analyzed, TypedDictExpr): + existing_info = defn.analyzed.info + if ( + len(defn.base_type_exprs) == 1 + and isinstance(defn.base_type_exprs[0], RefExpr) + and defn.base_type_exprs[0].fullname in TPDICT_NAMES + ): + # Building a new TypedDict + fields, types, required_keys = self.analyze_typeddict_classdef_fields(defn) + if fields is None: return True, None # Defer - keys.extend(new_keys) - types.extend(new_types) - required_keys.update(new_required_keys) info = self.build_typeddict_typeinfo( - defn.name, keys, types, required_keys, defn.line, existing_info + defn.name, fields, types, required_keys, defn.line, existing_info ) defn.analyzed = TypedDictExpr(info) defn.analyzed.line = defn.line defn.analyzed.column = defn.column return True, info - return False, None + + # Extending/merging existing TypedDicts + typeddict_bases: List[Expression] = [] + typeddict_bases_set = set() + for expr in defn.base_type_exprs: + if isinstance(expr, RefExpr) and expr.fullname in TPDICT_NAMES: + if "TypedDict" not in typeddict_bases_set: + typeddict_bases_set.add("TypedDict") + else: + self.fail('Duplicate base class "TypedDict"', defn) + elif isinstance(expr, RefExpr) and self.is_typeddict(expr): + assert expr.fullname + if expr.fullname not in typeddict_bases_set: + typeddict_bases_set.add(expr.fullname) + typeddict_bases.append(expr) + else: + assert isinstance(expr.node, TypeInfo) + self.fail(f'Duplicate base class "{expr.node.name}"', defn) + elif isinstance(expr, IndexExpr) and self.is_typeddict(expr.base): + assert isinstance(expr.base, RefExpr) + assert expr.base.fullname + if expr.base.fullname not in typeddict_bases_set: + typeddict_bases_set.add(expr.base.fullname) + typeddict_bases.append(expr) + else: + assert isinstance(expr.base.node, TypeInfo) + self.fail(f'Duplicate base class "{expr.base.node.name}"', defn) + else: + self.fail("All bases of a new TypedDict must be TypedDict types", defn) + + keys: List[str] = [] + types = [] + required_keys = set() + # Iterate over bases in reverse order so that leftmost base class' keys take precedence + for base in reversed(typeddict_bases): + self.add_keys_and_types_from_base(base, keys, types, required_keys, defn) + new_keys, new_types, new_required_keys = self.analyze_typeddict_classdef_fields(defn, keys) + if new_keys is None: + return True, None # Defer + keys.extend(new_keys) + types.extend(new_types) + required_keys.update(new_required_keys) + info = self.build_typeddict_typeinfo( + defn.name, keys, types, required_keys, defn.line, existing_info + ) + defn.analyzed = TypedDictExpr(info) + defn.analyzed.line = defn.line + defn.analyzed.column = defn.column + return True, info + + def add_keys_and_types_from_base( + self, + base: Expression, + keys: List[str], + types: List[Type], + required_keys: Set[str], + ctx: Context, + ) -> None: + if isinstance(base, RefExpr): + assert isinstance(base.node, TypeInfo) + info = base.node + base_args: List[Type] = [] + else: + assert isinstance(base, IndexExpr) + assert isinstance(base.base, RefExpr) + assert isinstance(base.base.node, TypeInfo) + info = base.base.node + args = self.analyze_base_args(base, ctx) + if args is None: + return + base_args = args + + assert info.typeddict_type is not None + base_typed_dict = info.typeddict_type + base_items = base_typed_dict.items + valid_items = base_items.copy() + + # Always fix invalid bases to avoid crashes. + tvars = info.type_vars + if len(base_args) != len(tvars): + any_kind = TypeOfAny.from_omitted_generics + if base_args: + self.fail(f'Invalid number of type arguments for "{info.name}"', ctx) + any_kind = TypeOfAny.from_error + base_args = [AnyType(any_kind) for _ in tvars] + + valid_items = self.map_items_to_base(valid_items, tvars, base_args) + for key in base_items: + if key in keys: + self.fail(f'Overwriting TypedDict field "{key}" while merging', ctx) + keys.extend(valid_items.keys()) + types.extend(valid_items.values()) + required_keys.update(base_typed_dict.required_keys) + + def analyze_base_args(self, base: IndexExpr, ctx: Context) -> Optional[List[Type]]: + """Analyze arguments of base type expressions as types. + + We need to do this, because normal base class processing happens after + the TypedDict special-casing (plus we get a custom error message). + """ + base_args = [] + if isinstance(base.index, TupleExpr): + args = base.index.items + else: + args = [base.index] + + for arg_expr in args: + try: + type = expr_to_unanalyzed_type(arg_expr, self.options, self.api.is_stub_file) + except TypeTranslationError: + self.fail("Invalid TypedDict type argument", ctx) + return None + analyzed = self.api.anal_type( + type, + allow_required=True, + allow_placeholder=self.options.enable_recursive_aliases + and not self.api.is_func_scope(), + ) + if analyzed is None: + return None + base_args.append(analyzed) + return base_args + + def map_items_to_base( + self, valid_items: Dict[str, Type], tvars: List[str], base_args: List[Type] + ) -> Dict[str, Type]: + """Map item types to how they would look in their base with type arguments applied. + + We would normally use expand_type() for such task, but we can't use it during + semantic analysis, because it can (indirectly) call is_subtype() etc., and it + will crash on placeholder types. So we hijack replace_alias_tvars() that was initially + intended to deal with eager expansion of generic type aliases during semantic analysis. + """ + mapped_items = {} + for key in valid_items: + type_in_base = valid_items[key] + if not tvars: + mapped_items[key] = type_in_base + continue + mapped_type = replace_alias_tvars( + type_in_base, tvars, base_args, type_in_base.line, type_in_base.column + ) + mapped_items[key] = mapped_type + return mapped_items def analyze_typeddict_classdef_fields( self, defn: ClassDef, oldfields: Optional[List[str]] = None diff --git a/test-data/unit/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 3b5a9569cb82..f9a0fea71f06 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -768,3 +768,21 @@ def collect(arg: Tree[T]) -> List[T]: ... reveal_type(collect({"left": {"right": {"value": 0}}})) # N: Revealed type is "builtins.list[builtins.int]" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testRecursiveGenericTypedDictExtending] +# flags: --enable-recursive-aliases +from typing import TypedDict, Generic, TypeVar, List + +T = TypeVar("T") + +class TD(TypedDict, Generic[T]): + val: T + other: STD[T] +class STD(TD[T]): + sval: T + one: TD[T] + +std: STD[str] +reveal_type(std) # N: Revealed type is "TypedDict('__main__.STD', {'val': builtins.str, 'other': ..., 'sval': builtins.str, 'one': TypedDict('__main__.TD', {'val': builtins.str, 'other': ...})})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 0c8069e35eb9..d8ae892bb918 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2436,9 +2436,21 @@ reveal_type(foo(dict(key=1, value=2))) # N: Revealed type is "builtins.list[bui from typing import TypedDict, Generic, TypeVar, List T = TypeVar("T") - class TD(TypedDict, Generic[T]): key: int value: T + +S = TypeVar("S") +class STD(TD[List[S]]): + other: S + +std: STD[str] +reveal_type(std) # N: Revealed type is "TypedDict('__main__.STD', {'key': builtins.int, 'value': builtins.list[builtins.str], 'other': builtins.str})" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testGenericTypedDictErrors] +from typing import TypedDict, Generic, TypeVar, List + [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] From 8801b878619a27d390689fb8734cf31f2079da2d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 11 Aug 2022 22:43:34 +0100 Subject: [PATCH 04/15] Add test for errors --- test-data/unit/check-typeddict.test | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index d8ae892bb918..582ca65fe76a 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2449,8 +2449,28 @@ reveal_type(std) # N: Revealed type is "TypedDict('__main__.STD', {'key': built [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] -[case testGenericTypedDictErrors] -from typing import TypedDict, Generic, TypeVar, List +[case testGenericTypedDictExtendingErrors] +from typing import TypedDict, Generic, TypeVar +T = TypeVar("T") +class Base(TypedDict, Generic[T]): + x: T +class Sub(Base[{}]): # E: Invalid TypedDict type argument \ + # E: Type expected within [...] \ + # E: Invalid base class "Base" + y: int +s: Sub +reveal_type(s) # N: Revealed type is "TypedDict('__main__.Sub', {'y': builtins.int})" + +class Sub2(Base[int, str]): # E: Invalid number of type arguments for "Base" \ + # E: "Base" expects 1 type argument, but 2 given + y: int +s2: Sub2 +reveal_type(s2) # N: Revealed type is "TypedDict('__main__.Sub2', {'x': Any, 'y': builtins.int})" + +class Sub3(Base): # OK + y: int +s3: Sub3 +reveal_type(s3) # N: Revealed type is "TypedDict('__main__.Sub3', {'x': Any, 'y': builtins.int})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] From 48af44ded97ec35bed2e7201c58258a983527e99 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 11 Aug 2022 22:54:40 +0100 Subject: [PATCH 05/15] Minor tweaks --- misc/proper_plugin.py | 1 + mypy/checkexpr.py | 7 +++---- mypy/semanal_typeddict.py | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/misc/proper_plugin.py b/misc/proper_plugin.py index 4f8af8d301a3..75b36bdf10f4 100644 --- a/misc/proper_plugin.py +++ b/misc/proper_plugin.py @@ -95,6 +95,7 @@ def is_special_target(right: ProperType) -> bool: "mypy.types.PartialType", "mypy.types.ErasedType", "mypy.types.DeletedType", + "mypy.types.RequiredType", ): # Special case: these are not valid targets for a type alias and thus safe. # TODO: introduce a SyntheticType base to simplify this? diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index fc07944307a2..f6a6695df2cb 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -408,13 +408,12 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> and isinstance(e.callee.base.node, TypeInfo) and e.callee.base.node.typeddict_type is not None ): - # Apply type argument form type application. typeddict_callable = get_proper_type(self.accept(e.callee)) if isinstance(typeddict_callable, CallableType): - typeddict_applied = get_proper_type(typeddict_callable.ret_type) - assert isinstance(typeddict_applied, TypedDictType) + typeddict_type = get_proper_type(typeddict_callable.ret_type) + assert isinstance(typeddict_type, TypedDictType) return self.check_typeddict_call( - typeddict_applied, e.arg_kinds, e.arg_names, e.args, e, self.accept(e.callee) + typeddict_type, e.arg_kinds, e.arg_names, e.args, e, typeddict_callable ) if ( isinstance(e.callee, NameExpr) diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 23e814f9479b..e11fa60dfd19 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -305,11 +305,11 @@ def analyze_typeddict_classdef_fields( required_keys = { field for (field, t) in zip(fields, types) - if (total or (isinstance(t, RequiredType) and t.required)) # type: ignore[misc] - and not (isinstance(t, RequiredType) and not t.required) # type: ignore[misc] + if (total or (isinstance(t, RequiredType) and t.required)) + and not (isinstance(t, RequiredType) and not t.required) } types = [ # unwrap Required[T] to just T - t.item if isinstance(t, RequiredType) else t for t in types # type: ignore[misc] + t.item if isinstance(t, RequiredType) else t for t in types ] return fields, types, required_keys @@ -361,11 +361,11 @@ def check_typeddict( required_keys = { field for (field, t) in zip(items, types) - if (total or (isinstance(t, RequiredType) and t.required)) # type: ignore[misc] - and not (isinstance(t, RequiredType) and not t.required) # type: ignore[misc] + if (total or (isinstance(t, RequiredType) and t.required)) + and not (isinstance(t, RequiredType) and not t.required) } types = [ # unwrap Required[T] to just T - t.item if isinstance(t, RequiredType) else t for t in types # type: ignore[misc] + t.item if isinstance(t, RequiredType) else t for t in types ] existing_info = None if isinstance(node.analyzed, TypedDictExpr): From d64c62aee002952e84008466007b7265e74f700e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 11 Aug 2022 23:24:36 +0100 Subject: [PATCH 06/15] Fix self-check --- mypy/checkexpr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f6a6695df2cb..60bfc575af2e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -410,10 +410,10 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> ): typeddict_callable = get_proper_type(self.accept(e.callee)) if isinstance(typeddict_callable, CallableType): - typeddict_type = get_proper_type(typeddict_callable.ret_type) - assert isinstance(typeddict_type, TypedDictType) + typeddict_ret_type = get_proper_type(typeddict_callable.ret_type) + assert isinstance(typeddict_ret_type, TypedDictType) return self.check_typeddict_call( - typeddict_type, e.arg_kinds, e.arg_names, e.args, e, typeddict_callable + typeddict_ret_type, e.arg_kinds, e.arg_names, e.args, e, typeddict_callable ) if ( isinstance(e.callee, NameExpr) From 9889c2830349f9eda88fc42abf8792207793d70b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 12 Aug 2022 00:04:18 +0100 Subject: [PATCH 07/15] Fix class member access --- mypy/checkmember.py | 2 ++ test-data/unit/check-typeddict.test | 12 ++++++++++++ test-data/unit/fixtures/dict.pyi | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index a2f9db117325..6cb77e9afb57 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -329,6 +329,8 @@ def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: Member assert isinstance(ret_type, ProperType) if isinstance(ret_type, TupleType): ret_type = tuple_fallback(ret_type) + if isinstance(ret_type, TypedDictType): + ret_type = ret_type.fallback if isinstance(ret_type, Instance): if not mx.is_operator: # When Python sees an operator (eg `3 == 4`), it automatically translates that diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 582ca65fe76a..40c25183f06d 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2474,3 +2474,15 @@ s3: Sub3 reveal_type(s3) # N: Revealed type is "TypedDict('__main__.Sub3', {'x': Any, 'y': builtins.int})" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testTypedDictAttributeOnClassObject] +from typing import TypedDict + +class TD(TypedDict): + x: str + y: str + +reveal_type(TD.__iter__) # N: Revealed type is "def (typing._TypedDict) -> typing.Iterator[builtins.str]" +reveal_type(TD.__annotations__) # N: Revealed type is "typing.Mapping[builtins.str, builtins.object]" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index 48c16f262f3e..f4ec15e4fa9a 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -13,7 +13,8 @@ class object: def __init_subclass__(cls) -> None: pass def __eq__(self, other: object) -> bool: pass -class type: pass +class type: + __annotations__: Mapping[str, object] class dict(Mapping[KT, VT]): @overload From c3b76c8d4f6eb1ac73da8320a73d239c34df84ed Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 12 Aug 2022 01:45:39 +0100 Subject: [PATCH 08/15] Handle couple more corner cases --- mypy/checkexpr.py | 66 +++++++++++++++++------------ mypy/nodes.py | 4 +- mypy/typeanal.py | 10 ++++- test-data/unit/check-typeddict.test | 21 +++++++++ 4 files changed, 70 insertions(+), 31 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 60bfc575af2e..199b3dd98ad0 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -389,31 +389,28 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: return self.accept(e.analyzed, self.type_context[-1]) return self.visit_call_expr_inner(e, allow_none_return=allow_none_return) + def refers_to_typeddict(self, base: Expression) -> bool: + if not isinstance(base, RefExpr): + return False + if isinstance(base.node, TypeInfo) and base.node.typeddict_type is not None: + # Direct reference. + return True + return isinstance(base.node, TypeAlias) and isinstance( + get_proper_type(base.node.target), TypedDictType + ) + def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> Type: if ( - isinstance(e.callee, RefExpr) - and isinstance(e.callee.node, TypeInfo) - and e.callee.node.typeddict_type is not None - ): - # Use named fallback for better error messages. - typeddict_type = e.callee.node.typeddict_type.copy_modified( - fallback=Instance(e.callee.node, []) - ) - return self.check_typeddict_call( - typeddict_type, e.arg_kinds, e.arg_names, e.args, e, self.accept(e.callee) - ) - if ( - isinstance(e.callee, IndexExpr) - and isinstance(e.callee.base, RefExpr) - and isinstance(e.callee.base.node, TypeInfo) - and e.callee.base.node.typeddict_type is not None + self.refers_to_typeddict(e.callee) + or isinstance(e.callee, IndexExpr) + and self.refers_to_typeddict(e.callee.base) ): typeddict_callable = get_proper_type(self.accept(e.callee)) if isinstance(typeddict_callable, CallableType): - typeddict_ret_type = get_proper_type(typeddict_callable.ret_type) - assert isinstance(typeddict_ret_type, TypedDictType) + typeddict_type = get_proper_type(typeddict_callable.ret_type) + assert isinstance(typeddict_type, TypedDictType) return self.check_typeddict_call( - typeddict_ret_type, e.arg_kinds, e.arg_names, e.args, e, typeddict_callable + typeddict_type, e.arg_kinds, e.arg_names, e.args, e, typeddict_callable ) if ( isinstance(e.callee, NameExpr) @@ -741,6 +738,15 @@ def typeddict_callable(self, info: TypeInfo) -> CallableType: variables=info.defn.type_vars, ) + def typeddict_callable_from_context(self, callee: TypedDictType) -> CallableType: + return CallableType( + list(callee.items.values()), + [ArgKind.ARG_NAMED] * len(callee.items), + list(callee.items.keys()), + callee, + self.named_type("builtins.type"), + ) + def check_typeddict_call_with_kwargs( self, callee: TypedDictType, @@ -768,13 +774,7 @@ def check_typeddict_call_with_kwargs( if callee.fallback.type.special_alias is not None: infer_callee = self.typeddict_callable(callee.fallback.type) else: - infer_callee = CallableType( - list(callee.items.values()), - [ArgKind.ARG_NAMED] * len(callee.items), - list(callee.items.keys()), - callee, - self.named_type("builtins.type"), - ) + infer_callee = self.typeddict_callable_from_context(callee) # We don't show any errors, just infer types in a generic TypedDict type, # a custom error message will be given below, if there are errors. @@ -3729,6 +3729,8 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: if isinstance(item, Instance): tp = type_object_type(item.type, self.named_type) return self.apply_type_arguments_to_callable(tp, item.args, tapp) + elif isinstance(item, TypedDictType): + return self.typeddict_callable_from_context(item) else: self.chk.fail(message_registry.ONLY_CLASS_APPLICATION, tapp) return AnyType(TypeOfAny.from_error) @@ -3782,7 +3784,15 @@ class LongName(Generic[T]): ... # For example: # A = List[Tuple[T, T]] # x = A() <- same as List[Tuple[Any, Any]], see PEP 484. - item = get_proper_type(set_any_tvars(alias, ctx.line, ctx.column)) + item = get_proper_type( + set_any_tvars( + alias, + ctx.line, + ctx.column, + disallow_any=self.chk.options.disallow_any_generics and not alias_definition, + fail=self.msg.fail, + ) + ) if isinstance(item, Instance): # Normally we get a callable type (or overloaded) with .is_type_obj() true # representing the class's constructor @@ -3797,6 +3807,8 @@ class LongName(Generic[T]): ... tuple_fallback(item).type.fullname != "builtins.tuple" ): return type_object_type(tuple_fallback(item).type, self.named_type) + elif isinstance(item, TypedDictType): + return self.typeddict_callable_from_context(item) elif isinstance(item, AnyType): return AnyType(TypeOfAny.from_another_any, source_any=item) else: diff --git a/mypy/nodes.py b/mypy/nodes.py index b7b3a6ef87f3..492db6fff8d3 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3192,8 +3192,8 @@ class TypeAlias(SymbolNode): following: 1. An alias targeting a generic class without explicit variables act as - the given class (this doesn't apply to Tuple and Callable, which are not proper - classes but special type constructors): + the given class (this doesn't apply to TypedDict, Tuple and Callable, which + are not proper classes but special type constructors): A = List AA = List[Any] diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 567f5e17aabc..883d0285263a 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1574,8 +1574,14 @@ def set_any_tvars( type_of_any = TypeOfAny.from_omitted_generics if disallow_any: assert fail is not None - otype = unexpanded_type or node.target - type_str = otype.name if isinstance(otype, UnboundType) else format_type_bare(otype) + if unexpanded_type: + type_str = ( + unexpanded_type.name + if isinstance(unexpanded_type, UnboundType) + else format_type_bare(unexpanded_type) + ) + else: + type_str = node.name fail( message_registry.BARE_GENERIC.format(quote_type_string(type_str)), diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 40c25183f06d..97fdd10b9931 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2486,3 +2486,24 @@ reveal_type(TD.__iter__) # N: Revealed type is "def (typing._TypedDict) -> typi reveal_type(TD.__annotations__) # N: Revealed type is "typing.Mapping[builtins.str, builtins.object]" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testGenericTypedDictAlias] +# flags: --disallow-any-generics +from typing import TypedDict, Generic, TypeVar, List + +T = TypeVar("T") +class TD(TypedDict, Generic[T]): + key: int + value: T + +Alias = TD[List[T]] + +ad: Alias[str] +reveal_type(ad) # N: Revealed type is "TypedDict('__main__.TD', {'key': builtins.int, 'value': builtins.list[builtins.str]})" +Alias[str](key=0, value=0) # E: Incompatible types (expression has type "int", TypedDict item "value" has type "List[str]") + +# Generic aliases are *always* filled with Any, so this is different from TD(...) call. +Alias(key=0, value=0) # E: Missing type parameters for generic type "Alias" \ + # E: Incompatible types (expression has type "int", TypedDict item "value" has type "List[Any]") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From c3b85a19f616a0afebe9988e0366c48077a11130 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 12 Aug 2022 01:55:57 +0100 Subject: [PATCH 09/15] Oops --- mypy/typeanal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 883d0285263a..0a6c971e0eab 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1572,7 +1572,7 @@ def set_any_tvars( type_of_any = TypeOfAny.from_error else: type_of_any = TypeOfAny.from_omitted_generics - if disallow_any: + if disallow_any and node.alias_tvars: assert fail is not None if unexpanded_type: type_str = ( From 5c0e3ee650d653eddd91f035b6b66d6024b8b35a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 12 Aug 2022 09:06:30 +0100 Subject: [PATCH 10/15] Make --disallow-any-generics more consistent for type aliases --- mypy/checkexpr.py | 26 +++++++++++++++++--------- test-data/unit/check-flags.test | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 199b3dd98ad0..2c8f5b5b2bc6 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -282,6 +282,12 @@ def __init__( self.resolved_type = {} + # Callee in a call expression is in some sense both runtime context and + # type context, because we support things like C[int](...). Store information + # on whether current expression is a callee, to give better error messages + # related to type context. + self.is_callee = False + def reset(self) -> None: self.resolved_type = {} @@ -405,7 +411,7 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> or isinstance(e.callee, IndexExpr) and self.refers_to_typeddict(e.callee.base) ): - typeddict_callable = get_proper_type(self.accept(e.callee)) + typeddict_callable = get_proper_type(self.accept(e.callee, is_callee=True)) if isinstance(typeddict_callable, CallableType): typeddict_type = get_proper_type(typeddict_callable.ret_type) assert isinstance(typeddict_type, TypedDictType) @@ -472,7 +478,9 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> ret_type=self.object_type(), fallback=self.named_type("builtins.function"), ) - callee_type = get_proper_type(self.accept(e.callee, type_context, always_allow_any=True)) + callee_type = get_proper_type( + self.accept(e.callee, type_context, always_allow_any=True, is_callee=True) + ) if ( self.chk.options.disallow_untyped_calls and self.chk.in_checked_function() @@ -2609,7 +2617,7 @@ def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type return self.analyze_ref_expr(e) else: # This is a reference to a non-module attribute. - original_type = self.accept(e.expr) + original_type = self.accept(e.expr, is_callee=self.is_callee) base = e.expr module_symbol_table = None @@ -3784,13 +3792,10 @@ class LongName(Generic[T]): ... # For example: # A = List[Tuple[T, T]] # x = A() <- same as List[Tuple[Any, Any]], see PEP 484. + disallow_any = self.chk.options.disallow_any_generics and self.is_callee item = get_proper_type( set_any_tvars( - alias, - ctx.line, - ctx.column, - disallow_any=self.chk.options.disallow_any_generics and not alias_definition, - fail=self.msg.fail, + alias, ctx.line, ctx.column, disallow_any=disallow_any, fail=self.msg.fail ) ) if isinstance(item, Instance): @@ -4570,6 +4575,7 @@ def accept( type_context: Optional[Type] = None, allow_none_return: bool = False, always_allow_any: bool = False, + is_callee: bool = False, ) -> Type: """Type check a node in the given type context. If allow_none_return is True and this expression is a call, allow it to return None. This @@ -4578,6 +4584,8 @@ def accept( if node in self.type_overrides: return self.type_overrides[node] self.type_context.append(type_context) + old_is_callee = self.is_callee + self.is_callee = is_callee try: if allow_none_return and isinstance(node, CallExpr): typ = self.visit_call_expr(node, allow_none_return=True) @@ -4593,7 +4601,7 @@ def accept( report_internal_error( err, self.chk.errors.file, node.line, self.chk.errors, self.chk.options ) - + self.is_callee = old_is_callee self.type_context.pop() assert typ is not None self.chk.store_type(node, typ) diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index ed4d2e72149b..5b5d49c80708 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -1894,6 +1894,28 @@ def f() -> G: # E: Missing type parameters for generic type "G" x: G[Any] = G() # no error y: G = x # E: Missing type parameters for generic type "G" +[case testDisallowAnyGenericsForAliasesInRuntimeContext] +# flags: --disallow-any-generics +from typing import Any, TypeVar, Generic, Tuple + +T = TypeVar("T") +class G(Generic[T]): + @classmethod + def foo(cls) -> T: ... + +A = G[Tuple[T, T]] +A() # E: Missing type parameters for generic type "A" +A.foo() # E: Missing type parameters for generic type "A" + +B = G +B() +B.foo() + +def foo(x: Any) -> None: ... +foo(A) +foo(A.foo) +[builtins fixtures/classmethod.pyi] + [case testDisallowSubclassingAny] # flags: --config-file tmp/mypy.ini import m From 5fd8c44fd81d28279c71837ab13f96c75ea82419 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 13 Aug 2022 11:46:13 +0100 Subject: [PATCH 11/15] Be more principled with fallback args (although we don't use the for anything) --- mypy/expandtype.py | 6 +++++- mypy/nodes.py | 4 +++- mypy/semanal.py | 3 +++ mypy/typeanal.py | 1 - 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 9a948ca2f115..c9b92a83118a 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -301,7 +301,11 @@ def visit_tuple_type(self, t: TupleType) -> Type: return items def visit_typeddict_type(self, t: TypedDictType) -> Type: - return t.copy_modified(item_types=self.expand_types(t.items.values())) + fallback = t.fallback.accept(self) + fallback = get_proper_type(fallback) + if not isinstance(fallback, Instance): + fallback = t.fallback + return t.copy_modified(item_types=self.expand_types(t.items.values()), fallback=fallback) def visit_literal_type(self, t: LiteralType) -> Type: # TODO: Verify this implementation is correct diff --git a/mypy/nodes.py b/mypy/nodes.py index 492db6fff8d3..f66236199e94 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3305,7 +3305,9 @@ def from_typeddict_type(cls, info: TypeInfo) -> "TypeAlias": """Generate an alias to the TypedDict type described by a given TypeInfo.""" assert info.typeddict_type return TypeAlias( - info.typeddict_type.copy_modified(fallback=mypy.types.Instance(info, [])), + info.typeddict_type.copy_modified( + fallback=mypy.types.Instance(info, info.defn.type_vars) + ), info.fullname, info.line, info.column, diff --git a/mypy/semanal.py b/mypy/semanal.py index 586266de2074..0d2be57c3b00 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1386,6 +1386,9 @@ def analyze_class(self, defn: ClassDef) -> None: defn.info.add_type_vars() assert defn.info.special_alias is not None defn.info.special_alias.alias_tvars = list(defn.info.type_vars) + target = defn.info.special_alias.target + assert isinstance(target, ProperType) and isinstance(target, TypedDictType) + target.fallback.args = tuple(defn.type_vars) return if self.analyze_namedtuple_classdef(defn): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0a6c971e0eab..f96e5dcc530c 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -622,7 +622,6 @@ def analyze_type_with_type_info( # The class has a TypedDict[...] base class so it will be # represented as a typeddict type. if info.special_alias: - # We don't support generic TypedDict types yet. return TypeAliasType(info.special_alias, self.anal_array(args)) # Create a named TypedDictType return td.copy_modified( From a8f667d7a9e881417a7cbb8a9d3f06fc09b3dd9b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 13 Aug 2022 12:09:59 +0100 Subject: [PATCH 12/15] Add incremental tests --- test-data/unit/check-incremental.test | 23 +++++++++++++++++++ test-data/unit/fine-grained.test | 32 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 0cf048bee959..8e5ec8fb02f4 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -5893,3 +5893,26 @@ reveal_type(a.n) tmp/c.py:4: note: Revealed type is "TypedDict('a.N', {'r': Union[TypedDict('b.M', {'r': Union[..., None], 'x': builtins.int}), None], 'x': builtins.int})" tmp/c.py:5: error: Incompatible types in assignment (expression has type "Optional[N]", variable has type "int") tmp/c.py:7: note: Revealed type is "TypedDict('a.N', {'r': Union[TypedDict('b.M', {'r': Union[..., None], 'x': builtins.int}), None], 'x': builtins.int})" + +[case testGenericTypedDictSerialization] +import b +[file a.py] +from typing import TypedDict, Generic, TypedDict + +class TD(TypedDict, Generic[T]): + key: int + value: T + +[file b.py] +from a import TD +td = TD(key=0, value="yes") +s: str = td["value"] +[file b.py.2] +from a import TD +td = TD(key=0, value=42) +s: str = td["value"] +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] +[out] +[out2] +tmp/b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 2ce647f9cba1..49260c85ca43 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -3666,6 +3666,38 @@ def foo(x: Point) -> int: == b.py:4: error: Unsupported operand types for + ("int" and "str") +[case testTypedDictUpdateGeneric] +# flags: --enable-recursive-aliases +import b +[file a.py] +from mypy_extensions import TypedDict +class Point(TypedDict): + x: int + y: int +[file a.py.2] +from mypy_extensions import TypedDict +from typing import Generic, TypeVar + +T = TypeVar("T") +class Point(TypedDict, Generic[T]): + x: int + y: T +[file b.py] +from a import Point +def foo() -> None: + p = Point(x=0, y=1) + i: int = p["y"] +[file b.py.3] +from a import Point +def foo() -> None: + p = Point(x=0, y="no") + i: int = p["y"] +[builtins fixtures/dict.pyi] +[out] +== +== +b.py:4: error: Incompatible types in assignment (expression has type "str", variable has type "int") + [case testBasicAliasUpdate] import b [file a.py] From cf0ba973db92f99f610f70cac6447cc9fb2c1a89 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 13 Aug 2022 13:36:40 +0100 Subject: [PATCH 13/15] Add support for call syntax --- mypy/checkexpr.py | 4 +++ mypy/semanal.py | 52 ++++++++++++++++++++++++----- mypy/semanal_shared.py | 5 +++ mypy/semanal_typeddict.py | 24 +++++++------ test-data/unit/check-typeddict.test | 17 ++++++++++ 5 files changed, 82 insertions(+), 20 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 2c8f5b5b2bc6..07be922557dc 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -730,6 +730,9 @@ def typeddict_callable(self, info: TypeInfo) -> CallableType: If it appears as a callee, it will be special-cased anyway, e.g. it is also allowed to accept a single positional argument if it is a dict literal. + + Note it is not safe to move this to type_object_type() since it will crash + on plugin-generated TypedDicts, that may not have the special_alias. """ assert info.special_alias is not None target = info.special_alias.target @@ -782,6 +785,7 @@ def check_typeddict_call_with_kwargs( if callee.fallback.type.special_alias is not None: infer_callee = self.typeddict_callable(callee.fallback.type) else: + # Likely a TypedDict type generated by a plugin. infer_callee = self.typeddict_callable_from_context(callee) # We don't show any errors, just infer types in a generic TypedDict type, diff --git a/mypy/semanal.py b/mypy/semanal.py index 0d2be57c3b00..d5c8835e4a04 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1381,14 +1381,8 @@ def analyze_class(self, defn: ClassDef) -> None: if self.analyze_typeddict_classdef(defn): if defn.info: - defn.type_vars = tvar_defs - defn.info.type_vars = [] - defn.info.add_type_vars() - assert defn.info.special_alias is not None - defn.info.special_alias.alias_tvars = list(defn.info.type_vars) - target = defn.info.special_alias.target - assert isinstance(target, ProperType) and isinstance(target, TypedDictType) - target.fallback.args = tuple(defn.type_vars) + self.setup_type_vars(defn, tvar_defs) + self.setup_alias_type_vars(defn) return if self.analyze_namedtuple_classdef(defn): @@ -1413,6 +1407,19 @@ def analyze_class(self, defn: ClassDef) -> None: self.analyze_class_decorator(defn, decorator) self.analyze_class_body_common(defn) + def setup_type_vars(self, defn: ClassDef, tvar_defs: List[TypeVarLikeType]) -> None: + defn.type_vars = tvar_defs + defn.info.type_vars = [] + # we want to make sure any additional logic in add_type_vars gets run + defn.info.add_type_vars() + + def setup_alias_type_vars(self, defn: ClassDef) -> None: + assert defn.info.special_alias is not None + defn.info.special_alias.alias_tvars = list(defn.info.type_vars) + target = defn.info.special_alias.target + assert isinstance(target, ProperType) and isinstance(target, TypedDictType) + target.fallback.args = tuple(defn.type_vars) + def is_core_builtin_class(self, defn: ClassDef) -> bool: return self.cur_mod_id == "builtins" and defn.name in CORE_BUILTIN_CLASSES @@ -1689,6 +1696,29 @@ def get_all_bases_tvars( tvars.extend(base_tvars) return remove_dups(tvars) + def get_and_bind_all_tvars(self, type_exprs: List[Expression]) -> List[TypeVarLikeType]: + """Return all type variable references in item type expressions. + + This is a helper for generic TypedDicts and NamedTuples. Essentially it is + a simplified version of the logic we use for ClassDef bases. We duplicate + some amount of code, because it is hard to refactor common pieces. + """ + tvars = [] + for base_expr in type_exprs: + try: + base = self.expr_to_unanalyzed_type(base_expr) + except TypeTranslationError: + # This error will be caught later. + continue + base_tvars = base.accept(TypeVarLikeQuery(self.lookup_qualified, self.tvar_scope)) + tvars.extend(base_tvars) + tvars = remove_dups(tvars) # Variables are defined in order of textual appearance. + tvar_defs = [] + for name, tvar_expr in tvars: + tvar_def = self.tvar_scope.bind_new(name, tvar_expr) + tvar_defs.append(tvar_def) + return tvar_defs + def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) -> None: """Prepare for the analysis of a class definition. @@ -2693,7 +2723,7 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool: return False lvalue = s.lvalues[0] name = lvalue.name - is_typed_dict, info = self.typed_dict_analyzer.check_typeddict( + is_typed_dict, info, tvar_defs = self.typed_dict_analyzer.check_typeddict( s.rvalue, name, self.is_func_scope() ) if not is_typed_dict: @@ -2704,6 +2734,10 @@ def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool: # Yes, it's a valid typed dict, but defer if it is not ready. if not info: self.mark_incomplete(name, lvalue, becomes_typeinfo=True) + else: + defn = info.defn + self.setup_type_vars(defn, tvar_defs) + self.setup_alias_type_vars(defn) return True def analyze_lvalues(self, s: AssignmentStmt) -> None: diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index 2c1d843f4c7a..d2cbe16b97e1 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -32,6 +32,7 @@ TupleType, Type, TypeVarId, + TypeVarLikeType, get_proper_type, ) @@ -158,6 +159,10 @@ def anal_type( ) -> Optional[Type]: raise NotImplementedError + @abstractmethod + def get_and_bind_all_tvars(self, type_exprs: List[Expression]) -> List[TypeVarLikeType]: + raise NotImplementedError + @abstractmethod def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo: raise NotImplementedError diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index e11fa60dfd19..856ae227abb6 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -38,6 +38,7 @@ Type, TypedDictType, TypeOfAny, + TypeVarLikeType, replace_alias_tvars, ) @@ -316,7 +317,7 @@ def analyze_typeddict_classdef_fields( def check_typeddict( self, node: Expression, var_name: Optional[str], is_func_scope: bool - ) -> Tuple[bool, Optional[TypeInfo]]: + ) -> Tuple[bool, Optional[TypeInfo], List[TypeVarLikeType]]: """Check if a call defines a TypedDict. The optional var_name argument is the name of the variable to @@ -329,20 +330,20 @@ def check_typeddict( return (True, None). """ if not isinstance(node, CallExpr): - return False, None + return False, None, [] call = node callee = call.callee if not isinstance(callee, RefExpr): - return False, None + return False, None, [] fullname = callee.fullname if fullname not in TPDICT_NAMES: - return False, None + return False, None, [] res = self.parse_typeddict_args(call) if res is None: # This is a valid typed dict, but some type is not ready. # The caller should defer this until next iteration. - return True, None - name, items, types, total, ok = res + return True, None, [] + name, items, types, total, tvar_defs, ok = res if not ok: # Error. Construct dummy return value. info = self.build_typeddict_typeinfo("TypedDict", [], [], set(), call.line, None) @@ -381,11 +382,11 @@ def check_typeddict( self.api.add_symbol(var_name, info, node) call.analyzed = TypedDictExpr(info) call.analyzed.set_line(call) - return True, info + return True, info, tvar_defs def parse_typeddict_args( self, call: CallExpr - ) -> Optional[Tuple[str, List[str], List[Type], bool, bool]]: + ) -> Optional[Tuple[str, List[str], List[Type], bool, List[TypeVarLikeType], bool]]: """Parse typed dict call expression. Return names, types, totality, was there an error during parsing. @@ -420,6 +421,7 @@ def parse_typeddict_args( 'TypedDict() "total" argument must be True or False', call ) dictexpr = args[1] + tvar_defs = self.api.get_and_bind_all_tvars([t for k, t in dictexpr.items]) res = self.parse_typeddict_fields_with_types(dictexpr.items, call) if res is None: # One of the types is not ready, defer. @@ -435,7 +437,7 @@ def parse_typeddict_args( if has_any_from_unimported_type(t): self.msg.unimported_type_becomes_any("Type of a TypedDict key", t, dictexpr) assert total is not None - return args[0].value, items, types, total, ok + return args[0].value, items, types, total, tvar_defs, ok def parse_typeddict_fields_with_types( self, dict_items: List[Tuple[Optional[Expression], Expression]], context: Context @@ -488,9 +490,9 @@ def parse_typeddict_fields_with_types( def fail_typeddict_arg( self, message: str, context: Context - ) -> Tuple[str, List[str], List[Type], bool, bool]: + ) -> Tuple[str, List[str], List[Type], bool, List[TypeVarLikeType], bool]: self.fail(message, context) - return "", [], [], True, False + return "", [], [], True, [], False def build_typeddict_typeinfo( self, diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 97fdd10b9931..58d19e59f7c3 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2507,3 +2507,20 @@ Alias(key=0, value=0) # E: Missing type parameters for generic type "Alias" \ # E: Incompatible types (expression has type "int", TypedDict item "value" has type "List[Any]") [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testGenericTypedDictCallSyntax] +from typing import TypedDict, TypeVar + +T = TypeVar("T") +TD = TypedDict("TD", {"key": int, "value": T}) +reveal_type(TD) # N: Revealed type is "def [T] (*, key: builtins.int, value: T`-1) -> TypedDict('__main__.TD', {'key': builtins.int, 'value': T`-1})" + +tds: TD[str] +reveal_type(tds) # N: Revealed type is "TypedDict('__main__.TD', {'key': builtins.int, 'value': builtins.str})" + +tdi = TD(key=0, value=0) +reveal_type(tdi) # N: Revealed type is "TypedDict('__main__.TD', {'key': builtins.int, 'value': builtins.int})" +TD[str](key=0, value=0) # E: Incompatible types (expression has type "int", TypedDict item "value" has type "str") +TD[str]({"key": 0, "value": 0}) # E: Incompatible types (expression has type "int", TypedDict item "value" has type "str") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] From e3c1ecfdc2823f67e5a2c4ae7773ffb0a2da932d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 13 Aug 2022 16:37:49 +0100 Subject: [PATCH 14/15] Cleanup tests --- test-data/unit/check-incremental.test | 3 ++- test-data/unit/fine-grained.test | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 8e5ec8fb02f4..0faf1bb62e42 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -5897,8 +5897,9 @@ tmp/c.py:7: note: Revealed type is "TypedDict('a.N', {'r': Union[TypedDict('b.M' [case testGenericTypedDictSerialization] import b [file a.py] -from typing import TypedDict, Generic, TypedDict +from typing import TypedDict, Generic, TypeVar +T = TypeVar("T") class TD(TypedDict, Generic[T]): key: int value: T diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 49260c85ca43..6bdcc77f87f9 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -3667,7 +3667,6 @@ def foo(x: Point) -> int: b.py:4: error: Unsupported operand types for + ("int" and "str") [case testTypedDictUpdateGeneric] -# flags: --enable-recursive-aliases import b [file a.py] from mypy_extensions import TypedDict From 1fbc42030ee102d8162659646106069f7dcdbe8d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 15 Aug 2022 09:54:04 +0100 Subject: [PATCH 15/15] Update a test (CR) --- test-data/unit/check-typeddict.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 58d19e59f7c3..998b710cf02a 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -2484,6 +2484,7 @@ class TD(TypedDict): reveal_type(TD.__iter__) # N: Revealed type is "def (typing._TypedDict) -> typing.Iterator[builtins.str]" reveal_type(TD.__annotations__) # N: Revealed type is "typing.Mapping[builtins.str, builtins.object]" +reveal_type(TD.values) # N: Revealed type is "def (self: typing.Mapping[T`1, T_co`2]) -> typing.Iterable[T_co`2]" [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi]