Skip to content

Commit

Permalink
bpo-43923: Add support for generic typing.NamedTuple (#92027)
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka committed May 2, 2022
1 parent 81fb354 commit b04e02c
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 4 deletions.
9 changes: 9 additions & 0 deletions Doc/library/typing.rst
Expand Up @@ -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)])
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.11.rst
Expand Up @@ -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
-----------

Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_typing.py
Expand Up @@ -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)
Expand Down
17 changes: 13 additions & 4 deletions Lib/typing.py
Expand Up @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -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
Expand Down
@@ -0,0 +1 @@
Add support for generic :class:`typing.NamedTuple`.

0 comments on commit b04e02c

Please sign in to comment.