From c8311bb5dbffd7b5cd8e2ba9d69e05bcacb914a2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 18 Sep 2019 12:47:39 +0300 Subject: [PATCH 1/2] bpo-40185: Refactor typing.NamedTuple --- Lib/test/test_typing.py | 35 ++++++++--------- Lib/typing.py | 84 +++++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3a0edb9e2d3237..5c4156ef0f8c29 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,23 @@ 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: + pass + with self.assertRaises(TypeError): + class X(NamedTuple, A): + x: int def test_multiple_inheritance(self): class A: diff --git a/Lib/typing.py b/Lib/typing.py index 99355d00666478..03c450bf38b1de 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1701,24 +1701,23 @@ 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]) +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) # Prior to PEP 526, only _field_types attribute was assigned. # Now __annotations__ are used and _field_types is deprecated (remove in 3.9) - nm_tpl.__annotations__ = nm_tpl._field_types = dict(types) - try: - nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass + nm_tpl._field_types = types + 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', '_field_types', - '_make', '_replace', '_asdict', '_source'} +_prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__', + '_fields', '_field_defaults', '_field_types', + '_make', '_replace', '_asdict', '_source'}) _special = {'__module__', '__name__', '__annotations__'} @@ -1726,28 +1725,20 @@ def _make_nmtuple(name, types): 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: @@ -1757,7 +1748,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:: @@ -1781,15 +1772,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): From 4cb1a27f6c350598a988fe12e5c9b1e02ca7cf8c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 5 Apr 2020 00:51:59 +0300 Subject: [PATCH 2/2] Polishing. --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 60a41281eafbe5..7002fb6995b656 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1716,7 +1716,7 @@ def _make_nmtuple(name, types, module, defaults = ()): '_fields', '_field_defaults', '_make', '_replace', '_asdict', '_source'}) -_special = {'__module__', '__name__', '__annotations__'} +_special = frozenset({'__module__', '__name__', '__annotations__'}) class NamedTupleMeta(type):