diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 868ea1b81be422..05ac05767f32e6 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1615,6 +1615,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)]) @@ -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, diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 0a8ba1e8843e06..80ce46261f1596 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`.) +* :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 cb7ca3610b06a6..c074e7a7800208 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5678,6 +5678,45 @@ 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)) + + 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) diff --git a/Lib/typing.py b/Lib/typing.py index 35deb32220eb88..29a3f43881b166 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2764,7 +2764,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: @@ -2778,12 +2783,18 @@ 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 + 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 @@ 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..2665a472db6272 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-28-18-45-58.gh-issue-88089.hu9kRk.rst @@ -0,0 +1 @@ +Add support for generic :class:`typing.NamedTuple`.