Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat TypedDict (old-style) aliases as regular TypedDicts #18852

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 40 additions & 31 deletions mypy/semanal_typeddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
StrExpr,
TempNode,
TupleExpr,
TypeAlias,
TypedDictExpr,
TypeInfo,
)
Expand All @@ -50,6 +51,7 @@
TypedDictType,
TypeOfAny,
TypeVarLikeType,
get_proper_type,
)

TPDICT_CLASS_ERROR: Final = (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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).

Expand Down Expand Up @@ -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:
Expand Down
103 changes: 103 additions & 0 deletions test-data/unit/check-typeddict.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]