From c01ca291909d79e4f5b3134ceef7d29925198921 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 23 Nov 2024 13:32:46 +0800 Subject: [PATCH 1/8] Set correct `__module__` attribute for methods of named tuple types --- Lib/collections/__init__.py | 41 ++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index d688141f9b183d..2a7ade57b23bdc 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -479,16 +479,33 @@ def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) + # For pickling to work, the __module__ variable needs to be set to the frame + # where the named tuple is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython), or where the user has + # specified a particular module. + if module is None: + try: + module = _sys._getframemodulename(1) or '__main__' + except AttributeError: + try: + module = _sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass + # Modify function metadata to help with introspection and debugging - for method in ( + methods = ( __new__, _make.__func__, _replace, __repr__, _asdict, __getnewargs__, - ): + ) + for method in methods: method.__qualname__ = f'{typename}.{method.__name__}' + if module is not None: + method.__module__ = module # Build-up the class namespace dictionary # and use type() to build the result class @@ -510,25 +527,11 @@ def __getnewargs__(self): doc = _sys.intern(f'Alias for field number {index}') class_namespace[name] = _tuplegetter(index, doc) - result = type(typename, (tuple,), class_namespace) - - # For pickling to work, the __module__ variable needs to be set to the frame - # where the named tuple is created. Bypass this step in environments where - # sys._getframe is not defined (Jython for example) or sys._getframe is not - # defined for arguments greater than 0 (IronPython), or where the user has - # specified a particular module. - if module is None: - try: - module = _sys._getframemodulename(1) or '__main__' - except AttributeError: - try: - module = _sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass + # Set `__module__` where the named tuple is created for pickling. if module is not None: - result.__module__ = module + class_namespace['__module__'] = module - return result + return type(typename, (tuple,), class_namespace) ######################################################################## From 38e518364f4209fea9eb4255994cfd821908aac8 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 23 Nov 2024 13:36:15 +0800 Subject: [PATCH 2/8] Simplify classmethod definition for `namedtuple._make` --- Lib/collections/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 2a7ade57b23bdc..af2247b628059b 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -448,15 +448,13 @@ def namedtuple(typename, field_names, *, rename=False, defaults=None, module=Non if defaults is not None: __new__.__defaults__ = defaults - @classmethod - def _make(cls, iterable): + def _make(cls, iterable): # will be wrapped as classmethod below result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result - _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence ' - 'or iterable') + _make.__doc__ = f'Make a new {typename} object from a sequence or iterable' def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) @@ -496,7 +494,7 @@ def __getnewargs__(self): # Modify function metadata to help with introspection and debugging methods = ( __new__, - _make.__func__, + _make, _replace, __repr__, _asdict, @@ -515,7 +513,7 @@ def __getnewargs__(self): '_fields': field_names, '_field_defaults': field_defaults, '__new__': __new__, - '_make': _make, + '_make': classmethod(_make), '__replace__': _replace, '_replace': _replace, '__repr__': __repr__, From 02ad6dd639e5db9155d6aaa0ad8fcf38e77ae3f0 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 23 Nov 2024 13:51:36 +0800 Subject: [PATCH 3/8] Add tests for `__module__` of namedtuple types --- Lib/test/test_collections.py | 15 +++++++++++++++ Lib/test/test_typing.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index a24d3e3ea142b7..aab945952f91aa 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -468,6 +468,21 @@ def test_module_parameter(self): NT = namedtuple('NT', ['x', 'y'], module=collections) self.assertEqual(NT.__module__, collections) + def test_module_attribute(self): + method_names = ( + '__new__', + '_make', + '_replace', + '__repr__', + '_asdict', + '__getnewargs__', + ) + for module in ('some.module', collections): + NT = namedtuple('NT', ['x', 'y'], module=module) + self.assertEqual(NT.__module__, module) + for method in method_names: + self.assertEqual(getattr(NT, method).__module__, module) + def test_instance(self): Point = namedtuple('Point', 'x y') p = Point(11, 22) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index aa42beca5f9256..c5093a840fc42c 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -7839,6 +7839,20 @@ def test_basics(self): self.assertEqual(Emp.__annotations__, collections.OrderedDict([('name', str), ('id', int)])) + def test_module_attribute(self): + method_names = ( + '__new__', + '_make', + '_replace', + '__repr__', + '_asdict', + '__getnewargs__', + ) + for nt in (CoolEmployee, self.NestedEmployee): + self.assertEqual(nt.__module__, __name__) + for method in method_names: + self.assertEqual(getattr(nt, method).__module__, __name__) + def test_annotation_usage(self): tim = CoolEmployee('Tim', 9000) self.assertIsInstance(tim, CoolEmployee) From 632c992d23a89a25d414da8a4d7f729576545a07 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 23 Nov 2024 05:55:05 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2024-11-23-05-55-03.gh-issue-127187.ruRv7S.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-11-23-05-55-03.gh-issue-127187.ruRv7S.rst diff --git a/Misc/NEWS.d/next/Library/2024-11-23-05-55-03.gh-issue-127187.ruRv7S.rst b/Misc/NEWS.d/next/Library/2024-11-23-05-55-03.gh-issue-127187.ruRv7S.rst new file mode 100644 index 00000000000000..4876258114a04a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-23-05-55-03.gh-issue-127187.ruRv7S.rst @@ -0,0 +1 @@ +Set correct ``__module__`` attribute for methods of named tuple types. Patch by Xuehai Pan. From bd3d759de6a33918db23e93bf7e288342f4dfe25 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 23 Nov 2024 14:07:33 +0800 Subject: [PATCH 5/8] Add Xuehai Pan to `ACKS` --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 08cd293eac3835..b43468d9d25212 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1389,6 +1389,7 @@ Todd R. Palmer Juan David Ibáñez Palomar Nicola Palumbo Jan Palus +Xuehai Pan Yongzhi Pan Martin Panter Mathias Panzenböck From cc4146519badf9e89d4c462ead465fc0d5fb3a83 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 23 Nov 2024 14:30:27 +0800 Subject: [PATCH 6/8] Fix tests --- Lib/collections/__init__.py | 30 +++++++++++++++--------------- Lib/test/test_collections.py | 2 ++ Lib/test/test_typing.py | 4 +++- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index af2247b628059b..821b8c37f47146 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -434,12 +434,26 @@ def namedtuple(typename, field_names, *, rename=False, defaults=None, module=Non tuple_new = tuple.__new__ _dict, _tuple, _len, _map, _zip = dict, tuple, len, map, zip + # For pickling to work, the __module__ variable needs to be set to the frame + # where the named tuple is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython), or where the user has + # specified a particular module. + if module is None: + try: + module = _sys._getframemodulename(1) or '__main__' + except AttributeError: + try: + module = _sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass + # Create all the named tuple methods to be added to the class namespace namespace = { '_tuple_new': tuple_new, '__builtins__': {}, - '__name__': f'namedtuple_{typename}', + '__name__': module or f'namedtuple_{typename}', } code = f'lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))' __new__ = eval(code, namespace) @@ -477,20 +491,6 @@ def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) - # For pickling to work, the __module__ variable needs to be set to the frame - # where the named tuple is created. Bypass this step in environments where - # sys._getframe is not defined (Jython for example) or sys._getframe is not - # defined for arguments greater than 0 (IronPython), or where the user has - # specified a particular module. - if module is None: - try: - module = _sys._getframemodulename(1) or '__main__' - except AttributeError: - try: - module = _sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass - # Modify function metadata to help with introspection and debugging methods = ( __new__, diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index aab945952f91aa..5bea7ba9c05c4f 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -468,6 +468,8 @@ def test_module_parameter(self): NT = namedtuple('NT', ['x', 'y'], module=collections) self.assertEqual(NT.__module__, collections) + @unittest.skipUnless(hasattr(sys, '_getframemodulename') or hasattr(sys, '_getframe'), + "Maybe cannot get the module name from the frame.") def test_module_attribute(self): method_names = ( '__new__', diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index c5093a840fc42c..09f191492de148 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -12,7 +12,7 @@ import pickle import re import sys -from unittest import TestCase, main, skip +from unittest import TestCase, main, skip, skipUnless from unittest.mock import patch from copy import copy, deepcopy @@ -7839,6 +7839,8 @@ def test_basics(self): self.assertEqual(Emp.__annotations__, collections.OrderedDict([('name', str), ('id', int)])) + @skipUnless(hasattr(sys, '_getframemodulename') or hasattr(sys, '_getframe'), + "Maybe cannot get the module name from the frame.") def test_module_attribute(self): method_names = ( '__new__', From c44a203d7d3b558b93f9cbc3fc42a007b408b642 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 23 Nov 2024 14:33:59 +0800 Subject: [PATCH 7/8] Fix tests --- Lib/collections/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 821b8c37f47146..d9550ce7de8988 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -503,7 +503,8 @@ def __getnewargs__(self): for method in methods: method.__qualname__ = f'{typename}.{method.__name__}' if module is not None: - method.__module__ = module + for method in methods: + method.__module__ = module # Build-up the class namespace dictionary # and use type() to build the result class From 935d7c5300d9bf5f8876eba184d3b4e3ebca260f Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 24 Nov 2024 21:18:26 +0800 Subject: [PATCH 8/8] Add more tests --- Lib/test/test_collections.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 5bea7ba9c05c4f..de8af31dd3be92 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -479,8 +479,10 @@ def test_module_attribute(self): '_asdict', '__getnewargs__', ) - for module in ('some.module', collections): + for module in (None, 'some.module', collections): NT = namedtuple('NT', ['x', 'y'], module=module) + if module is None: + module = __name__ self.assertEqual(NT.__module__, module) for method in method_names: self.assertEqual(getattr(NT, method).__module__, module)