From dc5f89142cb36debc0e804a472f49f51448d76f3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 15 Aug 2022 11:20:55 +0100 Subject: [PATCH] Enable generic TypedDicts (#13389) Fixes #3863 This builds on top of some infra I added for recursive types (Ref #13297). Implementation is quite straightforward. The only non-trivial thing is that when extending/merging TypedDicts, the item types need to me mapped to supertype during semantic analysis. This means we can't call `is_subtype()` etc., and can in theory get types like `Union[int, int]`. But OTOH this equally applies to type aliases, and doesn't seem to cause problems. --- misc/proper_plugin.py | 1 + mypy/checkexpr.py | 164 +++++++++++-- mypy/checkmember.py | 2 + mypy/expandtype.py | 6 +- mypy/fixup.py | 1 + mypy/nodes.py | 8 +- mypy/semanal.py | 22 +- mypy/semanal_typeddict.py | 277 +++++++++++++++------- mypy/typeanal.py | 18 +- mypy/types.py | 2 + test-data/unit/check-flags.test | 22 ++ test-data/unit/check-incremental.test | 24 ++ test-data/unit/check-recursive-types.test | 34 +++ test-data/unit/check-serialize.test | 4 +- test-data/unit/check-typeddict.test | 134 ++++++++++- test-data/unit/deps.test | 6 + test-data/unit/fine-grained.test | 31 +++ test-data/unit/fixtures/dict.pyi | 3 +- 18 files changed, 630 insertions(+), 129 deletions(-) diff --git a/misc/proper_plugin.py b/misc/proper_plugin.py index afa9185136f9e..0b93f67cb06b5 100644 --- a/misc/proper_plugin.py +++ b/misc/proper_plugin.py @@ -97,6 +97,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 992ca75f7a406..b57ba7d73042c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -283,6 +283,12 @@ def __init__(self, chk: mypy.checker.TypeChecker, msg: MessageBuilder, plugin: P 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 = {} @@ -319,7 +325,11 @@ 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: + # 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 result.ret_type, Instance ): @@ -386,17 +396,29 @@ 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 + self.refers_to_typeddict(e.callee) + or isinstance(e.callee, IndexExpr) + and self.refers_to_typeddict(e.callee.base) ): - # 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) + 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) + return self.check_typeddict_call( + typeddict_type, e.arg_kinds, e.arg_names, e.args, e, typeddict_callable + ) if ( isinstance(e.callee, NameExpr) and e.callee.name in ("isinstance", "issubclass") @@ -457,7 +479,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() @@ -628,6 +652,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) @@ -635,21 +660,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) @@ -683,18 +712,59 @@ 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, 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. + + 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 + assert isinstance(target, ProperType) and isinstance(target, TypedDictType) + expected_types = list(target.items.values()) + kinds = [ArgKind.ARG_NAMED] * len(expected_types) + names = list(target.items.keys()) + return CallableType( + expected_types, + kinds, + names, + target, + self.named_type("builtins.type"), + 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, 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 = [ @@ -708,7 +778,38 @@ def check_typeddict_call_with_kwargs( ) return AnyType(TypeOfAny.from_error) - for (item_name, item_expected_type) in callee.items.items(): + 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: + # 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, + # 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( + infer_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( @@ -721,7 +822,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. @@ -2547,7 +2648,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 @@ -3670,6 +3771,8 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: elif isinstance(item, TupleType) and item.partial_fallback.type.is_named_tuple: tp = type_object_type(item.partial_fallback.type, self.named_type) return self.apply_type_arguments_to_callable(tp, item.partial_fallback.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) @@ -3723,7 +3826,12 @@ 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)) + 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=disallow_any, 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 @@ -3738,6 +3846,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: @@ -3962,7 +4072,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 @@ -4494,6 +4609,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 @@ -4502,6 +4618,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) @@ -4517,7 +4635,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/mypy/checkmember.py b/mypy/checkmember.py index 75101b3359ea3..fbc7fdc39abdc 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -331,6 +331,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/mypy/expandtype.py b/mypy/expandtype.py index 2906a41df2014..2957b73498877 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -307,7 +307,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/fixup.py b/mypy/fixup.py index 87c6258ff2d73..18636e4f0404f 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -80,6 +80,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/mypy/nodes.py b/mypy/nodes.py index 197b049e44639..090c53843870f 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] @@ -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 020f73e46269d..d164544755455 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -270,6 +270,7 @@ TupleType, Type, TypeAliasType, + TypedDictType, TypeOfAny, TypeType, TypeVarLikeType, @@ -1390,6 +1391,9 @@ def analyze_class(self, defn: ClassDef) -> None: return if self.analyze_typeddict_classdef(defn): + if defn.info: + self.setup_type_vars(defn, tvar_defs) + self.setup_alias_type_vars(defn) return if self.analyze_namedtuple_classdef(defn, tvar_defs): @@ -1420,8 +1424,13 @@ 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, TupleType) - target.partial_fallback.args = tuple(defn.type_vars) + assert isinstance(target, ProperType) + if isinstance(target, TypedDictType): + target.fallback.args = tuple(defn.type_vars) + elif isinstance(target, TupleType): + target.partial_fallback.args = tuple(defn.type_vars) + else: + assert False, f"Unexpected special alias type: {type(target)}" def is_core_builtin_class(self, defn: ClassDef) -> bool: return self.cur_mod_id == "builtins" and defn.name in CORE_BUILTIN_CLASSES @@ -1705,6 +1714,7 @@ def get_all_bases_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. @@ -1866,6 +1876,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) @@ -2736,7 +2748,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: @@ -2747,6 +2759,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_typeddict.py b/mypy/semanal_typeddict.py index 71c8b04be73c7..94deb84f059c0 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -2,7 +2,7 @@ from __future__ import annotations -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 @@ -20,18 +20,29 @@ 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, + TypeVarLikeType, + replace_alias_tvars, +) TPDICT_CLASS_ERROR: Final = ( "Invalid statement in TypedDict definition; " 'expected "field_name: field_type"' @@ -63,84 +74,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 @@ -204,18 +308,18 @@ 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 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 @@ -228,20 +332,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) @@ -260,11 +364,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): @@ -280,11 +384,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. @@ -319,6 +423,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. @@ -334,7 +439,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 @@ -387,9 +492,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/mypy/typeanal.py b/mypy/typeanal.py index f1e4e66752b6e..8dc49921f0aba 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -619,12 +619,8 @@ 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 @@ -1573,10 +1569,16 @@ 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 - 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/mypy/types.py b/mypy/types.py index 64a28a25924d1..103379f0a2049 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1740,6 +1740,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-flags.test b/test-data/unit/check-flags.test index ed4d2e72149b7..5b5d49c807083 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 diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 3d617e93f94e6..44452e2072b38 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -5933,3 +5933,27 @@ s: str = nt.value [out] [out2] tmp/b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testGenericTypedDictSerialization] +import b +[file a.py] +from typing import TypedDict, Generic, TypeVar + +T = TypeVar("T") +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/check-recursive-types.test b/test-data/unit/check-recursive-types.test index 18e2d25cf7b3f..c326246436ba3 100644 --- a/test-data/unit/check-recursive-types.test +++ b/test-data/unit/check-recursive-types.test @@ -776,3 +776,37 @@ 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] + +[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-serialize.test b/test-data/unit/check-serialize.test index 0d7e9f74fa756..66d5d879ae681 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 bbde1fad5f29a..49c1fe1c92798 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] @@ -2412,3 +2412,133 @@ 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 testGenericTypedDictCreation] +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") +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 + +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 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] + +[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]" +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] + +[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] + +[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] diff --git a/test-data/unit/deps.test b/test-data/unit/deps.test index 0714940246e59..28d51f1a4c30b 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 diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index d5a37d85d221b..aa53c6482449e 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -3696,6 +3696,37 @@ def foo(x: Point) -> int: == b.py:4: error: Unsupported operand types for + ("int" and "str") +[case testTypedDictUpdateGeneric] +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] diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index 48c16f262f3ec..f4ec15e4fa9af 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