Skip to content

Commit 2907a4d

Browse files
Check duplicate bases when defining TypedDict (#11485)
### Description Closes #3673 Adds a trivial for-loop and a helper set to check duplicates of `typeddict_bases`. ## Test Plan Adds a new test case `testCannotCreateTypedDictWithDuplicateBases`
1 parent 4e2f4ff commit 2907a4d

File tree

2 files changed

+38
-6
lines changed

2 files changed

+38
-6
lines changed

mypy/semanal_typeddict.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,30 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[Typ
6767
defn.analyzed.line = defn.line
6868
defn.analyzed.column = defn.column
6969
return True, info
70+
7071
# Extending/merging existing TypedDicts
71-
if any(not isinstance(expr, RefExpr) or
72-
expr.fullname not in TPDICT_NAMES and
73-
not self.is_typeddict(expr) for expr in defn.base_type_exprs):
74-
self.fail("All bases of a new TypedDict must be TypedDict types", defn)
75-
typeddict_bases = list(filter(self.is_typeddict, defn.base_type_exprs))
72+
typeddict_bases = []
73+
typeddict_bases_set = set()
74+
for expr in defn.base_type_exprs:
75+
if isinstance(expr, RefExpr) and expr.fullname in TPDICT_NAMES:
76+
if 'TypedDict' not in typeddict_bases_set:
77+
typeddict_bases_set.add('TypedDict')
78+
else:
79+
self.fail('Duplicate base class "TypedDict"', defn)
80+
elif isinstance(expr, RefExpr) and self.is_typeddict(expr):
81+
assert expr.fullname
82+
if expr.fullname not in typeddict_bases_set:
83+
typeddict_bases_set.add(expr.fullname)
84+
typeddict_bases.append(expr)
85+
else:
86+
assert isinstance(expr.node, TypeInfo)
87+
self.fail('Duplicate base class "%s"' % expr.node.name, defn)
88+
else:
89+
self.fail("All bases of a new TypedDict must be TypedDict types", defn)
90+
7691
keys: List[str] = []
7792
types = []
7893
required_keys = set()
79-
8094
# Iterate over bases in reverse order so that leftmost base class' keys take precedence
8195
for base in reversed(typeddict_bases):
8296
assert isinstance(base, RefExpr)
@@ -328,3 +342,6 @@ def is_typeddict(self, expr: Expression) -> bool:
328342

329343
def fail(self, msg: str, ctx: Context, *, code: Optional[ErrorCode] = None) -> None:
330344
self.api.fail(msg, ctx, code=code)
345+
346+
def note(self, msg: str, ctx: Context) -> None:
347+
self.api.note(msg, ctx)

test-data/unit/check-typeddict.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,21 @@ p: Point2D
171171
reveal_type(p) # N: Revealed type is "TypedDict('__main__.Point2D', {'x': builtins.int, 'y': builtins.int})"
172172
[builtins fixtures/dict.pyi]
173173

174+
[case testCannotCreateTypedDictWithDuplicateBases]
175+
# https://github.com/python/mypy/issues/3673
176+
from typing import TypedDict
177+
178+
class A(TypedDict):
179+
x: str
180+
y: int
181+
182+
class B(A, A): # E: Duplicate base class "A"
183+
z: str
184+
185+
class C(TypedDict, TypedDict): # E: Duplicate base class "TypedDict"
186+
c1: int
187+
[typing fixtures/typing-typeddict.pyi]
188+
174189
[case testCannotCreateTypedDictWithClassWithOtherStuff]
175190
# flags: --python-version 3.6
176191
from mypy_extensions import TypedDict

0 commit comments

Comments
 (0)