From 939d5f03c1715d0481dfbfd8b8ddd30a4775643d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 9 Mar 2022 12:12:08 +0200 Subject: [PATCH 1/6] bpo-43923: Add support of generic typing.NamedTuple --- Doc/library/typing.rst | 9 ++++++ Doc/whatsnew/3.11.rst | 7 +++++ Lib/test/test_typing.py | 31 +++++++++++++++++++ Lib/typing.py | 12 ++++--- ...2-04-28-18-45-58.gh-issue-88089.hu9kRk.rst | 2 ++ 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 4635da7579ac75..7e97c9a1b22bbf 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1606,6 +1606,12 @@ These are not used in annotations. They are building blocks for declaring types. def __repr__(self) -> str: return f'' + ``NamedTuple`` subclasses can be generic:: + + class Group(NamedTuple, Generic[T]): + key: T + group: list[T] + Backward-compatible usage:: Employee = NamedTuple('Employee', [('name', str), ('id', int)]) @@ -1624,6 +1630,9 @@ These are not used in annotations. They are building blocks for declaring types. Removed the ``_field_types`` attribute in favor of the more standard ``__annotations__`` attribute which has the same information. + .. versionchanged:: 3.11 + Added support of multiple inheritance with :class:`Generic`. + .. class:: NewType(name, tp) A helper class to indicate a distinct type to a typechecker, diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index b2b98747d31a14..6b51148433932d 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -641,6 +641,13 @@ time (Contributed by Benjamin Szőke, Dong-hee Na, Eryk Sun and Victor Stinner in :issue:`21302` and :issue:`45429`.) +typing +------ + +* :class:`~typing.NamedTuple` subclasses can be generic. + (Contributed by Serhiy Storchaka in :issue:`43923`.) + + unicodedata ----------- diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a904b7a790c04d..da763ceed4efa2 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5278,6 +5278,37 @@ class A: with self.assertRaises(TypeError): class X(NamedTuple, A): x: int + with self.assertRaises(TypeError): + class X(NamedTuple, tuple): + x: int + with self.assertRaises(TypeError): + class X(NamedTuple, NamedTuple): + x: int + class A(NamedTuple): + x: int + with self.assertRaises(TypeError): + class X(NamedTuple, A): + y: str + + def test_generic(self): + class X(NamedTuple, Generic[T]): + x: T + self.assertEqual(X.__bases__, (tuple, Generic)) + self.assertEqual(X.__orig_bases__, (NamedTuple, Generic[T])) + self.assertEqual(X.__mro__, (X, tuple, Generic, object)) + + A = X[int] + self.assertEqual(A.__bases__, (tuple, Generic)) + self.assertEqual(A.__orig_bases__, (NamedTuple, Generic[T])) + self.assertEqual(A.__mro__, (X, tuple, Generic, object)) + self.assertIs(A.__origin__, X) + self.assertEqual(A.__args__, (int,)) + self.assertEqual(A.__parameters__, ()) + + a = A(3) + self.assertIs(type(a), X) + self.assertEqual(a.x, 3) + def test_namedtuple_keyword_usage(self): LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) diff --git a/Lib/typing.py b/Lib/typing.py index f4d4fa4d6713c5..8e2e8aa2cbe58b 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2763,7 +2763,12 @@ def _make_nmtuple(name, types, module, defaults = ()): class NamedTupleMeta(type): def __new__(cls, typename, bases, ns): - assert bases[0] is _NamedTuple + assert _NamedTuple in bases + for base in bases: + if base is not _NamedTuple and base is not Generic: + raise TypeError('can only inherit from a NamedTuple type ' + 'and Generic') + bases = tuple(tuple if base is _NamedTuple else base for base in bases) types = ns.get('__annotations__', {}) default_names = [] for field_name in types: @@ -2777,6 +2782,7 @@ def __new__(cls, typename, bases, ns): nm_tpl = _make_nmtuple(typename, types.items(), defaults=[ns[n] for n in default_names], module=ns['__module__']) + nm_tpl.__bases__ = bases # update from user namespace without overriding special namedtuple attributes for key in ns: if key in _prohibited: @@ -2820,9 +2826,7 @@ class Employee(NamedTuple): _NamedTuple = type.__new__(NamedTupleMeta, 'NamedTuple', (), {}) def _namedtuple_mro_entries(bases): - if len(bases) > 1: - raise TypeError("Multiple inheritance with NamedTuple is not supported") - assert bases[0] is NamedTuple + assert NamedTuple in bases return (_NamedTuple,) NamedTuple.__mro_entries__ = _namedtuple_mro_entries diff --git a/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst b/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst new file mode 100644 index 00000000000000..c531e5426aab65 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst @@ -0,0 +1,2 @@ +Add support of multiple inheritance of :class:`typing.NamedTuple` with +:class:`typing.Generic`. From 17a9f6130a6f33042e06963ea45d220032a16ca4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 29 Apr 2022 07:56:20 +0300 Subject: [PATCH 2/6] Address review comments. --- Doc/library/typing.rst | 2 +- Lib/typing.py | 4 ++-- .../Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 7e97c9a1b22bbf..ed71d735ea47d4 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1631,7 +1631,7 @@ These are not used in annotations. They are building blocks for declaring types. standard ``__annotations__`` attribute which has the same information. .. versionchanged:: 3.11 - Added support of multiple inheritance with :class:`Generic`. + Added support for generic namedtuples. .. class:: NewType(name, tp) diff --git a/Lib/typing.py b/Lib/typing.py index 8e2e8aa2cbe58b..5b34508edc5186 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2766,8 +2766,8 @@ def __new__(cls, typename, bases, ns): assert _NamedTuple in bases for base in bases: if base is not _NamedTuple and base is not Generic: - raise TypeError('can only inherit from a NamedTuple type ' - 'and Generic') + raise TypeError( + 'can only inherit from a NamedTuple type and Generic') bases = tuple(tuple if base is _NamedTuple else base for base in bases) types = ns.get('__annotations__', {}) default_names = [] diff --git a/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst b/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst index c531e5426aab65..2665a472db6272 100644 --- a/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst +++ b/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst @@ -1,2 +1 @@ -Add support of multiple inheritance of :class:`typing.NamedTuple` with -:class:`typing.Generic`. +Add support for generic :class:`typing.NamedTuple`. From 6c1fb7358e8ca5369a146238ac93afb679f1cb88 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 29 Apr 2022 18:26:15 +0300 Subject: [PATCH 3/6] Fix __parameters__ and __class_getitem__. --- Lib/test/test_typing.py | 6 +++--- Lib/typing.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index da763ceed4efa2..844695e175d72e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5296,11 +5296,9 @@ class X(NamedTuple, Generic[T]): self.assertEqual(X.__bases__, (tuple, Generic)) self.assertEqual(X.__orig_bases__, (NamedTuple, Generic[T])) self.assertEqual(X.__mro__, (X, tuple, Generic, object)) + self.assertEqual(X.__parameters__, (T,)) A = X[int] - self.assertEqual(A.__bases__, (tuple, Generic)) - self.assertEqual(A.__orig_bases__, (NamedTuple, Generic[T])) - self.assertEqual(A.__mro__, (X, tuple, Generic, object)) self.assertIs(A.__origin__, X) self.assertEqual(A.__args__, (int,)) self.assertEqual(A.__parameters__, ()) @@ -5309,6 +5307,8 @@ class X(NamedTuple, Generic[T]): self.assertIs(type(a), X) self.assertEqual(a.x, 3) + with self.assertRaises(TypeError): + X[int, str] def test_namedtuple_keyword_usage(self): LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) diff --git a/Lib/typing.py b/Lib/typing.py index 5b34508edc5186..d50aa3a63625ee 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2783,12 +2783,17 @@ def __new__(cls, typename, bases, ns): defaults=[ns[n] for n in default_names], module=ns['__module__']) nm_tpl.__bases__ = bases + if Generic in bases: + class_getitem = Generic.__class_getitem__.__func__.__wrapped__ + nm_tpl.__class_getitem__ = classmethod(_tp_cache(class_getitem)) # update from user namespace without overriding special namedtuple attributes for key in ns: if key in _prohibited: raise AttributeError("Cannot overwrite NamedTuple attribute " + key) elif key not in _special and key not in nm_tpl._fields: setattr(nm_tpl, key, ns[key]) + if Generic in bases: + nm_tpl.__init_subclass__() return nm_tpl From 0f6fcee7f225bea6047ebefac4c751927cf34a0b Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 29 Apr 2022 18:37:20 +0300 Subject: [PATCH 4/6] Add tests for different order of bases, --- Lib/test/test_typing.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 844695e175d72e..b1c2a31033cc5a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5296,19 +5296,27 @@ class X(NamedTuple, Generic[T]): self.assertEqual(X.__bases__, (tuple, Generic)) self.assertEqual(X.__orig_bases__, (NamedTuple, Generic[T])) self.assertEqual(X.__mro__, (X, tuple, Generic, object)) - self.assertEqual(X.__parameters__, (T,)) - A = X[int] - self.assertIs(A.__origin__, X) - self.assertEqual(A.__args__, (int,)) - self.assertEqual(A.__parameters__, ()) - - a = A(3) - self.assertIs(type(a), X) - self.assertEqual(a.x, 3) + class Y(Generic[T], NamedTuple): + x: T + self.assertEqual(Y.__bases__, (Generic, tuple)) + self.assertEqual(Y.__orig_bases__, (Generic[T], NamedTuple)) + self.assertEqual(Y.__mro__, (Y, Generic, tuple, object)) + + for G in X, Y: + with self.subTest(type=G): + self.assertEqual(G.__parameters__, (T,)) + A = G[int] + self.assertIs(A.__origin__, G) + self.assertEqual(A.__args__, (int,)) + self.assertEqual(A.__parameters__, ()) + + a = A(3) + self.assertIs(type(a), G) + self.assertEqual(a.x, 3) - with self.assertRaises(TypeError): - X[int, str] + with self.assertRaises(TypeError): + G[int, str] def test_namedtuple_keyword_usage(self): LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) From ee3ede6c3fbb1586f9a4c2e0b947fa8ce1c3b5ae Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 29 Apr 2022 19:18:54 +0300 Subject: [PATCH 5/6] Use the same cache. --- Lib/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index d50aa3a63625ee..7f9a9e0102749e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2784,8 +2784,8 @@ def __new__(cls, typename, bases, ns): module=ns['__module__']) nm_tpl.__bases__ = bases if Generic in bases: - class_getitem = Generic.__class_getitem__.__func__.__wrapped__ - nm_tpl.__class_getitem__ = classmethod(_tp_cache(class_getitem)) + class_getitem = Generic.__class_getitem__.__func__ + nm_tpl.__class_getitem__ = classmethod(class_getitem) # update from user namespace without overriding special namedtuple attributes for key in ns: if key in _prohibited: From c24c17c37fd71c2c36c569337c5ba77162ea9721 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 2 May 2022 13:21:26 -0600 Subject: [PATCH 6/6] Update Doc/whatsnew/3.11.rst --- Doc/whatsnew/3.11.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index c7a7c5af8589d7..80ce46261f1596 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -715,9 +715,6 @@ 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`.) -typing ------- - * :class:`~typing.NamedTuple` subclasses can be generic. (Contributed by Serhiy Storchaka in :issue:`43923`.)