From 6cfdc5feffced6ec957a00ff7a3a0b839d6e0401 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Sat, 7 Aug 2021 08:20:40 -0500 Subject: [PATCH 01/35] Allow TypedDict to inherit from Generic --- Lib/test/_typed_dict_helper.py | 7 ++++++- Lib/test/test_typing.py | 29 ++++++++++++++++++++++++++--- Lib/typing.py | 2 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Lib/test/_typed_dict_helper.py b/Lib/test/_typed_dict_helper.py index d333db193183eb..c11ecc90d0bee7 100644 --- a/Lib/test/_typed_dict_helper.py +++ b/Lib/test/_typed_dict_helper.py @@ -10,9 +10,14 @@ class Bar(_typed_dict_helper.Foo, total=False): from __future__ import annotations -from typing import Optional, TypedDict +from typing import Generic, Optional, TypedDict, TypeVar OptionalIntType = Optional[int] class Foo(TypedDict): a: OptionalIntType + +OptionableT = TypeVar("OptionableT") + +class FooGeneric(TypedDict, Generic[OptionableT]): + a: Optional[OptionableT] diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index fbdf634c5c3be8..39312780a0e41b 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2943,13 +2943,22 @@ def __add__(self, other): Label = TypedDict('Label', [('label', str)]) +TDG = TypeVar("TDG") + class Point2D(TypedDict): x: int y: int +class Point2DGeneric(Generic[TDG], TypedDict): + a: TDG + b: TDG + class Bar(_typed_dict_helper.Foo, total=False): b: int +class BarGeneric(_typed_dict_helper.FooGeneric[TDG], total=False): + b: int + class LabelPoint2D(Point2D, Label): ... class Options(TypedDict, total=False): @@ -4070,7 +4079,7 @@ def test_basics_functional_syntax(self): self.assertEqual(jim['id'], 1) self.assertEqual(Emp.__name__, 'Emp') self.assertEqual(Emp.__module__, __name__) - self.assertEqual(Emp.__bases__, (dict,)) + self.assertIn(dict, Emp.__bases__) self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) self.assertEqual(Emp.__total__, True) @@ -4085,7 +4094,7 @@ def test_basics_keywords_syntax(self): self.assertEqual(jim['id'], 1) self.assertEqual(Emp.__name__, 'Emp') self.assertEqual(Emp.__module__, __name__) - self.assertEqual(Emp.__bases__, (dict,)) + self.assertIn(dict, Emp.__bases__) self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) self.assertEqual(Emp.__total__, True) @@ -4135,7 +4144,7 @@ def test_py36_class_syntax_usage(self): self.assertEqual(LabelPoint2D.__name__, 'LabelPoint2D') self.assertEqual(LabelPoint2D.__module__, __name__) self.assertEqual(LabelPoint2D.__annotations__, {'x': int, 'y': int, 'label': str}) - self.assertEqual(LabelPoint2D.__bases__, (dict,)) + self.assertIn(dict, LabelPoint2D.__bases__) self.assertEqual(LabelPoint2D.__total__, True) self.assertNotIsSubclass(LabelPoint2D, typing.Sequence) not_origin = Point2D(x=0, y=1) @@ -4157,6 +4166,17 @@ def test_pickle(self): EmpDnew = pickle.loads(ZZ) self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane) + def test_pickle_generic(self): + point = Point2DGeneric(a=5.0, b=3.0) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + z = pickle.dumps(point, proto) + point2 = pickle.loads(z) + self.assertEqual(point2, point) + self.assertEqual(point2, {'a': 5.0, 'b': 3.0}) + ZZ = pickle.dumps(Point2DGeneric, proto) + Point2DGenericNew = pickle.loads(ZZ) + self.assertEqual(Point2DGenericNew({'a': 5.0, 'b': 3.0}), point) + def test_optional(self): EmpD = TypedDict('EmpD', name=str, id=int) @@ -4228,6 +4248,9 @@ def test_get_type_hints(self): {'a': typing.Optional[int], 'b': int} ) + def test_get_type_hints_generic(self): + assert set(get_type_hints(BarGeneric)) == {'a', 'b'} + class IOTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 702bb647269d0b..f0c950d33d6235 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2252,7 +2252,7 @@ def __new__(cls, name, bases, ns, total=True): Subclasses and instances of TypedDict return actual dictionaries. """ for base in bases: - if type(base) is not _TypedDictMeta: + if not (type(base) is _TypedDictMeta or base is Generic): raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') tp_dict = type.__new__(_TypedDictMeta, name, (dict,), ns) From c022e44bc52ade798f79d2be5b127a1be035d2d5 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Sat, 7 Aug 2021 08:29:29 -0500 Subject: [PATCH 02/35] TypedDict preserve MRO --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index f0c950d33d6235..3a077b11227804 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2255,7 +2255,7 @@ def __new__(cls, name, bases, ns, total=True): if not (type(base) is _TypedDictMeta or base is Generic): raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') - tp_dict = type.__new__(_TypedDictMeta, name, (dict,), ns) + tp_dict = type.__new__(_TypedDictMeta, name, bases + (dict,), ns) annotations = {} own_annotations = ns.get('__annotations__', {}) From 2fa00b81ea4df893be75f0a77e250e859e5e282b Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Thu, 2 Sep 2021 20:01:31 -0500 Subject: [PATCH 03/35] Fix base classes to be just generic and dict --- Lib/typing.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 3a077b11227804..10f895e6075ce3 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2255,7 +2255,15 @@ def __new__(cls, name, bases, ns, total=True): if not (type(base) is _TypedDictMeta or base is Generic): raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') - tp_dict = type.__new__(_TypedDictMeta, name, bases + (dict,), ns) + + generic_base = tuple(b for b in bases if b is Generic) + base_typevars = _collect_type_vars(ns.get("__orig_bases__", ())) + if not generic_base and base_typevars: + # If not directly inheriting from a Generic + # but inheriting from a generic TypedDict + generic_alias = Generic.__class_getitem__(base_typevars), + generic_base = types.resolve_bases(generic_alias) + tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns) annotations = {} own_annotations = ns.get('__annotations__', {}) From b829d8aa1e5e4e4f5e7e80938be8fa562412c681 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Fri, 3 Sep 2021 06:50:56 -0500 Subject: [PATCH 04/35] Simplify addiotion of Generic base class --- Lib/typing.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 10f895e6075ce3..40665e9bbdc528 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2256,13 +2256,7 @@ def __new__(cls, name, bases, ns, total=True): raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') - generic_base = tuple(b for b in bases if b is Generic) - base_typevars = _collect_type_vars(ns.get("__orig_bases__", ())) - if not generic_base and base_typevars: - # If not directly inheriting from a Generic - # but inheriting from a generic TypedDict - generic_alias = Generic.__class_getitem__(base_typevars), - generic_base = types.resolve_bases(generic_alias) + generic_base = (Generic,) if any(issubclass(b, Generic) for b in bases) else () tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns) annotations = {} From c5762dfe99f282e8bf6a397fb48423963a3a1dd6 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Fri, 3 Sep 2021 07:08:12 -0500 Subject: [PATCH 05/35] Fully proxy origin by using types.GenericAlias --- Lib/typing.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 40665e9bbdc528..e08717a8ef29a0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2296,6 +2296,15 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ + def __getitem__(cls, params): + if not isinstance(params, tuple): + params = (params,) + msg = "Parameters to generic types must be types." + params = tuple(_type_check(p, msg) for p in params) + nparams = len(cls.__dict__.get("__parameters__", ())) + _check_generic(cls, params, nparams) + return types.GenericAlias(cls, params) + def TypedDict(typename, fields=None, /, *, total=True, **kwargs): """A simple typed namespace. At runtime it is equivalent to a plain dict. From 4c5db0a9de4adce90bd01fa924fae23522e418a0 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Fri, 3 Sep 2021 07:37:14 -0500 Subject: [PATCH 06/35] Add NEWS blurb --- .../NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst diff --git a/Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst b/Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst new file mode 100644 index 00000000000000..2986646398ae92 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst @@ -0,0 +1 @@ +Allow ``TypedDict`` to also inherit from ``Generic``. From 34189dd70d156452c656e6ba82524362b3589e29 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Fri, 3 Sep 2021 07:57:25 -0500 Subject: [PATCH 07/35] Add better NEWS blurb --- .../next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst | 1 - .../next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst create mode 100644 Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst diff --git a/Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst b/Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst deleted file mode 100644 index 2986646398ae92..00000000000000 --- a/Misc/NEWS.d/next/Library/2021-09-03-07-36-54.bpo-44863.VG4PPH.rst +++ /dev/null @@ -1 +0,0 @@ -Allow ``TypedDict`` to also inherit from ``Generic``. diff --git a/Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst b/Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst new file mode 100644 index 00000000000000..c79d3481bb841a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst @@ -0,0 +1,4 @@ +Allow ``TypedDict`` subclasses to also include ``Generic`` as a base class +in class based syntax. Thereby allowing the user to define a ``Generic +TypedDict``, just like a user defined ``Generic class`` but with +``TypedDict`` semantics. From 0cfd637d7b5fce9ddcbe9fa8e28ad903828ab9d4 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Fri, 3 Sep 2021 17:32:19 -0500 Subject: [PATCH 08/35] Add more testcases --- Lib/test/_typed_dict_helper.py | 6 ++--- Lib/test/test_typing.py | 49 +++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/Lib/test/_typed_dict_helper.py b/Lib/test/_typed_dict_helper.py index c11ecc90d0bee7..2b2e21a078e390 100644 --- a/Lib/test/_typed_dict_helper.py +++ b/Lib/test/_typed_dict_helper.py @@ -17,7 +17,7 @@ class Bar(_typed_dict_helper.Foo, total=False): class Foo(TypedDict): a: OptionalIntType -OptionableT = TypeVar("OptionableT") +T = TypeVar("T") -class FooGeneric(TypedDict, Generic[OptionableT]): - a: Optional[OptionableT] +class FooGeneric(TypedDict, Generic[T]): + a: Optional[T] diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 39312780a0e41b..35da41fa2633c3 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2943,20 +2943,18 @@ def __add__(self, other): Label = TypedDict('Label', [('label', str)]) -TDG = TypeVar("TDG") - class Point2D(TypedDict): x: int y: int -class Point2DGeneric(Generic[TDG], TypedDict): - a: TDG - b: TDG +class Point2DGeneric(Generic[T], TypedDict): + a: T + b: T class Bar(_typed_dict_helper.Foo, total=False): b: int -class BarGeneric(_typed_dict_helper.FooGeneric[TDG], total=False): +class BarGeneric(_typed_dict_helper.FooGeneric[T], total=False): b: int class LabelPoint2D(Point2D, Label): ... @@ -4249,7 +4247,44 @@ def test_get_type_hints(self): ) def test_get_type_hints_generic(self): - assert set(get_type_hints(BarGeneric)) == {'a', 'b'} + self.assertEqual( + get_type_hints(BarGeneric[int]), + {'a': typing.Optional[T], 'b': int} + ) + + def test_generic(self): + class A(TypedDict, Generic[T]): + a: T + + self.assertEqual(A.__parameters__, (T,)) + self.assertEqual(A[str].__parameters__, ()) + self.assertEqual(A[str].__args__, (str,)) + + class B(A[KT], total=False): + b: KT + + self.assertEqual(B.__total__, False) + self.assertEqual(B.__parameters__, (KT, )) + self.assertEqual(B.__optional_keys__, frozenset(['b'])) + self.assertEqual(B.__required_keys__, frozenset(['a'])) + + self.assertEqual(B[str].__parameters__, ()) + self.assertEqual(B[str].__args__, (str,)) + self.assertEqual(B[str].__optional_keys__, frozenset(['b'])) + self.assertEqual(B[str].__required_keys__, frozenset(['a'])) + + class C(B[int]): + c: int + + self.assertEqual(C.__total__, True) + self.assertEqual(C.__parameters__, ()) + self.assertEqual(C.__optional_keys__, frozenset(['b'])) + self.assertEqual(C.__required_keys__, frozenset(['a', 'c'])) + assert C.__annotations__ == { + 'a': T, + 'b': KT, + 'c': int, + } class IOTests(BaseTestCase): From 5e808af25517f876f993858ebc0affd6372936f3 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Sat, 4 Sep 2021 08:16:18 -0500 Subject: [PATCH 09/35] Add implicit any test case --- Lib/test/test_typing.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 35da41fa2633c3..acc6d9a262820a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4286,6 +4286,19 @@ class C(B[int]): 'c': int, } + class WithImplicitAny(B): + c: int + + self.assertEqual(WithImplicitAny.__total__, True) + self.assertEqual(WithImplicitAny.__parameters__, ()) + self.assertEqual(WithImplicitAny.__optional_keys__, frozenset(['b'])) + self.assertEqual(WithImplicitAny.__required_keys__, frozenset(['a', 'c'])) + assert WithImplicitAny.__annotations__ == { + 'a': T, + 'b': KT, + 'c': int, + } + class IOTests(BaseTestCase): From a2482ff8a2533c36fefdf4881671981fe012f301 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Sat, 4 Sep 2021 09:54:30 -0500 Subject: [PATCH 10/35] Fix issue with plain generic inheritance --- Lib/test/test_typing.py | 10 ++++++---- Lib/typing.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index acc6d9a262820a..b365b7847cc180 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4252,7 +4252,7 @@ def test_get_type_hints_generic(self): {'a': typing.Optional[T], 'b': int} ) - def test_generic(self): + def test_generic_inheritance(self): class A(TypedDict, Generic[T]): a: T @@ -4277,7 +4277,6 @@ class C(B[int]): c: int self.assertEqual(C.__total__, True) - self.assertEqual(C.__parameters__, ()) self.assertEqual(C.__optional_keys__, frozenset(['b'])) self.assertEqual(C.__required_keys__, frozenset(['a', 'c'])) assert C.__annotations__ == { @@ -4285,12 +4284,13 @@ class C(B[int]): 'b': KT, 'c': int, } + with self.assertRaises(TypeError): + C[str] class WithImplicitAny(B): c: int self.assertEqual(WithImplicitAny.__total__, True) - self.assertEqual(WithImplicitAny.__parameters__, ()) self.assertEqual(WithImplicitAny.__optional_keys__, frozenset(['b'])) self.assertEqual(WithImplicitAny.__required_keys__, frozenset(['a', 'c'])) assert WithImplicitAny.__annotations__ == { @@ -4298,7 +4298,9 @@ class WithImplicitAny(B): 'b': KT, 'c': int, } - + with self.assertRaises(TypeError): + WithImplicitAny[str] + class IOTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index e08717a8ef29a0..56ef55afac41bf 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2256,7 +2256,7 @@ def __new__(cls, name, bases, ns, total=True): raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') - generic_base = (Generic,) if any(issubclass(b, Generic) for b in bases) else () + generic_base = (Generic,) if _collect_type_vars(ns.get("__orig_bases__", ())) else () tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns) annotations = {} From 071c1b6a72b04b459747ca8976b5caa7111b7612 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Sat, 4 Sep 2021 17:21:20 -0500 Subject: [PATCH 11/35] Include bases assertion --- Lib/test/test_typing.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index b365b7847cc180..72cf3f15c49ed3 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4077,7 +4077,7 @@ def test_basics_functional_syntax(self): self.assertEqual(jim['id'], 1) self.assertEqual(Emp.__name__, 'Emp') self.assertEqual(Emp.__module__, __name__) - self.assertIn(dict, Emp.__bases__) + self.assertEqual(Emp.__bases__, (dict,)) self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) self.assertEqual(Emp.__total__, True) @@ -4092,7 +4092,7 @@ def test_basics_keywords_syntax(self): self.assertEqual(jim['id'], 1) self.assertEqual(Emp.__name__, 'Emp') self.assertEqual(Emp.__module__, __name__) - self.assertIn(dict, Emp.__bases__) + self.assertEqual(Emp.__bases__, (dict,)) self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) self.assertEqual(Emp.__total__, True) @@ -4142,7 +4142,7 @@ def test_py36_class_syntax_usage(self): self.assertEqual(LabelPoint2D.__name__, 'LabelPoint2D') self.assertEqual(LabelPoint2D.__module__, __name__) self.assertEqual(LabelPoint2D.__annotations__, {'x': int, 'y': int, 'label': str}) - self.assertIn(dict, LabelPoint2D.__bases__) + self.assertEqual(LabelPoint2D.__bases__, (dict,)) self.assertEqual(LabelPoint2D.__total__, True) self.assertNotIsSubclass(LabelPoint2D, typing.Sequence) not_origin = Point2D(x=0, y=1) @@ -4256,6 +4256,7 @@ def test_generic_inheritance(self): class A(TypedDict, Generic[T]): a: T + self.assertEqual(A.__bases__, (Generic, dict,)) self.assertEqual(A.__parameters__, (T,)) self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) @@ -4263,6 +4264,7 @@ class A(TypedDict, Generic[T]): class B(A[KT], total=False): b: KT + self.assertEqual(B.__bases__, (Generic, dict,)) self.assertEqual(B.__total__, False) self.assertEqual(B.__parameters__, (KT, )) self.assertEqual(B.__optional_keys__, frozenset(['b'])) @@ -4276,6 +4278,7 @@ class B(A[KT], total=False): class C(B[int]): c: int + self.assertEqual(C.__bases__, (dict,)) self.assertEqual(C.__total__, True) self.assertEqual(C.__optional_keys__, frozenset(['b'])) self.assertEqual(C.__required_keys__, frozenset(['a', 'c'])) @@ -4290,6 +4293,7 @@ class C(B[int]): class WithImplicitAny(B): c: int + self.assertEqual(WithImplicitAny.__bases__, (dict,)) self.assertEqual(WithImplicitAny.__total__, True) self.assertEqual(WithImplicitAny.__optional_keys__, frozenset(['b'])) self.assertEqual(WithImplicitAny.__required_keys__, frozenset(['a', 'c'])) From 3ce517bf0fb89ec841e56a7e46bcaa63df4d9533 Mon Sep 17 00:00:00 2001 From: Samodya Abey <379594+sransara@users.noreply.github.com> Date: Sun, 5 Sep 2021 03:34:39 -0500 Subject: [PATCH 12/35] Update NEWS blurb with better formatting Co-authored-by: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> --- .../next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst b/Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst index c79d3481bb841a..130856587fd918 100644 --- a/Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst +++ b/Misc/NEWS.d/next/Library/2021-09-03-07-56-48.bpo-44863.udgz95.rst @@ -1,4 +1,4 @@ -Allow ``TypedDict`` subclasses to also include ``Generic`` as a base class -in class based syntax. Thereby allowing the user to define a ``Generic -TypedDict``, just like a user defined ``Generic class`` but with -``TypedDict`` semantics. +Allow :class:`~typing.TypedDict` subclasses to also include +:class:`~typing.Generic` as a base class in class based syntax. Thereby allowing +the user to define a generic ``TypedDict``, just like a user-defined generic but +with ``TypedDict`` semantics. From 2f003e01166eb88988f2860a94b0be54ab5a5ecd Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Sun, 5 Sep 2021 05:23:59 -0500 Subject: [PATCH 13/35] Revert overriding of getitem and not proxy dunders --- Lib/test/test_typing.py | 13 ++++++------- Lib/typing.py | 9 --------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 72cf3f15c49ed3..f424cec7dbf2e8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4248,7 +4248,7 @@ def test_get_type_hints(self): def test_get_type_hints_generic(self): self.assertEqual( - get_type_hints(BarGeneric[int]), + get_type_hints(BarGeneric[int].__origin__), {'a': typing.Optional[T], 'b': int} ) @@ -4272,8 +4272,7 @@ class B(A[KT], total=False): self.assertEqual(B[str].__parameters__, ()) self.assertEqual(B[str].__args__, (str,)) - self.assertEqual(B[str].__optional_keys__, frozenset(['b'])) - self.assertEqual(B[str].__required_keys__, frozenset(['a'])) + self.assertEqual(B[str].__origin__, B) class C(B[int]): c: int @@ -4287,8 +4286,8 @@ class C(B[int]): 'b': KT, 'c': int, } - with self.assertRaises(TypeError): - C[str] + # with self.assertRaises(TypeError): + # C[str] class WithImplicitAny(B): c: int @@ -4302,8 +4301,8 @@ class WithImplicitAny(B): 'b': KT, 'c': int, } - with self.assertRaises(TypeError): - WithImplicitAny[str] + # with self.assertRaises(TypeError): + # WithImplicitAny[str] class IOTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 56ef55afac41bf..d4f14e036e2561 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2296,15 +2296,6 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ - def __getitem__(cls, params): - if not isinstance(params, tuple): - params = (params,) - msg = "Parameters to generic types must be types." - params = tuple(_type_check(p, msg) for p in params) - nparams = len(cls.__dict__.get("__parameters__", ())) - _check_generic(cls, params, nparams) - return types.GenericAlias(cls, params) - def TypedDict(typename, fields=None, /, *, total=True, **kwargs): """A simple typed namespace. At runtime it is equivalent to a plain dict. From 964f7d3ca0a27fc354e35cbae64e24a0ec2eaa4a Mon Sep 17 00:00:00 2001 From: Samodya Abey <379594+sransara@users.noreply.github.com> Date: Mon, 6 Sep 2021 08:27:06 -0500 Subject: [PATCH 14/35] Use alternative way to find if Generic base is needed Fixes issue described in: https://github.com/python/cpython/pull/27663#discussion_r702870507 Co-authored-by: Yurii Karabas <1998uriyyo@gmail.com> --- Lib/typing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index d4f14e036e2561..2b9eb67783f23d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2256,7 +2256,10 @@ def __new__(cls, name, bases, ns, total=True): raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') - generic_base = (Generic,) if _collect_type_vars(ns.get("__orig_bases__", ())) else () + if '__orig_bases__' in ns and any(issubclass(b, Generic) for b in bases): + generic_base = (Generic,) + else: + generic_base = () tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns) annotations = {} From c3c0e51285db55330bcba5792f7a345750dd9ca4 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 6 Sep 2021 08:53:57 -0500 Subject: [PATCH 15/35] Make it clear when Generic base is included --- Lib/typing.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 2b9eb67783f23d..1c127d3a29ab23 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2256,8 +2256,16 @@ def __new__(cls, name, bases, ns, total=True): raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') - if '__orig_bases__' in ns and any(issubclass(b, Generic) for b in bases): - generic_base = (Generic,) + if any(issubclass(b, Generic) for b in bases): + if '__orig_bases__' in ns: + # Original base is a Generic[X] or A[X] + generic_base = (Generic,) + else: + # Implicit Any case: a generic base with no type args + generic_base = () + # Offloading work from Generic.__init_subclass__ + # to keep consistency with normal generic classes + ns['__parameters__'] = () else: generic_base = () tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns) From d34c99e0ffe04eae6a2a626b8f0e8243aafd627b Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 6 Sep 2021 09:10:04 -0500 Subject: [PATCH 16/35] Add getitem:so TD is subscriptable only if Generic --- Lib/typing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 1c127d3a29ab23..ad0fac4c9d700c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2307,6 +2307,13 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ + @_tp_cache + def __getitem__(cls, params): + if issubclass(cls, Generic): + return cls.__class_getitem__(params) + + raise TypeError(f"'{cls!r}' is not subscriptable") + def TypedDict(typename, fields=None, /, *, total=True, **kwargs): """A simple typed namespace. At runtime it is equivalent to a plain dict. From cea66c4f94e5f5b9259b3a941204b1d17d1dd816 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 6 Sep 2021 09:14:55 -0500 Subject: [PATCH 17/35] Fix test consistency for empty params --- Lib/test/test_typing.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f424cec7dbf2e8..4c8970a27c43a8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4265,8 +4265,8 @@ class B(A[KT], total=False): b: KT self.assertEqual(B.__bases__, (Generic, dict,)) - self.assertEqual(B.__total__, False) self.assertEqual(B.__parameters__, (KT, )) + self.assertEqual(B.__total__, False) self.assertEqual(B.__optional_keys__, frozenset(['b'])) self.assertEqual(B.__required_keys__, frozenset(['a'])) @@ -4277,7 +4277,8 @@ class B(A[KT], total=False): class C(B[int]): c: int - self.assertEqual(C.__bases__, (dict,)) + self.assertEqual(C.__bases__, (Generic, dict,)) + self.assertEqual(C.__parameters__, ()) self.assertEqual(C.__total__, True) self.assertEqual(C.__optional_keys__, frozenset(['b'])) self.assertEqual(C.__required_keys__, frozenset(['a', 'c'])) @@ -4286,13 +4287,15 @@ class C(B[int]): 'b': KT, 'c': int, } - # with self.assertRaises(TypeError): - # C[str] + with self.assertRaises(TypeError): + C[str] class WithImplicitAny(B): c: int self.assertEqual(WithImplicitAny.__bases__, (dict,)) + # Consistent with GenericTests.test_implicit_any + self.assertEqual(WithImplicitAny.__parameters__, ()) self.assertEqual(WithImplicitAny.__total__, True) self.assertEqual(WithImplicitAny.__optional_keys__, frozenset(['b'])) self.assertEqual(WithImplicitAny.__required_keys__, frozenset(['a', 'c'])) @@ -4301,8 +4304,8 @@ class WithImplicitAny(B): 'b': KT, 'c': int, } - # with self.assertRaises(TypeError): - # WithImplicitAny[str] + with self.assertRaises(TypeError): + WithImplicitAny[str] class IOTests(BaseTestCase): From 0ed4326a4d6c01661ffd46752a863266ae9d1465 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 6 Sep 2021 09:24:45 -0500 Subject: [PATCH 18/35] Test for adding new generic arg in child class --- Lib/test/test_typing.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 4c8970a27c43a8..ffc6f99dca9c79 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4290,6 +4290,29 @@ class C(B[int]): with self.assertRaises(TypeError): C[str] + + class Point3D(Point2DGeneric[T], Generic[T, KT]): + c: KT + + self.assertEqual(Point3D.__bases__, (Generic, dict,)) + self.assertEqual(Point3D.__parameters__, (T, KT)) + self.assertEqual(Point3D.__total__, True) + self.assertEqual(Point3D.__optional_keys__, frozenset()) + self.assertEqual(Point3D.__required_keys__, frozenset(['a', 'b', 'c'])) + assert Point3D.__annotations__ == { + 'a': T, + 'b': T, + 'c': KT, + } + self.assertEqual(Point3D[int, str].__origin__, Point3D) + + with self.assertRaises(TypeError): + Point3D[int] + + with self.assertRaises(TypeError): + class Point3D(Point2DGeneric[T], Generic[KT]): + c: KT + class WithImplicitAny(B): c: int From b1fcd16f5c9a8a1a7fddf4d978818dcedaa8442a Mon Sep 17 00:00:00 2001 From: Samodya Abey <379594+sransara@users.noreply.github.com> Date: Sat, 30 Apr 2022 07:18:33 +0530 Subject: [PATCH 19/35] Update Lib/typing.py Co-authored-by: Jelle Zijlstra --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index ad0fac4c9d700c..72b79a704052c0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2252,7 +2252,7 @@ def __new__(cls, name, bases, ns, total=True): Subclasses and instances of TypedDict return actual dictionaries. """ for base in bases: - if not (type(base) is _TypedDictMeta or base is Generic): + if type(base) is not _TypedDictMeta and base is not Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') From afa5e51e0e0939ab117e3564d74212607b9d526c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 30 Apr 2022 19:35:06 +0300 Subject: [PATCH 20/35] Fix merge error. --- Lib/test/_typed_dict_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/_typed_dict_helper.py b/Lib/test/_typed_dict_helper.py index 4e2da133d32405..9df0ede7d40ee5 100644 --- a/Lib/test/_typed_dict_helper.py +++ b/Lib/test/_typed_dict_helper.py @@ -13,7 +13,7 @@ class Bar(_typed_dict_helper.Foo, total=False): from __future__ import annotations -from typing import Annotated, Generic, Optional, Required, TypedDict, +from typing import Annotated, Generic, Optional, Required, TypedDict, TypeVar OptionalIntType = Optional[int] From 94138f630a4a2e0a3776e83ca96beea76bc94ad6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 30 Apr 2022 19:36:40 +0300 Subject: [PATCH 21/35] Fix trailing spaces. --- Lib/test/test_typing.py | 6 +++--- Lib/typing.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 15b0459e78c551..8be29fb9ab5120 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6044,7 +6044,7 @@ def test_get_type_hints(self): def test_get_type_hints_generic(self): self.assertEqual( - get_type_hints(BarGeneric[int].__origin__), + get_type_hints(BarGeneric[int].__origin__), {'a': typing.Optional[T], 'b': int} ) @@ -6086,7 +6086,7 @@ class C(B[int]): with self.assertRaises(TypeError): C[str] - + class Point3D(Point2DGeneric[T], Generic[T, KT]): c: KT @@ -6125,7 +6125,7 @@ class WithImplicitAny(B): } with self.assertRaises(TypeError): WithImplicitAny[str] - + class RequiredTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 845d68b36d5bb9..a3e717792568ba 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2842,7 +2842,7 @@ def __new__(cls, name, bases, ns, total=True): if type(base) is not _TypedDictMeta and base is not Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') - + if any(issubclass(b, Generic) for b in bases): if '__orig_bases__' in ns: # Original base is a Generic[X] or A[X] @@ -2850,7 +2850,7 @@ def __new__(cls, name, bases, ns, total=True): else: # Implicit Any case: a generic base with no type args generic_base = () - # Offloading work from Generic.__init_subclass__ + # Offloading work from Generic.__init_subclass__ # to keep consistency with normal generic classes ns['__parameters__'] = () else: From c2e1d8d1fb13874935b832e93317fbc8d54fade6 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 07:40:25 +0530 Subject: [PATCH 22/35] Fix indentation --- Lib/test/test_typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8be29fb9ab5120..434eeef0c22fbb 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6084,7 +6084,7 @@ class C(B[int]): 'c': int, } with self.assertRaises(TypeError): - C[str] + C[str] class Point3D(Point2DGeneric[T], Generic[T, KT]): @@ -6124,7 +6124,7 @@ class WithImplicitAny(B): 'c': int, } with self.assertRaises(TypeError): - WithImplicitAny[str] + WithImplicitAny[str] class RequiredTests(BaseTestCase): From d3d9456cf51d305c594061ee5f7fd28b2f680a0f Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 07:42:47 +0530 Subject: [PATCH 23/35] Remove trailing commas in tuples --- Lib/test/test_typing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 434eeef0c22fbb..ba979f8db8a3cc 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6052,7 +6052,7 @@ def test_generic_inheritance(self): class A(TypedDict, Generic[T]): a: T - self.assertEqual(A.__bases__, (Generic, dict,)) + self.assertEqual(A.__bases__, (Generic, dict)) self.assertEqual(A.__parameters__, (T,)) self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) @@ -6060,7 +6060,7 @@ class A(TypedDict, Generic[T]): class B(A[KT], total=False): b: KT - self.assertEqual(B.__bases__, (Generic, dict,)) + self.assertEqual(B.__bases__, (Generic, dict)) self.assertEqual(B.__parameters__, (KT, )) self.assertEqual(B.__total__, False) self.assertEqual(B.__optional_keys__, frozenset(['b'])) @@ -6073,7 +6073,7 @@ class B(A[KT], total=False): class C(B[int]): c: int - self.assertEqual(C.__bases__, (Generic, dict,)) + self.assertEqual(C.__bases__, (Generic, dict)) self.assertEqual(C.__parameters__, ()) self.assertEqual(C.__total__, True) self.assertEqual(C.__optional_keys__, frozenset(['b'])) @@ -6090,7 +6090,7 @@ class C(B[int]): class Point3D(Point2DGeneric[T], Generic[T, KT]): c: KT - self.assertEqual(Point3D.__bases__, (Generic, dict,)) + self.assertEqual(Point3D.__bases__, (Generic, dict)) self.assertEqual(Point3D.__parameters__, (T, KT)) self.assertEqual(Point3D.__total__, True) self.assertEqual(Point3D.__optional_keys__, frozenset()) From 311852c062c45033cfba842fba58c3e7fac4abb7 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 08:00:17 +0530 Subject: [PATCH 24/35] Add test with flipped bases --- Lib/test/test_typing.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ba979f8db8a3cc..4287947bc0a3ba 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6057,11 +6057,19 @@ class A(TypedDict, Generic[T]): self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) + class A2(Generic[T], TypedDict): + a: T + + self.assertEqual(A2.__bases__, (Generic, dict)) + self.assertEqual(A2.__parameters__, (T,)) + self.assertEqual(A2[str].__parameters__, ()) + self.assertEqual(A2[str].__args__, (str,)) + class B(A[KT], total=False): b: KT self.assertEqual(B.__bases__, (Generic, dict)) - self.assertEqual(B.__parameters__, (KT, )) + self.assertEqual(B.__parameters__, (KT,)) self.assertEqual(B.__total__, False) self.assertEqual(B.__optional_keys__, frozenset(['b'])) self.assertEqual(B.__required_keys__, frozenset(['a'])) From dc98753e634bfaa0ac1992e8e005beea1fd2e0f7 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 08:20:56 +0530 Subject: [PATCH 25/35] Move implicit any test to on its own case --- Lib/test/test_typing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 4287947bc0a3ba..5c4fdc603f943c 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6117,6 +6117,13 @@ class Point3D(Point2DGeneric[T], Generic[T, KT]): class Point3D(Point2DGeneric[T], Generic[KT]): c: KT + def test_implicit_any_inheritance(self): + class A(TypedDict, Generic[T]): + a: T + + class B(A[KT], total=False): + b: KT + class WithImplicitAny(B): c: int From 9bed1d4f8ed955df0f564250e361302b888b3c96 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 08:27:05 +0530 Subject: [PATCH 26/35] Add checks for orig_bases and mro --- Lib/test/test_typing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 5c4fdc603f943c..5e8f1610eebf07 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6053,6 +6053,8 @@ class A(TypedDict, Generic[T]): a: T self.assertEqual(A.__bases__, (Generic, dict)) + self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) + self.assertEqual(A.__mro__, (A, Generic, dict, object)) self.assertEqual(A.__parameters__, (T,)) self.assertEqual(A[str].__parameters__, ()) self.assertEqual(A[str].__args__, (str,)) @@ -6061,6 +6063,8 @@ class A2(Generic[T], TypedDict): a: T self.assertEqual(A2.__bases__, (Generic, dict)) + self.assertEqual(A2.__orig_bases__, (Generic[T], TypedDict)) + self.assertEqual(A2.__mro__, (A2, Generic, dict, object)) self.assertEqual(A2.__parameters__, (T,)) self.assertEqual(A2[str].__parameters__, ()) self.assertEqual(A2[str].__args__, (str,)) @@ -6069,6 +6073,8 @@ class B(A[KT], total=False): b: KT self.assertEqual(B.__bases__, (Generic, dict)) + self.assertEqual(B.__orig_bases__, (A[KT],)) + self.assertEqual(B.__mro__, (B, Generic, dict, object)) self.assertEqual(B.__parameters__, (KT,)) self.assertEqual(B.__total__, False) self.assertEqual(B.__optional_keys__, frozenset(['b'])) @@ -6082,6 +6088,8 @@ class C(B[int]): c: int self.assertEqual(C.__bases__, (Generic, dict)) + self.assertEqual(C.__orig_bases__, (B[int],)) + self.assertEqual(C.__mro__, (C, Generic, dict, object)) self.assertEqual(C.__parameters__, ()) self.assertEqual(C.__total__, True) self.assertEqual(C.__optional_keys__, frozenset(['b'])) @@ -6099,6 +6107,8 @@ class Point3D(Point2DGeneric[T], Generic[T, KT]): c: KT self.assertEqual(Point3D.__bases__, (Generic, dict)) + self.assertEqual(Point3D.__orig_bases__, (Point2DGeneric[T], Generic[T, KT])) + self.assertEqual(Point3D.__mro__, (Point3D, Generic, dict, object)) self.assertEqual(Point3D.__parameters__, (T, KT)) self.assertEqual(Point3D.__total__, True) self.assertEqual(Point3D.__optional_keys__, frozenset()) From 6c152e7fa8f6a4443a3e9a8b46a0d96ad804e2fd Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 08:46:39 +0530 Subject: [PATCH 27/35] Remove specialization from generic get_type_hints --- Lib/test/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 5e8f1610eebf07..c714f98718f2d8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6044,7 +6044,7 @@ def test_get_type_hints(self): def test_get_type_hints_generic(self): self.assertEqual( - get_type_hints(BarGeneric[int].__origin__), + get_type_hints(BarGeneric), {'a': typing.Optional[T], 'b': int} ) From f88201be7e78230271c9e3bf833a93ea2b39bdec Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 08:51:17 +0530 Subject: [PATCH 28/35] Check type hints of inherited generic typeddict --- Lib/test/test_typing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index c714f98718f2d8..94aa06abec2400 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6048,6 +6048,14 @@ def test_get_type_hints_generic(self): {'a': typing.Optional[T], 'b': int} ) + class FooBarGeneric(BarGeneric[int]): + c: str + + self.assertEqual( + get_type_hints(FooBarGeneric), + {'a': typing.Optional[T], 'b': int, 'c': str} + ) + def test_generic_inheritance(self): class A(TypedDict, Generic[T]): a: T From 6ea95df4361cf59b82e9f529f777553427e0818f Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 09:01:34 +0530 Subject: [PATCH 29/35] Remove unused statement --- Lib/typing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index a3e717792568ba..0d8c098bcd78d0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2859,7 +2859,6 @@ def __new__(cls, name, bases, ns, total=True): annotations = {} own_annotations = ns.get('__annotations__', {}) - own_annotation_keys = set(own_annotations.keys()) msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" own_annotations = { n: _type_check(tp, msg, module=tp_dict.__module__) From 80e9104f020fac2647ff158842c5dff6e5d96b88 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 09:20:03 +0530 Subject: [PATCH 30/35] Add class method through the metaclass --- Lib/typing.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 0d8c098bcd78d0..85965dd9f949d2 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2855,7 +2855,11 @@ def __new__(cls, name, bases, ns, total=True): ns['__parameters__'] = () else: generic_base = () + tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns) + if generic_base: + class_getitem = Generic.__class_getitem__.__func__ + tp_dict.__class_getitem__ = classmethod(class_getitem) annotations = {} own_annotations = ns.get('__annotations__', {}) @@ -2905,13 +2909,6 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ - @_tp_cache - def __getitem__(cls, params): - if issubclass(cls, Generic): - return cls.__class_getitem__(params) - - raise TypeError(f"'{cls!r}' is not subscriptable") - def TypedDict(typename, fields=None, /, *, total=True, **kwargs): """A simple typed namespace. At runtime it is equivalent to a plain dict. From dbbb7077460f68d9c3f66288ca01dd4cd7e15c19 Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 18:34:44 +0530 Subject: [PATCH 31/35] Fix generic base when inherited from implicit any --- Lib/test/test_typing.py | 3 ++- Lib/typing.py | 14 ++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 94aa06abec2400..0325c30ca97884 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6145,7 +6145,8 @@ class B(A[KT], total=False): class WithImplicitAny(B): c: int - self.assertEqual(WithImplicitAny.__bases__, (dict,)) + self.assertEqual(WithImplicitAny.__bases__, (Generic, dict,)) + self.assertEqual(WithImplicitAny.__mro__, (WithImplicitAny, Generic, dict, object)) # Consistent with GenericTests.test_implicit_any self.assertEqual(WithImplicitAny.__parameters__, ()) self.assertEqual(WithImplicitAny.__total__, True) diff --git a/Lib/typing.py b/Lib/typing.py index 85965dd9f949d2..48eb5fda2e05df 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1775,7 +1775,9 @@ def __init_subclass__(cls, *args, **kwargs): if '__orig_bases__' in cls.__dict__: error = Generic in cls.__orig_bases__ else: - error = Generic in cls.__bases__ and cls.__name__ != 'Protocol' + error = (Generic in cls.__bases__ and + cls.__name__ != 'Protocol' and + type(cls) != _TypedDictMeta) if error: raise TypeError("Cannot inherit from plain Generic") if '__orig_bases__' in cls.__dict__: @@ -2844,15 +2846,7 @@ def __new__(cls, name, bases, ns, total=True): 'and a non-TypedDict base class') if any(issubclass(b, Generic) for b in bases): - if '__orig_bases__' in ns: - # Original base is a Generic[X] or A[X] - generic_base = (Generic,) - else: - # Implicit Any case: a generic base with no type args - generic_base = () - # Offloading work from Generic.__init_subclass__ - # to keep consistency with normal generic classes - ns['__parameters__'] = () + generic_base = (Generic,) else: generic_base = () From 56b69e0456d7137842ea4daf6d9f31b36621b16a Mon Sep 17 00:00:00 2001 From: Samodya Abey Date: Mon, 2 May 2022 19:02:49 +0530 Subject: [PATCH 32/35] Fix whitespacing with reindent.py --- Lib/test/test_typing.py | 2 +- Lib/typing.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 0325c30ca97884..027f8725e86344 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6138,7 +6138,7 @@ class Point3D(Point2DGeneric[T], Generic[KT]): def test_implicit_any_inheritance(self): class A(TypedDict, Generic[T]): a: T - + class B(A[KT], total=False): b: KT diff --git a/Lib/typing.py b/Lib/typing.py index 48eb5fda2e05df..db9bef462903a2 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1775,8 +1775,8 @@ def __init_subclass__(cls, *args, **kwargs): if '__orig_bases__' in cls.__dict__: error = Generic in cls.__orig_bases__ else: - error = (Generic in cls.__bases__ and - cls.__name__ != 'Protocol' and + error = (Generic in cls.__bases__ and + cls.__name__ != 'Protocol' and type(cls) != _TypedDictMeta) if error: raise TypeError("Cannot inherit from plain Generic") From 4a408253553f941b56af76d8a28ff9867dfd85b0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 2 May 2022 12:20:42 -0600 Subject: [PATCH 33/35] fix tuple --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index db9bef462903a2..f41c0187d6b380 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2850,7 +2850,7 @@ def __new__(cls, name, bases, ns, total=True): else: generic_base = () - tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict,), ns) + tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict), ns) if generic_base: class_getitem = Generic.__class_getitem__.__func__ tp_dict.__class_getitem__ = classmethod(class_getitem) From 4b50ae289c3ea5c3e8fd25eaebcc02e166df9527 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 2 May 2022 12:26:02 -0600 Subject: [PATCH 34/35] remove unnecessary __class_getitem__ override --- Lib/typing.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index f41c0187d6b380..dd40ec0c5a79cd 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2851,9 +2851,6 @@ def __new__(cls, name, bases, ns, total=True): generic_base = () tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict), ns) - if generic_base: - class_getitem = Generic.__class_getitem__.__func__ - tp_dict.__class_getitem__ = classmethod(class_getitem) annotations = {} own_annotations = ns.get('__annotations__', {}) From 5b5a98340f9017c981aa7237bf3e0c3c869d6fe0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 2 May 2022 13:14:26 -0600 Subject: [PATCH 35/35] docs --- Doc/library/typing.rst | 11 ++++++++++- Doc/whatsnew/3.11.rst | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 868ea1b81be422..1927ede6ff7e48 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1729,7 +1729,7 @@ These are not used in annotations. They are building blocks for declaring types. z: int A ``TypedDict`` cannot inherit from a non-TypedDict class, - notably including :class:`Generic`. For example:: + except for :class:`Generic`. For example:: class X(TypedDict): x: int @@ -1746,6 +1746,12 @@ These are not used in annotations. They are building blocks for declaring types. T = TypeVar('T') class XT(X, Generic[T]): pass # raises TypeError + A ``TypedDict`` can be generic:: + + class Group(TypedDict, Generic[T]): + key: T + group: list[T] + A ``TypedDict`` can be introspected via annotations dicts (see :ref:`annotations-howto` for more information on annotations best practices), :attr:`__total__`, :attr:`__required_keys__`, and :attr:`__optional_keys__`. @@ -1793,6 +1799,9 @@ These are not used in annotations. They are building blocks for declaring types. .. versionadded:: 3.8 + .. versionchanged:: 3.11 + Added support for generic ``TypedDict``\ s. + Generic concrete collections ---------------------------- diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 0a8ba1e8843e06..9c2517e8fc4c51 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -715,6 +715,10 @@ For major changes, see :ref:`new-feat-related-type-hints-311`. to clear all registered overloads of a function. (Contributed by Jelle Zijlstra in :gh:`89263`.) +* :data:`typing.TypedDict` subclasses can now be generic. (Contributed by + Samodya Abey in :gh:`89026`.) + + unicodedata -----------