From a2ec06938f46683e33692615aca3875d8b8e110c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Apr 2020 10:59:04 +0300 Subject: [PATCH] bpo-40185: Refactor typing.NamedTuple (GH-19371) --- Lib/test/test_typing.py | 28 ++++++-------- Lib/typing.py | 83 +++++++++++++++++++++-------------------- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 95f865f8c34..489836c459b 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 6cc3b0342ec..bcb2233ee50 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 + 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) - 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) +_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):