Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-43923: Add support of generic typing.NamedTuple #92027

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`.