diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index f70bbe427124f..1b076ac4835d8 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -67,16 +67,30 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> Tuple[bool, Optional[Typ defn.analyzed.line = defn.line defn.analyzed.column = defn.column return True, info + # Extending/merging existing TypedDicts - if any(not isinstance(expr, RefExpr) or - expr.fullname not in TPDICT_NAMES and - not self.is_typeddict(expr) for expr in defn.base_type_exprs): - self.fail("All bases of a new TypedDict must be TypedDict types", defn) - typeddict_bases = list(filter(self.is_typeddict, defn.base_type_exprs)) + 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('Duplicate base class "%s"' % 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) @@ -328,3 +342,6 @@ def is_typeddict(self, expr: Expression) -> bool: def fail(self, msg: str, ctx: Context, *, code: Optional[ErrorCode] = None) -> None: self.api.fail(msg, ctx, code=code) + + def note(self, msg: str, ctx: Context) -> None: + self.api.note(msg, ctx) diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 04bbc9b2b4d4b..5fa5514796a01 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -171,6 +171,21 @@ p: Point2D reveal_type(p) # N: Revealed type is "TypedDict('__main__.Point2D', {'x': builtins.int, 'y': builtins.int})" [builtins fixtures/dict.pyi] +[case testCannotCreateTypedDictWithDuplicateBases] +# https://github.com/python/mypy/issues/3673 +from typing import TypedDict + +class A(TypedDict): + x: str + y: int + +class B(A, A): # E: Duplicate base class "A" + z: str + +class C(TypedDict, TypedDict): # E: Duplicate base class "TypedDict" + c1: int +[typing fixtures/typing-typeddict.pyi] + [case testCannotCreateTypedDictWithClassWithOtherStuff] # flags: --python-version 3.6 from mypy_extensions import TypedDict