[Python-checkins] bpo-40185: Refactor typing.NamedTuple (GH-19371)

Serhiy Storchaka webhook-mailer at python.org
Wed Apr 8 03:59:21 EDT 2020


https://github.com/python/cpython/commit/a2ec06938f46683e33692615aca3875d8b8e110c
commit: a2ec06938f46683e33692615aca3875d8b8e110c
branch: master
author: Serhiy Storchaka <storchaka at gmail.com>
committer: GitHub <noreply at github.com>
date: 2020-04-08T10:59:04+03:00
summary:

bpo-40185: Refactor typing.NamedTuple (GH-19371)

files:
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 95f865f8c3474..489836c459b1c 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -3598,11 +3598,9 @@ def test_annotation_usage_with_default(self):
         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 @@ def test_annotation_usage_with_methods(self):
         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 6cc3b0342ec77..bcb2233ee50cf 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1702,51 +1702,41 @@ def __round__(self, ndigits: int = 0) -> 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 @@ def __new__(cls, typename, bases, ns):
         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 Employee(NamedTuple):
 
         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):



More information about the Python-checkins mailing list