From 03f1573fbc7449d67ad6ded0b3aeb83d78ef0b20 Mon Sep 17 00:00:00 2001 From: julius Date: Fri, 4 Mar 2022 01:35:21 -0800 Subject: [PATCH 1/3] Revert "bpo-40185: Refactor typing.NamedTuple (GH-19371)" This reverts commit a2ec06938f46683e33692615aca3875d8b8e110c. --- Lib/test/test_typing.py | 28 ++++++++------ Lib/typing.py | 81 +++++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index bd9920436223ce..27d8068e15de44 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4441,9 +4441,11 @@ def test_annotation_usage_with_default(self): self.assertEqual(CoolEmployeeWithDefault._field_defaults, dict(cool=0)) with self.assertRaises(TypeError): - class NonDefaultAfterDefault(NamedTuple): - x: int = 3 - y: int + exec(""" +class NonDefaultAfterDefault(NamedTuple): + x: int = 3 + y: int +""") def test_annotation_usage_with_methods(self): self.assertEqual(XMeth(1).double(), 2) @@ -4452,16 +4454,20 @@ def test_annotation_usage_with_methods(self): self.assertEqual(XRepr(1, 2) + XRepr(3), 0) with self.assertRaises(AttributeError): - class XMethBad(NamedTuple): - x: int - def _fields(self): - return 'no chance for this' + exec(""" +class XMethBad(NamedTuple): + x: int + def _fields(self): + return 'no chance for this' +""") with self.assertRaises(AttributeError): - class XMethBad2(NamedTuple): - x: int - def _source(self): - return 'no chance for this as well' + exec(""" +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 6e0c68c842420b..2f948820cf5870 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2396,41 +2396,51 @@ 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" + 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 return nm_tpl # attributes prohibited to set in NamedTuple class syntax -_prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__', - '_fields', '_field_defaults', - '_make', '_replace', '_asdict', '_source'}) +_prohibited = {'__new__', '__init__', '__slots__', '__getnewargs__', + '_fields', '_field_defaults', + '_make', '_replace', '_asdict', '_source'} -_special = frozenset({'__module__', '__name__', '__annotations__'}) +_special = {'__module__', '__name__', '__annotations__'} 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) + if len(bases) > 1: + raise TypeError("Multiple inheritance with NamedTuple is not supported") + 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 # update from user namespace without overriding special namedtuple attributes for key in ns: if key in _prohibited: @@ -2440,7 +2450,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:: @@ -2464,22 +2474,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): From 28039c7bcf012f0cbffbd6dd6f17cbbb941d1383 Mon Sep 17 00:00:00 2001 From: julius Date: Mon, 7 Mar 2022 14:13:36 -0800 Subject: [PATCH 2/3] revert changes to test_typing.py --- Lib/test/test_typing.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 27d8068e15de44..bd9920436223ce 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4441,11 +4441,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) @@ -4454,20 +4452,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: From c44efc27db55bdefbe74e2a1467b5b22a45f3f2a Mon Sep 17 00:00:00 2001 From: julius Date: Mon, 7 Mar 2022 15:12:54 -0800 Subject: [PATCH 3/3] fix frozenset changes --- Lib/typing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 2f948820cf5870..c2f790ec8a930d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2409,11 +2409,11 @@ def _make_nmtuple(name, types): # 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):