Skip to content

Commit a2ec069

Browse files
bpo-40185: Refactor typing.NamedTuple (GH-19371)
1 parent 307b9d0 commit a2ec069

File tree

2 files changed

+54
-59
lines changed

2 files changed

+54
-59
lines changed

Lib/test/test_typing.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3598,11 +3598,9 @@ def test_annotation_usage_with_default(self):
35983598
self.assertEqual(CoolEmployeeWithDefault._field_defaults, dict(cool=0))
35993599

36003600
with self.assertRaises(TypeError):
3601-
exec("""
3602-
class NonDefaultAfterDefault(NamedTuple):
3603-
x: int = 3
3604-
y: int
3605-
""")
3601+
class NonDefaultAfterDefault(NamedTuple):
3602+
x: int = 3
3603+
y: int
36063604

36073605
def test_annotation_usage_with_methods(self):
36083606
self.assertEqual(XMeth(1).double(), 2)
@@ -3611,20 +3609,16 @@ def test_annotation_usage_with_methods(self):
36113609
self.assertEqual(XRepr(1, 2) + XRepr(3), 0)
36123610

36133611
with self.assertRaises(AttributeError):
3614-
exec("""
3615-
class XMethBad(NamedTuple):
3616-
x: int
3617-
def _fields(self):
3618-
return 'no chance for this'
3619-
""")
3612+
class XMethBad(NamedTuple):
3613+
x: int
3614+
def _fields(self):
3615+
return 'no chance for this'
36203616

36213617
with self.assertRaises(AttributeError):
3622-
exec("""
3623-
class XMethBad2(NamedTuple):
3624-
x: int
3625-
def _source(self):
3626-
return 'no chance for this as well'
3627-
""")
3618+
class XMethBad2(NamedTuple):
3619+
x: int
3620+
def _source(self):
3621+
return 'no chance for this as well'
36283622

36293623
def test_multiple_inheritance(self):
36303624
class A:

Lib/typing.py

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,51 +1702,41 @@ def __round__(self, ndigits: int = 0) -> T_co:
17021702
pass
17031703

17041704

1705-
def _make_nmtuple(name, types):
1706-
msg = "NamedTuple('Name', [(f0, t0), (f1, t1), ...]); each t must be a type"
1707-
types = [(n, _type_check(t, msg)) for n, t in types]
1708-
nm_tpl = collections.namedtuple(name, [n for n, t in types])
1709-
nm_tpl.__annotations__ = dict(types)
1710-
try:
1711-
nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__')
1712-
except (AttributeError, ValueError):
1713-
pass
1705+
def _make_nmtuple(name, types, module, defaults = ()):
1706+
fields = [n for n, t in types]
1707+
types = {n: _type_check(t, f"field {n} annotation must be a type")
1708+
for n, t in types}
1709+
nm_tpl = collections.namedtuple(name, fields,
1710+
defaults=defaults, module=module)
1711+
nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = types
17141712
return nm_tpl
17151713

17161714

17171715
# attributes prohibited to set in NamedTuple class syntax
1718-
_prohibited = {'__new__', '__init__', '__slots__', '__getnewargs__',
1719-
'_fields', '_field_defaults',
1720-
'_make', '_replace', '_asdict', '_source'}
1716+
_prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
1717+
'_fields', '_field_defaults',
1718+
'_make', '_replace', '_asdict', '_source'})
17211719

1722-
_special = {'__module__', '__name__', '__annotations__'}
1720+
_special = frozenset({'__module__', '__name__', '__annotations__'})
17231721

17241722

17251723
class NamedTupleMeta(type):
17261724

17271725
def __new__(cls, typename, bases, ns):
1728-
if ns.get('_root', False):
1729-
return super().__new__(cls, typename, bases, ns)
1730-
if len(bases) > 1:
1731-
raise TypeError("Multiple inheritance with NamedTuple is not supported")
1732-
assert bases[0] is NamedTuple
1726+
assert bases[0] is _NamedTuple
17331727
types = ns.get('__annotations__', {})
1734-
nm_tpl = _make_nmtuple(typename, types.items())
1735-
defaults = []
1736-
defaults_dict = {}
1728+
default_names = []
17371729
for field_name in types:
17381730
if field_name in ns:
1739-
default_value = ns[field_name]
1740-
defaults.append(default_value)
1741-
defaults_dict[field_name] = default_value
1742-
elif defaults:
1743-
raise TypeError("Non-default namedtuple field {field_name} cannot "
1744-
"follow default field(s) {default_names}"
1745-
.format(field_name=field_name,
1746-
default_names=', '.join(defaults_dict.keys())))
1747-
nm_tpl.__new__.__annotations__ = dict(types)
1748-
nm_tpl.__new__.__defaults__ = tuple(defaults)
1749-
nm_tpl._field_defaults = defaults_dict
1731+
default_names.append(field_name)
1732+
elif default_names:
1733+
raise TypeError(f"Non-default namedtuple field {field_name} "
1734+
f"cannot follow default field"
1735+
f"{'s' if len(default_names) > 1 else ''} "
1736+
f"{', '.join(default_names)}")
1737+
nm_tpl = _make_nmtuple(typename, types.items(),
1738+
defaults=[ns[n] for n in default_names],
1739+
module=ns['__module__'])
17501740
# update from user namespace without overriding special namedtuple attributes
17511741
for key in ns:
17521742
if key in _prohibited:
@@ -1756,7 +1746,7 @@ def __new__(cls, typename, bases, ns):
17561746
return nm_tpl
17571747

17581748

1759-
class NamedTuple(metaclass=NamedTupleMeta):
1749+
def NamedTuple(typename, fields=None, /, **kwargs):
17601750
"""Typed version of namedtuple.
17611751
17621752
Usage in Python versions >= 3.6::
@@ -1780,15 +1770,26 @@ class Employee(NamedTuple):
17801770
17811771
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
17821772
"""
1783-
_root = True
1784-
1785-
def __new__(cls, typename, fields=None, /, **kwargs):
1786-
if fields is None:
1787-
fields = kwargs.items()
1788-
elif kwargs:
1789-
raise TypeError("Either list of fields or keywords"
1790-
" can be provided to NamedTuple, not both")
1791-
return _make_nmtuple(typename, fields)
1773+
if fields is None:
1774+
fields = kwargs.items()
1775+
elif kwargs:
1776+
raise TypeError("Either list of fields or keywords"
1777+
" can be provided to NamedTuple, not both")
1778+
try:
1779+
module = sys._getframe(1).f_globals.get('__name__', '__main__')
1780+
except (AttributeError, ValueError):
1781+
module = None
1782+
return _make_nmtuple(typename, fields, module=module)
1783+
1784+
_NamedTuple = type.__new__(NamedTupleMeta, 'NamedTuple', (), {})
1785+
1786+
def _namedtuple_mro_entries(bases):
1787+
if len(bases) > 1:
1788+
raise TypeError("Multiple inheritance with NamedTuple is not supported")
1789+
assert bases[0] is NamedTuple
1790+
return (_NamedTuple,)
1791+
1792+
NamedTuple.__mro_entries__ = _namedtuple_mro_entries
17921793

17931794

17941795
def _dict_new(cls, /, *args, **kwargs):

0 commit comments

Comments
 (0)