Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 28 additions & 26 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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
Expand All @@ -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__,
Expand All @@ -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)


########################################################################
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 17 additions & 1 deletion Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Set correct ``__module__`` attribute for methods of named tuple types. Patch by Xuehai Pan.
Loading