diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index d688141f9b183d..d9550ce7de8988 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) @@ -448,15 +462,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)) @@ -480,15 +492,19 @@ def __getnewargs__(self): return _tuple(self) # Modify function metadata to help with introspection and debugging - for method in ( + methods = ( __new__, - _make.__func__, + _make, _replace, __repr__, _asdict, __getnewargs__, - ): + ) + for method in methods: method.__qualname__ = f'{typename}.{method.__name__}' + if module is not None: + for method in methods: + method.__module__ = module # Build-up the class namespace dictionary # and use type() to build the result class @@ -498,7 +514,7 @@ def __getnewargs__(self): '_fields': field_names, '_field_defaults': field_defaults, '__new__': __new__, - '_make': _make, + '_make': classmethod(_make), '__replace__': _replace, '_replace': _replace, '__repr__': __repr__, @@ -510,25 +526,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) ######################################################################## diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index a24d3e3ea142b7..de8af31dd3be92 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -468,6 +468,25 @@ 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__', + '_make', + '_replace', + '__repr__', + '_asdict', + '__getnewargs__', + ) + 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) + 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..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,22 @@ 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__', + '_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) 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 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.