diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 0d6a0b7ff87f..8bf073d30f71 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -30,6 +30,7 @@ StrExpr, TempNode, TupleExpr, + TypeAlias, TypedDictExpr, TypeInfo, ) @@ -50,6 +51,7 @@ TypedDictType, TypeOfAny, TypeVarLikeType, + get_proper_type, ) TPDICT_CLASS_ERROR: Final = ( @@ -137,23 +139,18 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N 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) + elif ( + isinstance(expr, RefExpr) + and self.is_typeddict(expr) + or isinstance(expr, IndexExpr) + and self.is_typeddict(expr.base) + ): + info = self._parse_typeddict_base(expr, defn) + if info.fullname not in typeddict_bases_set: + typeddict_bases_set.add(info.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) + self.fail(f'Duplicate base class "{info.name}"', defn) else: self.fail("All bases of a new TypedDict must be TypedDict types", defn) @@ -190,22 +187,13 @@ def add_keys_and_types_from_base( readonly_keys: set[str], ctx: Context, ) -> None: + info = self._parse_typeddict_base(base, ctx) base_args: list[Type] = [] - if isinstance(base, RefExpr): - assert isinstance(base.node, TypeInfo) - info = base.node - elif isinstance(base, IndexExpr): - assert isinstance(base.base, RefExpr) - assert isinstance(base.base.node, TypeInfo) - info = base.base.node + if isinstance(base, IndexExpr): args = self.analyze_base_args(base, ctx) if args is None: return base_args = args - else: - assert isinstance(base, CallExpr) - assert isinstance(base.analyzed, TypedDictExpr) - info = base.analyzed.info assert info.typeddict_type is not None base_typed_dict = info.typeddict_type @@ -231,6 +219,26 @@ def add_keys_and_types_from_base( required_keys.update(base_typed_dict.required_keys) readonly_keys.update(base_typed_dict.readonly_keys) + def _parse_typeddict_base(self, base: Expression, ctx: Context) -> TypeInfo: + if isinstance(base, RefExpr): + if isinstance(base.node, TypeInfo): + return base.node + elif isinstance(base.node, TypeAlias): + # Only old TypeAlias / plain assignment, PEP695 `type` stmt + # cannot be used as a base class + target = get_proper_type(base.node.target) + assert isinstance(target, TypedDictType) + return target.fallback.type + else: + assert False + elif isinstance(base, IndexExpr): + assert isinstance(base.base, RefExpr) + return self._parse_typeddict_base(base.base, ctx) + else: + assert isinstance(base, CallExpr) + assert isinstance(base.analyzed, TypedDictExpr) + return base.analyzed.info + def analyze_base_args(self, base: IndexExpr, ctx: Context) -> list[Type] | None: """Analyze arguments of base type expressions as types. @@ -527,7 +535,7 @@ def parse_typeddict_args( return "", [], [], True, [], False 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) + res = self.parse_typeddict_fields_with_types(dictexpr.items) if res is None: # One of the types is not ready, defer. return None @@ -536,7 +544,7 @@ def parse_typeddict_args( return args[0].value, items, types, total, tvar_defs, ok def parse_typeddict_fields_with_types( - self, dict_items: list[tuple[Expression | None, Expression]], context: Context + self, dict_items: list[tuple[Expression | None, Expression]] ) -> tuple[list[str], list[Type], bool] | None: """Parse typed dict items passed as pairs (name expression, type expression). @@ -609,10 +617,11 @@ def build_typeddict_typeinfo( # Helpers def is_typeddict(self, expr: Expression) -> bool: - return ( - isinstance(expr, RefExpr) - and isinstance(expr.node, TypeInfo) + return isinstance(expr, RefExpr) and ( + isinstance(expr.node, TypeInfo) and expr.node.typeddict_type is not None + or isinstance(expr.node, TypeAlias) + and isinstance(get_proper_type(expr.node.target), TypedDictType) ) def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None: diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 48bfa4bdba49..47c8a71ba0e3 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -4151,3 +4151,106 @@ class Base: # E: TypedDict() expects a dictionary literal as the second argument [builtins fixtures/dict.pyi] [typing fixtures/typing-typeddict.pyi] + +[case testTypedDictAlias] +from typing import NotRequired, TypedDict +from typing_extensions import TypeAlias + +class Base(TypedDict): + foo: int + +Base1 = Base +class Child1(Base1): + bar: NotRequired[int] +c11: Child1 = {"foo": 0} +c12: Child1 = {"foo": 0, "bar": 1} +c13: Child1 = {"foo": 0, "bar": 1, "baz": "error"} # E: Extra key "baz" for TypedDict "Child1" + +Base2: TypeAlias = Base +class Child2(Base2): + bar: NotRequired[int] +c21: Child2 = {"foo": 0} +c22: Child2 = {"foo": 0, "bar": 1} +c23: Child2 = {"foo": 0, "bar": 1, "baz": "error"} # E: Extra key "baz" for TypedDict "Child2" +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictAliasInheritance] +from typing import TypedDict +from typing_extensions import TypeAlias + +class A(TypedDict): + x: str +class B(TypedDict): + y: int + +B1 = B +B2: TypeAlias = B + +class C(A, B1): + pass +c1: C = {"y": 1} # E: Missing key "x" for TypedDict "C" +c2: C = {"x": "x", "y": 2} +c3: C = {"x": 1, "y": 2} # E: Incompatible types (expression has type "int", TypedDict item "x" has type "str") + +class D(A, B2): + pass +d1: D = {"y": 1} # E: Missing key "x" for TypedDict "D" +d2: D = {"x": "x", "y": 2} +d3: D = {"x": 1, "y": 2} # E: Incompatible types (expression has type "int", TypedDict item "x" has type "str") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictAliasDuplicateBases] +from typing import TypedDict +from typing_extensions import TypeAlias + +class A(TypedDict): + x: str + +A1 = A +A2 = A +A3: TypeAlias = A + +class E(A1, A2): pass # E: Duplicate base class "A" +class F(A1, A3): pass # E: Duplicate base class "A" +class G(A, A1): pass # E: Duplicate base class "A" + +class H(A, list): pass # E: All bases of a new TypedDict must be TypedDict types +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi] + +[case testTypedDictAliasGeneric] +from typing import Generic, TypedDict, TypeVar +from typing_extensions import TypeAlias + +_T = TypeVar("_T") + +class A(Generic[_T], TypedDict): + x: _T + +# This is by design - no_args aliases are only supported for instances +A0 = A +class B(A0[str]): # E: Bad number of arguments for type alias, expected 0, given 1 + y: int + +A1 = A[_T] +A2: TypeAlias = A[_T] +Aint = A[int] + +class C(A1[_T]): + y: str +c1: C[int] = {"x": 0, "y": "a"} +c2: C[int] = {"x": "no", "y": "a"} # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int") + +class D(A2[_T]): + y: str +d1: D[int] = {"x": 0, "y": "a"} +d2: D[int] = {"x": "no", "y": "a"} # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int") + +class E(Aint): + y: str +e1: E = {"x": 0, "y": "a"} +e2: E = {"x": "no", "y": "a"} +[builtins fixtures/dict.pyi] +[typing fixtures/typing-typeddict.pyi]