Skip to content

Commit 43d2a38

Browse files
committed
Allow TypeVarTuple as type argument for subclasses of generic TypedDict (fixes #16975)
1 parent 3c87af2 commit 43d2a38

File tree

4 files changed

+79
-2
lines changed

4 files changed

+79
-2
lines changed

mypy/semanal_shared.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ def anal_type(
182182
allow_tuple_literal: bool = False,
183183
allow_unbound_tvars: bool = False,
184184
allow_required: bool = False,
185+
allow_unpack: bool = False,
185186
allow_placeholder: bool = False,
186187
report_invalid_types: bool = True,
187188
prohibit_self_type: str | None = None,

mypy/semanal_typeddict.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,17 @@ def analyze_base_args(self, base: IndexExpr, ctx: Context) -> list[Type] | None:
235235

236236
for arg_expr in args:
237237
try:
238-
type = expr_to_unanalyzed_type(arg_expr, self.options, self.api.is_stub_file)
238+
type = expr_to_unanalyzed_type(arg_expr, self.options,
239+
allow_new_syntax=self.api.is_stub_file,
240+
allow_unpack=True)
239241
except TypeTranslationError:
240242
self.fail("Invalid TypedDict type argument", ctx)
241243
return None
242244
analyzed = self.api.anal_type(
243-
type, allow_required=True, allow_placeholder=not self.api.is_func_scope()
245+
type,
246+
allow_required=True,
247+
allow_unpack=True,
248+
allow_placeholder=not self.api.is_func_scope()
244249
)
245250
if analyzed is None:
246251
return None
@@ -316,6 +321,7 @@ def analyze_typeddict_classdef_fields(
316321
analyzed = self.api.anal_type(
317322
stmt.type,
318323
allow_required=True,
324+
allow_unpack=True,
319325
allow_placeholder=not self.api.is_func_scope(),
320326
prohibit_self_type="TypedDict item type",
321327
)
@@ -520,6 +526,7 @@ def parse_typeddict_fields_with_types(
520526
analyzed = self.api.anal_type(
521527
type,
522528
allow_required=True,
529+
allow_unpack=True,
523530
allow_placeholder=not self.api.is_func_scope(),
524531
prohibit_self_type="TypedDict item type",
525532
)

test-data/unit/check-python311.test

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,38 @@ myclass3 = MyClass(float, float, float) # E: No overload variant of "MyClass" m
142142
# N: def [T1, T2] __init__(Type[T1], Type[T2], /) -> MyClass[T1, T2]
143143
reveal_type(myclass3) # N: Revealed type is "Any"
144144
[builtins fixtures/tuple.pyi]
145+
146+
[case testTypeVarTupleNewSyntaxTypedDict]
147+
# flags: --python-version 3.11
148+
from typing import Tuple, Callable, Generic
149+
from typing_extensions import TypeVarTuple, Unpack, TypedDict
150+
151+
Ts = TypeVarTuple("Ts")
152+
class A(TypedDict, Generic[*Ts, T]):
153+
fn: Callable[[*Ts], None]
154+
val: T
155+
156+
class B(A[*Ts, T]):
157+
gn: Callable[[*Ts], None]
158+
vals: Tuple[*Ts]
159+
160+
y: B[int, str]
161+
reveal_type(y) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (builtins.int), 'val': builtins.str, 'gn': def (builtins.int), 'vals': Tuple[builtins.int]})"
162+
reveal_type(y["gn"]) # N: Revealed type is "def (builtins.int)"
163+
reveal_type(y["vals"]) # N: Revealed type is "Tuple[builtins.int]"
164+
165+
z: B[*Tuple[int, ...]]
166+
reveal_type(z) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (*builtins.int), 'val': builtins.int, 'gn': def (*builtins.int), 'vals': builtins.tuple[builtins.int, ...]})"
167+
reveal_type(z["gn"]) # N: Revealed type is "def (*builtins.int)"
168+
169+
t: B[int, *Tuple[int, str], str]
170+
reveal_type(t) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (builtins.int, builtins.int, builtins.str), 'val': builtins.str, 'gn': def (builtins.int, builtins.int, builtins.str), 'vals': Tuple[builtins.int, builtins.int, builtins.str]})"
171+
172+
def ftest(x: int, y: str) -> None: ...
173+
def gtest(x: int, y: str) -> None: ...
174+
td = B({"fn": ftest, "val": 42, "gn": gtest, "vals": (6, "7")})
175+
reveal_type(td) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (builtins.int, builtins.str), 'val': builtins.int, 'gn': def (builtins.int, builtins.str), 'vals': Tuple[builtins.int, builtins.str]})"
176+
177+
def gbad() -> int: ...
178+
td2 = B({"fn": ftest, "val": 42, "gn": gbad, "vals": (6, "7")}) # E: Incompatible types (expression has type "Callable[[], int]", TypedDict item "gn" has type "Callable[[int, str], None]")
179+
[builtins fixtures/tuple.pyi]

test-data/unit/check-typevar-tuple.test

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,40 @@ def bad() -> int: ...
11551155
td2 = A({"fn": bad, "val": 42}) # E: Incompatible types (expression has type "Callable[[], int]", TypedDict item "fn" has type "Callable[[], None]")
11561156
[builtins fixtures/tuple.pyi]
11571157

1158+
[case testVariadicTypedDictExtending]
1159+
from typing import Tuple, Callable, Generic
1160+
from typing_extensions import TypeVarTuple, Unpack, TypedDict
1161+
1162+
Ts = TypeVarTuple("Ts")
1163+
class A(TypedDict, Generic[Unpack[Ts], T]):
1164+
fn: Callable[[Unpack[Ts]], None]
1165+
val: T
1166+
1167+
class B(A[Unpack[Ts], T]):
1168+
gn: Callable[[Unpack[Ts]], None]
1169+
vals: Tuple[Unpack[Ts]]
1170+
1171+
y: B[int, str]
1172+
reveal_type(y) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (builtins.int), 'val': builtins.str, 'gn': def (builtins.int), 'vals': Tuple[builtins.int]})"
1173+
reveal_type(y["gn"]) # N: Revealed type is "def (builtins.int)"
1174+
reveal_type(y["vals"]) # N: Revealed type is "Tuple[builtins.int]"
1175+
1176+
z: B[Unpack[Tuple[int, ...]]]
1177+
reveal_type(z) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (*builtins.int), 'val': builtins.int, 'gn': def (*builtins.int), 'vals': builtins.tuple[builtins.int, ...]})"
1178+
reveal_type(z["gn"]) # N: Revealed type is "def (*builtins.int)"
1179+
1180+
t: B[int, Unpack[Tuple[int, str]], str]
1181+
reveal_type(t) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (builtins.int, builtins.int, builtins.str), 'val': builtins.str, 'gn': def (builtins.int, builtins.int, builtins.str), 'vals': Tuple[builtins.int, builtins.int, builtins.str]})"
1182+
1183+
def ftest(x: int, y: str) -> None: ...
1184+
def gtest(x: int, y: str) -> None: ...
1185+
td = B({"fn": ftest, "val": 42, "gn": gtest, "vals": (6, "7")})
1186+
reveal_type(td) # N: Revealed type is "TypedDict('__main__.B', {'fn': def (builtins.int, builtins.str), 'val': builtins.int, 'gn': def (builtins.int, builtins.str), 'vals': Tuple[builtins.int, builtins.str]})"
1187+
1188+
def gbad() -> int: ...
1189+
td2 = B({"fn": ftest, "val": 42, "gn": gbad, "vals": (6, "7")}) # E: Incompatible types (expression has type "Callable[[], int]", TypedDict item "gn" has type "Callable[[int, str], None]")
1190+
[builtins fixtures/tuple.pyi]
1191+
11581192
[case testFixedUnpackWithRegularInstance]
11591193
from typing import Tuple, Generic, TypeVar
11601194
from typing_extensions import Unpack

0 commit comments

Comments
 (0)