]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-43923: Add support for generic typing.NamedTuple (#92027)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 2 May 2022 22:41:23 +0000 (01:41 +0300)
committerGitHub <noreply@github.com>
Mon, 2 May 2022 22:41:23 +0000 (16:41 -0600)
Doc/library/typing.rst
Doc/whatsnew/3.11.rst
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst [new file with mode: 0644]

index 868ea1b81be4228b11798c242ba9a72ef1f589b0..05ac05767f32e6381ab1a390fa71b3719f42bd49 100644 (file)
@@ -1615,6 +1615,12 @@ These are not used in annotations. They are building blocks for declaring types.
           def __repr__(self) -> str:
               return f'<Employee {self.name}, id={self.id}>'
 
+   ``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)])
@@ -1633,6 +1639,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 for generic namedtuples.
+
 .. class:: NewType(name, tp)
 
    A helper class to indicate a distinct type to a typechecker,
index 0a8ba1e8843e06bbcb26ce4244bbf5a28d067d40..80ce46261f1596e9aa995da0bfa1b0e6c390e06f 100644 (file)
@@ -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`.)
 
+* :class:`~typing.NamedTuple` subclasses can be generic.
+  (Contributed by Serhiy Storchaka in :issue:`43923`.)
+
+
 unicodedata
 -----------
 
index cb7ca3610b06a66082e690240aecf33c73e9e938..c074e7a7800208a9db7f5b90651a8db68bfff803 100644 (file)
@@ -5678,6 +5678,45 @@ class NamedTupleTests(BaseTestCase):
         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))
+
+        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):
+                    G[int, str]
 
     def test_namedtuple_keyword_usage(self):
         LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)
index 35deb32220eb8877f2c69c343bcb45d4a860a196..29a3f43881b166f5e37717759495a38573a91cec 100644 (file)
@@ -2764,7 +2764,12 @@ _special = frozenset({'__module__', '__name__', '__annotations__'})
 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:
@@ -2778,12 +2783,18 @@ class NamedTupleMeta(type):
         nm_tpl = _make_nmtuple(typename, types.items(),
                                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__
+            nm_tpl.__class_getitem__ = classmethod(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
 
 
@@ -2821,9 +2832,7 @@ def NamedTuple(typename, fields=None, /, **kwargs):
 _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 (file)
index 0000000..2665a47
--- /dev/null
@@ -0,0 +1 @@
+Add support for generic :class:`typing.NamedTuple`.