From deaffcd8ae6d556ce0408aacfc27f4d4c4d0c06e Mon Sep 17 00:00:00 2001 From: julius Date: Tue, 8 Mar 2022 19:13:23 -0800 Subject: [PATCH 01/10] initial commit --- Lib/test/test_typing.py | 5 +++ Lib/typing.py | 73 +++++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 5c1e907070ee41..235aa4ee89e74f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5282,6 +5282,11 @@ def test_get_type_hints(self): {'a': typing.Optional[int], 'b': int} ) + def test_generic_subclasses(self): + T = TypeVar("T") + class GenericNamedTuple(NamedTuple, Generic[T]): + pass + class IOTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index abb8bcefc5c04a..9a6a1b0cb66215 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2595,13 +2595,15 @@ 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 @@ -2616,20 +2618,28 @@ def _make_nmtuple(name, types, module, defaults = ()): 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: @@ -2639,7 +2649,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:: @@ -2663,22 +2673,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 a93e0d22d29f6ee524b44decf9ec5f41e9202ac4 Mon Sep 17 00:00:00 2001 From: julius Date: Tue, 8 Mar 2022 19:42:35 -0800 Subject: [PATCH 02/10] one change was mangled by revert --- Lib/typing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 9a6a1b0cb66215..2b68677e89ba5e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2620,8 +2620,6 @@ 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 types = ns.get('__annotations__', {}) nm_tpl = _make_nmtuple(typename, types.items()) From a921abd78eda240910e89c8a05d606bd9ef5e727 Mon Sep 17 00:00:00 2001 From: julius Date: Tue, 8 Mar 2022 20:13:20 -0800 Subject: [PATCH 03/10] add news entry --- .../next/Library/2022-03-08-20-13-08.bpo-43923.gefhqh.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-03-08-20-13-08.bpo-43923.gefhqh.rst diff --git a/Misc/NEWS.d/next/Library/2022-03-08-20-13-08.bpo-43923.gefhqh.rst b/Misc/NEWS.d/next/Library/2022-03-08-20-13-08.bpo-43923.gefhqh.rst new file mode 100644 index 00000000000000..d849cf05eaaa7f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-03-08-20-13-08.bpo-43923.gefhqh.rst @@ -0,0 +1,2 @@ +Multiple inheritance with :class:`typing.NamedTuple` now no longer raises an +error. From ec69c42fdf2eafc28c0764283b2256a51b0604ea Mon Sep 17 00:00:00 2001 From: julius Date: Wed, 9 Mar 2022 12:10:08 -0800 Subject: [PATCH 04/10] fix failing test --- Lib/test/test_typing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 235aa4ee89e74f..3068ffb5db02ca 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4947,9 +4947,8 @@ def _source(self): def test_multiple_inheritance(self): class A: pass - with self.assertRaises(TypeError): - class X(NamedTuple, A): - x: int + class X(NamedTuple, A): + x: int def test_namedtuple_keyword_usage(self): LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) From b3d8c38431d7f9b9d7b2262969a831afe4e7c158 Mon Sep 17 00:00:00 2001 From: julius Date: Thu, 10 Mar 2022 08:28:36 -0800 Subject: [PATCH 05/10] refactor NamedTuple --- Lib/typing.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 2b68677e89ba5e..3599c656a64cf6 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2595,15 +2595,13 @@ 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, defaults=(), module=None): + 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__ = dict(types) - try: - nm_tpl.__module__ = sys._getframe(2).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass return nm_tpl @@ -2620,24 +2618,22 @@ class NamedTupleMeta(type): def __new__(cls, typename, bases, ns): if ns.get('_root', False): return super().__new__(cls, typename, bases, ns) - assert bases[0] is NamedTuple + assert NamedTuple in bases 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: + default_names.append(field_name) + elif default_names: 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=', '.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: From 4daa7f2710c7b7355bb272d6158569782ad1f3b5 Mon Sep 17 00:00:00 2001 From: julius Date: Thu, 10 Mar 2022 09:36:46 -0800 Subject: [PATCH 06/10] fix up some issues after refactor --- Lib/typing.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 3599c656a64cf6..886eab27afe47c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2595,13 +2595,13 @@ def __round__(self, ndigits: int = 0) -> T_co: pass -def _make_nmtuple(name, types, defaults=(), module=None): +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__ = dict(types) + nm_tpl.__annotations__ = types return nm_tpl @@ -2625,15 +2625,14 @@ def __new__(cls, typename, bases, ns): if field_name in ns: default_names.append(field_name) elif default_names: - raise TypeError("Non-default namedtuple field {field_name} cannot " - "follow default field(s) {default_names}" - .format(field_name=field_name, - default_names=', '.join(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: @@ -2675,7 +2674,7 @@ def __new__(cls, typename, fields=None, /, **kwargs): elif kwargs: raise TypeError("Either list of fields or keywords" " can be provided to NamedTuple, not both") - return _make_nmtuple(typename, fields) + return _make_nmtuple(typename, fields, module=_caller()) class _TypedDictMeta(type): From 2d6651617ef1b1b961b5a399322d414a0d471399 Mon Sep 17 00:00:00 2001 From: julius Date: Thu, 10 Mar 2022 09:45:53 -0800 Subject: [PATCH 07/10] fix space --- Lib/test/test_typing.py | 2 ++ Lib/typing.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3068ffb5db02ca..2c0958eab1f5fa 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4949,6 +4949,8 @@ class A: pass class X(NamedTuple, A): x: int + class Y(X, A): + y: int def test_namedtuple_keyword_usage(self): LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) diff --git a/Lib/typing.py b/Lib/typing.py index 886eab27afe47c..2791b4a5023754 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2629,7 +2629,6 @@ def __new__(cls, typename, bases, ns): 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__']) From 031af03f982e283c5bec8a496105cc1b497e809d Mon Sep 17 00:00:00 2001 From: julius Date: Thu, 10 Mar 2022 13:16:51 -0800 Subject: [PATCH 08/10] add back in some things that should have stayed --- Lib/typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 2791b4a5023754..fe6eb2a2899238 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2602,6 +2602,8 @@ def _make_nmtuple(name, types, module, defaults = ()): nm_tpl = collections.namedtuple(name, fields, defaults=defaults, module=module) nm_tpl.__annotations__ = types + nm_tpl.__new__.__annotations__ = types + nm_tpl.__new__.__defaults__ = default return nm_tpl From bf5e7a1fa2ab84f4402d6a4173dfc914a64a9d72 Mon Sep 17 00:00:00 2001 From: julius Date: Thu, 10 Mar 2022 13:31:13 -0800 Subject: [PATCH 09/10] forgot an s --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index fe6eb2a2899238..bb30358039642c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2603,7 +2603,7 @@ def _make_nmtuple(name, types, module, defaults = ()): defaults=defaults, module=module) nm_tpl.__annotations__ = types nm_tpl.__new__.__annotations__ = types - nm_tpl.__new__.__defaults__ = default + nm_tpl.__new__.__defaults__ = defaults return nm_tpl From 0c94643f6cc57a8e776becc27f0e7cf7d2d7c792 Mon Sep 17 00:00:00 2001 From: julius Date: Thu, 10 Mar 2022 14:19:28 -0800 Subject: [PATCH 10/10] cast to tuple again --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index bb30358039642c..42f14e311f6783 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2603,7 +2603,7 @@ def _make_nmtuple(name, types, module, defaults = ()): defaults=defaults, module=module) nm_tpl.__annotations__ = types nm_tpl.__new__.__annotations__ = types - nm_tpl.__new__.__defaults__ = defaults + nm_tpl.__new__.__defaults__ = tuple(defaults) return nm_tpl