Skip to content

Commit 67b70ce

Browse files
authored
Treat TypedDict (old-style) aliases as regular TypedDicts (#18852)
Fixes #18692. This PR makes mypy recognize old-style aliases to TypedDict types: ```python Alias = SomeTypedDict ExplicitAlias: TypeAlias = SomeTypedDict ``` Still doesn't support generic no_args aliases: ```python from typing import Generic, TypedDict, TypeVar _T = TypeVar("_T") class TD(TypedDict, Generic[_T]): foo: _T Alias = TD # but works with OtherAlias = TD[_T] ``` that's because `no_args` aliases are handled in code in several places and all of them expect such an alias to have `Instance` target.
1 parent e867132 commit 67b70ce

File tree

2 files changed

+143
-31
lines changed

2 files changed

+143
-31
lines changed

mypy/semanal_typeddict.py

+40-31
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
StrExpr,
3131
TempNode,
3232
TupleExpr,
33+
TypeAlias,
3334
TypedDictExpr,
3435
TypeInfo,
3536
)
@@ -50,6 +51,7 @@
5051
TypedDictType,
5152
TypeOfAny,
5253
TypeVarLikeType,
54+
get_proper_type,
5355
)
5456

5557
TPDICT_CLASS_ERROR: Final = (
@@ -137,23 +139,18 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
137139
typeddict_bases_set.add("TypedDict")
138140
else:
139141
self.fail('Duplicate base class "TypedDict"', defn)
140-
elif isinstance(expr, RefExpr) and self.is_typeddict(expr):
141-
assert expr.fullname
142-
if expr.fullname not in typeddict_bases_set:
143-
typeddict_bases_set.add(expr.fullname)
142+
elif (
143+
isinstance(expr, RefExpr)
144+
and self.is_typeddict(expr)
145+
or isinstance(expr, IndexExpr)
146+
and self.is_typeddict(expr.base)
147+
):
148+
info = self._parse_typeddict_base(expr, defn)
149+
if info.fullname not in typeddict_bases_set:
150+
typeddict_bases_set.add(info.fullname)
144151
typeddict_bases.append(expr)
145152
else:
146-
assert isinstance(expr.node, TypeInfo)
147-
self.fail(f'Duplicate base class "{expr.node.name}"', defn)
148-
elif isinstance(expr, IndexExpr) and self.is_typeddict(expr.base):
149-
assert isinstance(expr.base, RefExpr)
150-
assert expr.base.fullname
151-
if expr.base.fullname not in typeddict_bases_set:
152-
typeddict_bases_set.add(expr.base.fullname)
153-
typeddict_bases.append(expr)
154-
else:
155-
assert isinstance(expr.base.node, TypeInfo)
156-
self.fail(f'Duplicate base class "{expr.base.node.name}"', defn)
153+
self.fail(f'Duplicate base class "{info.name}"', defn)
157154
else:
158155
self.fail("All bases of a new TypedDict must be TypedDict types", defn)
159156

@@ -190,22 +187,13 @@ def add_keys_and_types_from_base(
190187
readonly_keys: set[str],
191188
ctx: Context,
192189
) -> None:
190+
info = self._parse_typeddict_base(base, ctx)
193191
base_args: list[Type] = []
194-
if isinstance(base, RefExpr):
195-
assert isinstance(base.node, TypeInfo)
196-
info = base.node
197-
elif isinstance(base, IndexExpr):
198-
assert isinstance(base.base, RefExpr)
199-
assert isinstance(base.base.node, TypeInfo)
200-
info = base.base.node
192+
if isinstance(base, IndexExpr):
201193
args = self.analyze_base_args(base, ctx)
202194
if args is None:
203195
return
204196
base_args = args
205-
else:
206-
assert isinstance(base, CallExpr)
207-
assert isinstance(base.analyzed, TypedDictExpr)
208-
info = base.analyzed.info
209197

210198
assert info.typeddict_type is not None
211199
base_typed_dict = info.typeddict_type
@@ -231,6 +219,26 @@ def add_keys_and_types_from_base(
231219
required_keys.update(base_typed_dict.required_keys)
232220
readonly_keys.update(base_typed_dict.readonly_keys)
233221

222+
def _parse_typeddict_base(self, base: Expression, ctx: Context) -> TypeInfo:
223+
if isinstance(base, RefExpr):
224+
if isinstance(base.node, TypeInfo):
225+
return base.node
226+
elif isinstance(base.node, TypeAlias):
227+
# Only old TypeAlias / plain assignment, PEP695 `type` stmt
228+
# cannot be used as a base class
229+
target = get_proper_type(base.node.target)
230+
assert isinstance(target, TypedDictType)
231+
return target.fallback.type
232+
else:
233+
assert False
234+
elif isinstance(base, IndexExpr):
235+
assert isinstance(base.base, RefExpr)
236+
return self._parse_typeddict_base(base.base, ctx)
237+
else:
238+
assert isinstance(base, CallExpr)
239+
assert isinstance(base.analyzed, TypedDictExpr)
240+
return base.analyzed.info
241+
234242
def analyze_base_args(self, base: IndexExpr, ctx: Context) -> list[Type] | None:
235243
"""Analyze arguments of base type expressions as types.
236244
@@ -527,7 +535,7 @@ def parse_typeddict_args(
527535
return "", [], [], True, [], False
528536
dictexpr = args[1]
529537
tvar_defs = self.api.get_and_bind_all_tvars([t for k, t in dictexpr.items])
530-
res = self.parse_typeddict_fields_with_types(dictexpr.items, call)
538+
res = self.parse_typeddict_fields_with_types(dictexpr.items)
531539
if res is None:
532540
# One of the types is not ready, defer.
533541
return None
@@ -536,7 +544,7 @@ def parse_typeddict_args(
536544
return args[0].value, items, types, total, tvar_defs, ok
537545

538546
def parse_typeddict_fields_with_types(
539-
self, dict_items: list[tuple[Expression | None, Expression]], context: Context
547+
self, dict_items: list[tuple[Expression | None, Expression]]
540548
) -> tuple[list[str], list[Type], bool] | None:
541549
"""Parse typed dict items passed as pairs (name expression, type expression).
542550
@@ -609,10 +617,11 @@ def build_typeddict_typeinfo(
609617
# Helpers
610618

611619
def is_typeddict(self, expr: Expression) -> bool:
612-
return (
613-
isinstance(expr, RefExpr)
614-
and isinstance(expr.node, TypeInfo)
620+
return isinstance(expr, RefExpr) and (
621+
isinstance(expr.node, TypeInfo)
615622
and expr.node.typeddict_type is not None
623+
or isinstance(expr.node, TypeAlias)
624+
and isinstance(get_proper_type(expr.node.target), TypedDictType)
616625
)
617626

618627
def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:

test-data/unit/check-typeddict.test

+103
Original file line numberDiff line numberDiff line change
@@ -4151,3 +4151,106 @@ class Base:
41514151
# E: TypedDict() expects a dictionary literal as the second argument
41524152
[builtins fixtures/dict.pyi]
41534153
[typing fixtures/typing-typeddict.pyi]
4154+
4155+
[case testTypedDictAlias]
4156+
from typing import NotRequired, TypedDict
4157+
from typing_extensions import TypeAlias
4158+
4159+
class Base(TypedDict):
4160+
foo: int
4161+
4162+
Base1 = Base
4163+
class Child1(Base1):
4164+
bar: NotRequired[int]
4165+
c11: Child1 = {"foo": 0}
4166+
c12: Child1 = {"foo": 0, "bar": 1}
4167+
c13: Child1 = {"foo": 0, "bar": 1, "baz": "error"} # E: Extra key "baz" for TypedDict "Child1"
4168+
4169+
Base2: TypeAlias = Base
4170+
class Child2(Base2):
4171+
bar: NotRequired[int]
4172+
c21: Child2 = {"foo": 0}
4173+
c22: Child2 = {"foo": 0, "bar": 1}
4174+
c23: Child2 = {"foo": 0, "bar": 1, "baz": "error"} # E: Extra key "baz" for TypedDict "Child2"
4175+
[builtins fixtures/dict.pyi]
4176+
[typing fixtures/typing-typeddict.pyi]
4177+
4178+
[case testTypedDictAliasInheritance]
4179+
from typing import TypedDict
4180+
from typing_extensions import TypeAlias
4181+
4182+
class A(TypedDict):
4183+
x: str
4184+
class B(TypedDict):
4185+
y: int
4186+
4187+
B1 = B
4188+
B2: TypeAlias = B
4189+
4190+
class C(A, B1):
4191+
pass
4192+
c1: C = {"y": 1} # E: Missing key "x" for TypedDict "C"
4193+
c2: C = {"x": "x", "y": 2}
4194+
c3: C = {"x": 1, "y": 2} # E: Incompatible types (expression has type "int", TypedDict item "x" has type "str")
4195+
4196+
class D(A, B2):
4197+
pass
4198+
d1: D = {"y": 1} # E: Missing key "x" for TypedDict "D"
4199+
d2: D = {"x": "x", "y": 2}
4200+
d3: D = {"x": 1, "y": 2} # E: Incompatible types (expression has type "int", TypedDict item "x" has type "str")
4201+
[builtins fixtures/dict.pyi]
4202+
[typing fixtures/typing-typeddict.pyi]
4203+
4204+
[case testTypedDictAliasDuplicateBases]
4205+
from typing import TypedDict
4206+
from typing_extensions import TypeAlias
4207+
4208+
class A(TypedDict):
4209+
x: str
4210+
4211+
A1 = A
4212+
A2 = A
4213+
A3: TypeAlias = A
4214+
4215+
class E(A1, A2): pass # E: Duplicate base class "A"
4216+
class F(A1, A3): pass # E: Duplicate base class "A"
4217+
class G(A, A1): pass # E: Duplicate base class "A"
4218+
4219+
class H(A, list): pass # E: All bases of a new TypedDict must be TypedDict types
4220+
[builtins fixtures/dict.pyi]
4221+
[typing fixtures/typing-typeddict.pyi]
4222+
4223+
[case testTypedDictAliasGeneric]
4224+
from typing import Generic, TypedDict, TypeVar
4225+
from typing_extensions import TypeAlias
4226+
4227+
_T = TypeVar("_T")
4228+
4229+
class A(Generic[_T], TypedDict):
4230+
x: _T
4231+
4232+
# This is by design - no_args aliases are only supported for instances
4233+
A0 = A
4234+
class B(A0[str]): # E: Bad number of arguments for type alias, expected 0, given 1
4235+
y: int
4236+
4237+
A1 = A[_T]
4238+
A2: TypeAlias = A[_T]
4239+
Aint = A[int]
4240+
4241+
class C(A1[_T]):
4242+
y: str
4243+
c1: C[int] = {"x": 0, "y": "a"}
4244+
c2: C[int] = {"x": "no", "y": "a"} # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int")
4245+
4246+
class D(A2[_T]):
4247+
y: str
4248+
d1: D[int] = {"x": 0, "y": "a"}
4249+
d2: D[int] = {"x": "no", "y": "a"} # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int")
4250+
4251+
class E(Aint):
4252+
y: str
4253+
e1: E = {"x": 0, "y": "a"}
4254+
e2: E = {"x": "no", "y": "a"}
4255+
[builtins fixtures/dict.pyi]
4256+
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)