Skip to content

Commit 62e2ee6

Browse files
committed
Handle generics (except for TypeAlias without explicit params - that does not work?
1 parent 2b05866 commit 62e2ee6

File tree

2 files changed

+99
-46
lines changed

2 files changed

+99
-46
lines changed

mypy/semanal_typeddict.py

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,18 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
139139
typeddict_bases_set.add("TypedDict")
140140
else:
141141
self.fail('Duplicate base class "TypedDict"', defn)
142-
elif isinstance(expr, RefExpr) and self.is_typeddict(expr):
143-
assert expr.fullname
144-
if expr.fullname not in typeddict_bases_set:
145-
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)
146151
typeddict_bases.append(expr)
147152
else:
148-
assert isinstance(expr.node, TypeInfo)
149-
self.fail(f'Duplicate base class "{expr.node.name}"', defn)
150-
elif isinstance(expr, IndexExpr) and self.is_typeddict(expr.base):
151-
assert isinstance(expr.base, RefExpr)
152-
assert expr.base.fullname
153-
if expr.base.fullname not in typeddict_bases_set:
154-
typeddict_bases_set.add(expr.base.fullname)
155-
typeddict_bases.append(expr)
156-
else:
157-
assert isinstance(expr.base.node, TypeInfo)
158-
self.fail(f'Duplicate base class "{expr.base.node.name}"', defn)
153+
self.fail(f'Duplicate base class "{info.name}"', defn)
159154
else:
160155
self.fail("All bases of a new TypedDict must be TypedDict types", defn)
161156

@@ -192,30 +187,13 @@ def add_keys_and_types_from_base(
192187
readonly_keys: set[str],
193188
ctx: Context,
194189
) -> None:
190+
info = self._parse_typeddict_base(base, ctx)
195191
base_args: list[Type] = []
196-
if isinstance(base, RefExpr):
197-
if isinstance(base.node, TypeAlias):
198-
# Only old TypeAlias / plain assignment, PEP695 `type` stmt
199-
# cannot be used as a base class
200-
target = get_proper_type(base.node.target)
201-
assert isinstance(target, TypedDictType)
202-
info = target.fallback.type
203-
elif isinstance(base.node, TypeInfo):
204-
info = base.node
205-
else:
206-
assert False
207-
elif isinstance(base, IndexExpr):
208-
assert isinstance(base.base, RefExpr)
209-
assert isinstance(base.base.node, TypeInfo)
210-
info = base.base.node
192+
if isinstance(base, IndexExpr):
211193
args = self.analyze_base_args(base, ctx)
212194
if args is None:
213195
return
214196
base_args = args
215-
else:
216-
assert isinstance(base, CallExpr)
217-
assert isinstance(base.analyzed, TypedDictExpr)
218-
info = base.analyzed.info
219197

220198
assert info.typeddict_type is not None
221199
base_typed_dict = info.typeddict_type
@@ -241,6 +219,26 @@ def add_keys_and_types_from_base(
241219
required_keys.update(base_typed_dict.required_keys)
242220
readonly_keys.update(base_typed_dict.readonly_keys)
243221

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+
244242
def analyze_base_args(self, base: IndexExpr, ctx: Context) -> list[Type] | None:
245243
"""Analyze arguments of base type expressions as types.
246244
@@ -537,7 +535,7 @@ def parse_typeddict_args(
537535
return "", [], [], True, [], False
538536
dictexpr = args[1]
539537
tvar_defs = self.api.get_and_bind_all_tvars([t for k, t in dictexpr.items])
540-
res = self.parse_typeddict_fields_with_types(dictexpr.items, call)
538+
res = self.parse_typeddict_fields_with_types(dictexpr.items)
541539
if res is None:
542540
# One of the types is not ready, defer.
543541
return None
@@ -546,7 +544,7 @@ def parse_typeddict_args(
546544
return args[0].value, items, types, total, tvar_defs, ok
547545

548546
def parse_typeddict_fields_with_types(
549-
self, dict_items: list[tuple[Expression | None, Expression]], context: Context
547+
self, dict_items: list[tuple[Expression | None, Expression]]
550548
) -> tuple[list[str], list[Type], bool] | None:
551549
"""Parse typed dict items passed as pairs (name expression, type expression).
552550

test-data/unit/check-typeddict.test

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4162,22 +4162,77 @@ c23: Child2 = {"foo": 0, "bar": 1, "baz": "error"} # E: Extra key "baz" for Typ
41624162
[builtins fixtures/dict.pyi]
41634163
[typing fixtures/typing-typeddict.pyi]
41644164

4165-
[case testTypedDictAliasMultiple]
4165+
[case testTypedDictAliasInheritance]
41664166
from typing import TypedDict
4167+
from typing_extensions import TypeAlias
41674168

4168-
class A(TypedDict, total=False):
4169-
y: int
4170-
class B(TypedDict, total=False):
4169+
class A(TypedDict):
41714170
x: str
4171+
class B(TypedDict):
4172+
y: int
4173+
4174+
B1 = B
4175+
B2: TypeAlias = B
41724176

4173-
D = B
4177+
class C(A, B1):
4178+
pass
4179+
c1: C = {"y": 1} # E: Missing key "x" for TypedDict "C"
4180+
c2: C = {"x": "x", "y": 2}
4181+
c3: C = {"x": 1, "y": 2} # E: Incompatible types (expression has type "int", TypedDict item "x" has type "str")
41744182

4175-
class C(A, D):
4183+
class D(A, B2):
41764184
pass
4185+
d1: D = {"y": 1} # E: Missing key "x" for TypedDict "D"
4186+
d2: D = {"x": "x", "y": 2}
4187+
d3: D = {"x": 1, "y": 2} # E: Incompatible types (expression has type "int", TypedDict item "x" has type "str")
4188+
[builtins fixtures/dict.pyi]
4189+
[typing fixtures/typing-typeddict.pyi]
4190+
4191+
[case testTypedDictAliasDuplicateBases]
4192+
from typing import TypedDict
4193+
from typing_extensions import TypeAlias
4194+
4195+
class A(TypedDict):
4196+
x: str
4197+
4198+
A1 = A
4199+
A2 = A
4200+
A3: TypeAlias = A
4201+
4202+
class E(A1, A2): pass # E: Duplicate base class "A"
4203+
class F(A1, A3): pass # E: Duplicate base class "A"
4204+
class G(A, A1): pass # E: Duplicate base class "A"
4205+
4206+
class H(A, list): pass # E: All bases of a new TypedDict must be TypedDict types
4207+
[builtins fixtures/dict.pyi]
4208+
[typing fixtures/typing-typeddict.pyi]
4209+
4210+
[case testTypedDictAliasGeneric]
4211+
from typing import Generic, TypedDict, TypeVar
4212+
from typing_extensions import TypeAlias
41774213

4178-
c1: C = {}
4179-
c2: C = {"x": "x"}
4180-
c3: C = {"y": 1}
4181-
c4: C = {"x": "x", "y": 2}
4214+
_T = TypeVar("_T")
4215+
4216+
class A(Generic[_T], TypedDict):
4217+
x: _T
4218+
4219+
A1 = A[_T]
4220+
A2: TypeAlias = A[_T]
4221+
Aint = A[int]
4222+
4223+
class C(A1[_T]):
4224+
y: str
4225+
c1: C[int] = {"x": 0, "y": "a"}
4226+
c2: C[int] = {"x": "no", "y": "a"} # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int")
4227+
4228+
class D(A2[_T]):
4229+
y: str
4230+
d1: D[int] = {"x": 0, "y": "a"}
4231+
d2: D[int] = {"x": "no", "y": "a"} # E: Incompatible types (expression has type "str", TypedDict item "x" has type "int")
4232+
4233+
class E(Aint):
4234+
y: str
4235+
e1: E = {"x": 0, "y": "a"}
4236+
e2: E = {"x": "no", "y": "a"}
41824237
[builtins fixtures/dict.pyi]
41834238
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)