From 591f377abe8a652fca30a631059e9d21b3d7a809 Mon Sep 17 00:00:00 2001 From: Erik Kemperman Date: Fri, 1 Mar 2024 09:42:36 +0100 Subject: [PATCH 1/2] Allow TypeVarTuple as type argument for subclasses of generic TypedDict (fixes #16975) --- mypy/semanal_shared.py | 1 + mypy/semanal_typeddict.py | 14 ++++++++-- test-data/unit/check-python311.test | 35 +++++++++++++++++++++++++ test-data/unit/check-typevar-tuple.test | 34 ++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py index b5ec2bb52a0d..01dcb7c9a67a 100644 --- a/mypy/semanal_shared.py +++ b/mypy/semanal_shared.py @@ -182,6 +182,7 @@ def anal_type( allow_tuple_literal: bool = False, allow_unbound_tvars: bool = False, allow_required: bool = False, + allow_unpack: bool = False, allow_placeholder: bool = False, report_invalid_types: bool = True, prohibit_self_type: str | None = None, diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index eee98d4d20fa..7ae3d368dd88 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -235,12 +235,20 @@ def analyze_base_args(self, base: IndexExpr, ctx: Context) -> list[Type] | None: for arg_expr in args: try: - type = expr_to_unanalyzed_type(arg_expr, self.options, self.api.is_stub_file) + type = expr_to_unanalyzed_type( + arg_expr, + self.options, + allow_new_syntax=self.api.is_stub_file, + allow_unpack=True, + ) except TypeTranslationError: self.fail("Invalid TypedDict type argument", ctx) return None analyzed = self.api.anal_type( - type, allow_required=True, allow_placeholder=not self.api.is_func_scope() + type, + allow_required=True, + allow_unpack=True, + allow_placeholder=not self.api.is_func_scope(), ) if analyzed is None: return None @@ -316,6 +324,7 @@ def analyze_typeddict_classdef_fields( analyzed = self.api.anal_type( stmt.type, allow_required=True, + allow_unpack=True, allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="TypedDict item type", ) @@ -520,6 +529,7 @@ def parse_typeddict_fields_with_types( analyzed = self.api.anal_type( type, allow_required=True, + allow_unpack=True, allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="TypedDict item type", ) diff --git a/test-data/unit/check-python311.test b/test-data/unit/check-python311.test index 2d1a09ef3336..4db0222f1918 100644 --- a/test-data/unit/check-python311.test +++ b/test-data/unit/check-python311.test @@ -173,3 +173,38 @@ Alias4 = Callable[[*IntList], int] # E: "List[int]" cannot be unpacked (must be x4: Alias4[int] # E: Bad number of arguments for type alias, expected 0, given 1 reveal_type(x4) # N: Revealed type is "def (*Unpack[builtins.tuple[Any, ...]]) -> builtins.int" [builtins fixtures/tuple.pyi] + +[case testTypeVarTupleNewSyntaxTypedDict] +# flags: --python-version 3.11 +from typing import Tuple, Callable, Generic +from typing_extensions import TypeVarTuple, Unpack, TypedDict + +Ts = TypeVarTuple("Ts") +class A(TypedDict, Generic[*Ts, T]): + fn: Callable[[*Ts], None] + val: T + +class B(A[*Ts, T]): + gn: Callable[[*Ts], None] + vals: Tuple[*Ts] + +y: B[int, str] +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]})" +reveal_type(y["gn"]) # N: Revealed type is "def (builtins.int)" +reveal_type(y["vals"]) # N: Revealed type is "Tuple[builtins.int]" + +z: B[*Tuple[int, ...]] +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, ...]})" +reveal_type(z["gn"]) # N: Revealed type is "def (*builtins.int)" + +t: B[int, *Tuple[int, str], str] +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]})" + +def ftest(x: int, y: str) -> None: ... +def gtest(x: int, y: str) -> None: ... +td = B({"fn": ftest, "val": 42, "gn": gtest, "vals": (6, "7")}) +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]})" + +def gbad() -> int: ... +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]") +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index f704e3c5c713..201a1af9983c 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -1155,6 +1155,40 @@ def bad() -> int: ... td2 = A({"fn": bad, "val": 42}) # E: Incompatible types (expression has type "Callable[[], int]", TypedDict item "fn" has type "Callable[[], None]") [builtins fixtures/tuple.pyi] +[case testVariadicTypedDictExtending] +from typing import Tuple, Callable, Generic +from typing_extensions import TypeVarTuple, Unpack, TypedDict + +Ts = TypeVarTuple("Ts") +class A(TypedDict, Generic[Unpack[Ts], T]): + fn: Callable[[Unpack[Ts]], None] + val: T + +class B(A[Unpack[Ts], T]): + gn: Callable[[Unpack[Ts]], None] + vals: Tuple[Unpack[Ts]] + +y: B[int, str] +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]})" +reveal_type(y["gn"]) # N: Revealed type is "def (builtins.int)" +reveal_type(y["vals"]) # N: Revealed type is "Tuple[builtins.int]" + +z: B[Unpack[Tuple[int, ...]]] +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, ...]})" +reveal_type(z["gn"]) # N: Revealed type is "def (*builtins.int)" + +t: B[int, Unpack[Tuple[int, str]], str] +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]})" + +def ftest(x: int, y: str) -> None: ... +def gtest(x: int, y: str) -> None: ... +td = B({"fn": ftest, "val": 42, "gn": gtest, "vals": (6, "7")}) +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]})" + +def gbad() -> int: ... +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]") +[builtins fixtures/tuple.pyi] + [case testFixedUnpackWithRegularInstance] from typing import Tuple, Generic, TypeVar from typing_extensions import Unpack From 6f92522942b447f19eaa57d42e8ed2a528d089ca Mon Sep 17 00:00:00 2001 From: Erik Kemperman Date: Wed, 8 May 2024 16:47:01 +0200 Subject: [PATCH 2/2] Change two uncertain allow_unpack=True kwargs to TODO comments --- mypy/semanal_typeddict.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 7ae3d368dd88..616d427da45a 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -324,7 +324,7 @@ def analyze_typeddict_classdef_fields( analyzed = self.api.anal_type( stmt.type, allow_required=True, - allow_unpack=True, + # TODO allow_unpack=True ? allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="TypedDict item type", ) @@ -529,7 +529,7 @@ def parse_typeddict_fields_with_types( analyzed = self.api.anal_type( type, allow_required=True, - allow_unpack=True, + # TODO allow_unpack=True ? allow_placeholder=not self.api.is_func_scope(), prohibit_self_type="TypedDict item type", )