Skip to content
10 changes: 7 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4947,9 +4947,8 @@ def _source(self):
def test_multiple_inheritance(self):
class A:
pass
with self.assertRaises(TypeError):
class X(NamedTuple, A):
x: int
class X(NamedTuple, A):
x: int

def test_namedtuple_keyword_usage(self):
LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int)
Expand Down Expand Up @@ -5282,6 +5281,11 @@ def test_get_type_hints(self):
{'a': typing.Optional[int], 'b': int}
)

def test_generic_subclasses(self):
T = TypeVar("T")
class GenericNamedTuple(NamedTuple, Generic[T]):
pass


class IOTests(BaseTestCase):

Expand Down
71 changes: 36 additions & 35 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2595,13 +2595,15 @@ def __round__(self, ndigits: int = 0) -> T_co:
pass


def _make_nmtuple(name, types, module, defaults = ()):
fields = [n for n, t in types]
types = {n: _type_check(t, f"field {n} annotation must be a type")
for n, t in types}
nm_tpl = collections.namedtuple(name, fields,
defaults=defaults, module=module)
nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = types
def _make_nmtuple(name, types):
msg = "NamedTuple('Name', [(f0, t0), (f1, t1), ...]); each t must be a type"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're missing the improved error message from the existing code

types = [(n, _type_check(t, msg)) for n, t in types]
nm_tpl = collections.namedtuple(name, [n for n, t in types])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collections.namedtuple now supports default= directly, so we should use that

nm_tpl.__annotations__ = dict(types)
try:
nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
return nm_tpl


Expand All @@ -2616,20 +2618,26 @@ def _make_nmtuple(name, types, module, defaults = ()):
class NamedTupleMeta(type):

def __new__(cls, typename, bases, ns):
assert bases[0] is _NamedTuple
if ns.get('_root', False):
return super().__new__(cls, typename, bases, ns)
assert bases[0] is NamedTuple
types = ns.get('__annotations__', {})
default_names = []
nm_tpl = _make_nmtuple(typename, types.items())
defaults = []
defaults_dict = {}
for field_name in types:
if field_name in ns:
default_names.append(field_name)
elif default_names:
raise TypeError(f"Non-default namedtuple field {field_name} "
f"cannot follow default field"
f"{'s' if len(default_names) > 1 else ''} "
f"{', '.join(default_names)}")
nm_tpl = _make_nmtuple(typename, types.items(),
defaults=[ns[n] for n in default_names],
module=ns['__module__'])
default_value = ns[field_name]
defaults.append(default_value)
defaults_dict[field_name] = default_value
elif defaults:
raise TypeError("Non-default namedtuple field {field_name} cannot "
"follow default field(s) {default_names}"
.format(field_name=field_name,
default_names=', '.join(defaults_dict.keys())))
nm_tpl.__new__.__annotations__ = dict(types)
nm_tpl.__new__.__defaults__ = tuple(defaults)
nm_tpl._field_defaults = defaults_dict
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to restore this old attribute

# update from user namespace without overriding special namedtuple attributes
for key in ns:
if key in _prohibited:
Expand All @@ -2639,7 +2647,7 @@ def __new__(cls, typename, bases, ns):
return nm_tpl


def NamedTuple(typename, fields=None, /, **kwargs):
class NamedTuple(metaclass=NamedTupleMeta):
"""Typed version of namedtuple.

Usage in Python versions >= 3.6::
Expand All @@ -2663,22 +2671,15 @@ class Employee(NamedTuple):

Employee = NamedTuple('Employee', [('name', str), ('id', int)])
"""
if fields is None:
fields = kwargs.items()
elif kwargs:
raise TypeError("Either list of fields or keywords"
" can be provided to NamedTuple, not both")
return _make_nmtuple(typename, fields, module=_caller())

_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
return (_NamedTuple,)

NamedTuple.__mro_entries__ = _namedtuple_mro_entries
_root = True

def __new__(cls, typename, fields=None, /, **kwargs):
if fields is None:
fields = kwargs.items()
elif kwargs:
raise TypeError("Either list of fields or keywords"
" can be provided to NamedTuple, not both")
return _make_nmtuple(typename, fields)


class _TypedDictMeta(type):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Multiple inheritance with :class:`typing.NamedTuple` now no longer raises an
error.