summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2020-04-08 07:59:04 (GMT)
committerGitHub <noreply@github.com>2020-04-08 07:59:04 (GMT)
commita2ec06938f46683e33692615aca3875d8b8e110c (patch)
treef5f59f4b3e02eedb6a005af4460e9e6d71c31b05
parent307b9d0144e719b016a47fcc43397c070615e01e (diff)
downloadcpython-a2ec06938f46683e33692615aca3875d8b8e110c.zip
cpython-a2ec06938f46683e33692615aca3875d8b8e110c.tar.gz
cpython-a2ec06938f46683e33692615aca3875d8b8e110c.tar.bz2
bpo-40185: Refactor typing.NamedTuple (GH-19371)
-rw-r--r--Lib/test/test_typing.py28
-rw-r--r--Lib/typing.py85
2 files changed, 54 insertions, 59 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 95f865f..489836c 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -3598,11 +3598,9 @@ class NamedTupleTests(BaseTestCase):
self.assertEqual(CoolEmployeeWithDefault._field_defaults, dict(cool=0))
with self.assertRaises(TypeError):
- exec("""
-class NonDefaultAfterDefault(NamedTuple):
- x: int = 3
- y: int
-""")
+ class NonDefaultAfterDefault(NamedTuple):
+ x: int = 3
+ y: int
def test_annotation_usage_with_methods(self):
self.assertEqual(XMeth(1).double(), 2)
@@ -3611,20 +3609,16 @@ class NonDefaultAfterDefault(NamedTuple):
self.assertEqual(XRepr(1, 2) + XRepr(3), 0)
with self.assertRaises(AttributeError):
- exec("""
-class XMethBad(NamedTuple):
- x: int
- def _fields(self):
- return 'no chance for this'
-""")
+ class XMethBad(NamedTuple):
+ x: int
+ def _fields(self):
+ return 'no chance for this'
with self.assertRaises(AttributeError):
- exec("""
-class XMethBad2(NamedTuple):
- x: int
- def _source(self):
- return 'no chance for this as well'
-""")
+ class XMethBad2(NamedTuple):
+ x: int
+ def _source(self):
+ return 'no chance for this as well'
def test_multiple_inheritance(self):
class A:
diff --git a/Lib/typing.py b/Lib/typing.py
index 6cc3b03..bcb2233 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1702,51 +1702,41 @@ class SupportsRound(Protocol[T_co]):
pass
-def _make_nmtuple(name, types):
- msg = "NamedTuple('Name', [(f0, t0), (f1, t1), ...]); each t must be a type"
- types = [(n, _type_check(t, msg)) for n, t in types]
- nm_tpl = collections.namedtuple(name, [n for n, t in types])
- nm_tpl.__annotations__ = dict(types)
- try:
- nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__')
- except (AttributeError, ValueError):
- 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
return nm_tpl
# attributes prohibited to set in NamedTuple class syntax
-_prohibited = {'__new__', '__init__', '__slots__', '__getnewargs__',
- '_fields', '_field_defaults',
- '_make', '_replace', '_asdict', '_source'}
+_prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
+ '_fields', '_field_defaults',
+ '_make', '_replace', '_asdict', '_source'})
-_special = {'__module__', '__name__', '__annotations__'}
+_special = frozenset({'__module__', '__name__', '__annotations__'})
class NamedTupleMeta(type):
def __new__(cls, typename, bases, ns):
- if ns.get('_root', False):
- return super().__new__(cls, typename, bases, ns)
- if len(bases) > 1:
- raise TypeError("Multiple inheritance with NamedTuple is not supported")
- assert bases[0] is NamedTuple
+ assert bases[0] is _NamedTuple
types = ns.get('__annotations__', {})
- nm_tpl = _make_nmtuple(typename, types.items())
- defaults = []
- defaults_dict = {}
+ default_names = []
for field_name in types:
if field_name in ns:
- 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
+ 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__'])
# update from user namespace without overriding special namedtuple attributes
for key in ns:
if key in _prohibited:
@@ -1756,7 +1746,7 @@ class NamedTupleMeta(type):
return nm_tpl
-class NamedTuple(metaclass=NamedTupleMeta):
+def NamedTuple(typename, fields=None, /, **kwargs):
"""Typed version of namedtuple.
Usage in Python versions >= 3.6::
@@ -1780,15 +1770,26 @@ class NamedTuple(metaclass=NamedTupleMeta):
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
"""
- _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)
+ if fields is None:
+ fields = kwargs.items()
+ elif kwargs:
+ raise TypeError("Either list of fields or keywords"
+ " can be provided to NamedTuple, not both")
+ try:
+ module = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ module = None
+ return _make_nmtuple(typename, fields, module=module)
+
+_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
def _dict_new(cls, /, *args, **kwargs):