Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 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
5 changes: 5 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2389,6 +2389,11 @@ types.
disallowed in Python 3.15. To create a NamedTuple class with 0 fields,
use ``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``.

.. versionchanged:: 3.14
Added support for calls to :func:`super` inside user-defined methods
of ``NamedTuple`` subclasses to reuse functionality from built-in classes
:class:`tuple` and :class:`object`.

.. class:: NewType(name, tp)

Helper class to create low-overhead :ref:`distinct types <distinct>`.
Expand Down
61 changes: 36 additions & 25 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,30 +358,10 @@ def __ror__(self, other):
except ImportError:
_tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc)

def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
"""Returns a new subclass of tuple with named fields.

>>> Point = namedtuple('Point', ['x', 'y'])
>>> Point.__doc__ # docstring for the new class
'Point(x, y)'
>>> p = Point(11, y=22) # instantiate with positional args or keywords
>>> p[0] + p[1] # indexable like a plain tuple
33
>>> x, y = p # unpack like a regular tuple
>>> x, y
(11, 22)
>>> p.x + p.y # fields also accessible by name
33
>>> d = p._asdict() # convert to a dictionary
>>> d['x']
11
>>> Point(**d) # convert from a dictionary
Point(x=11, y=22)
>>> p._replace(x=100) # _replace() is like str.replace() but targets named fields
Point(x=100, y=22)

"""
_nmtuple_classcell_sentinel = object()

def _namedtuple(typename, field_names, *, rename=False, defaults=None, module=None,
classcell=_nmtuple_classcell_sentinel, stack_offset=1):
# Validate the field names. At the user's option, either generate an error
# message or automatically replace the field name with a valid name.
if isinstance(field_names, str):
Expand Down Expand Up @@ -508,6 +488,12 @@ def __getnewargs__(self):
'__getnewargs__': __getnewargs__,
'__match_args__': field_names,
}

# gh-85795: `super()` calls inside `typing.NamedTuple` methods will not
# work unless `__classcell__` is propagated by `collections._namedtuple`
if classcell is not _nmtuple_classcell_sentinel:
class_namespace["__classcell__"] = classcell

for index, name in enumerate(field_names):
doc = _sys.intern(f'Alias for field number {index}')
class_namespace[name] = _tuplegetter(index, doc)
Expand All @@ -521,17 +507,42 @@ def __getnewargs__(self):
# specified a particular module.
if module is None:
try:
module = _sys._getframemodulename(1) or '__main__'
module = _sys._getframemodulename(stack_offset) or '__main__'
except AttributeError:
try:
module = _sys._getframe(1).f_globals.get('__name__', '__main__')
module = _sys._getframe(stack_offset).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
if module is not None:
result.__module__ = module

return result

def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
"""Returns a new subclass of tuple with named fields.

>>> Point = namedtuple('Point', ['x', 'y'])
>>> Point.__doc__ # docstring for the new class
'Point(x, y)'
>>> p = Point(11, y=22) # instantiate with positional args or keywords
>>> p[0] + p[1] # indexable like a plain tuple
33
>>> x, y = p # unpack like a regular tuple
>>> x, y
(11, 22)
>>> p.x + p.y # fields also accessible by name
33
>>> d = p._asdict() # convert to a dictionary
>>> d['x']
11
>>> Point(**d) # convert from a dictionary
Point(x=11, y=22)
>>> p._replace(x=100) # _replace() is like str.replace() but targets named fields
Point(x=100, y=22)

"""
return _namedtuple(typename, field_names, rename=rename, defaults=defaults, module=module,
stack_offset=2)

########################################################################
### Counter
Expand Down
32 changes: 32 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8135,6 +8135,38 @@ class Group(NamedTuple):
self.assertIs(type(a), Group)
self.assertEqual(a, (1, [2]))

def test_super_and_dunder_class_work(self):
# See #85795: __class__ not set defining 'X' as <class '__main__.X'>

class Pointer(NamedTuple):
address: int
target_type = "int"

@property
def typename(self):
return __class__.target_type

def count(self, item):
if item == 0:
return -1
return super().count(self.address)

ptr = Pointer(0xdeadbeef)
self.assertEqual(ptr.typename, "int")
self.assertEqual(ptr.count(0), -1)
self.assertEqual(ptr.count(0xdeadbeef), 1)

@cpython_only
def test_classcell_not_leaked(self):
# __classcell__ should never leak into end classes

class Spam(NamedTuple):
lambda: super()
lambda: __class__

with self.assertRaises(AttributeError):
Spam.__classcell__

def test_namedtuple_keyword_usage(self):
with self.assertWarnsRegex(
DeprecationWarning,
Expand Down
11 changes: 6 additions & 5 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2929,9 +2929,10 @@ def __round__(self, ndigits: int = 0) -> T:
pass


def _make_nmtuple(name, fields, annotate_func, module, defaults = ()):
nm_tpl = collections.namedtuple(name, fields,
defaults=defaults, module=module)
def _make_nmtuple(name, fields, annotate_func, module, defaults = (),
classcell=collections._nmtuple_classcell_sentinel):
nm_tpl = collections._namedtuple(name, fields, defaults=defaults,
module=module, classcell=classcell)
nm_tpl.__annotate__ = nm_tpl.__new__.__annotate__ = annotate_func
return nm_tpl

Expand Down Expand Up @@ -2999,8 +3000,8 @@ def annotate(format):
f"{'s' if len(default_names) > 1 else ''} "
f"{', '.join(default_names)}")
nm_tpl = _make_nmtuple(typename, field_names, annotate,
defaults=[ns[n] for n in default_names],
module=ns['__module__'])
defaults=[ns[n] for n in default_names], module=ns['__module__'],
classcell=ns.pop('__classcell__', collections._nmtuple_classcell_sentinel))
nm_tpl.__bases__ = bases
if Generic in bases:
class_getitem = _generic_class_getitem
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added support for :func:`super` calls in user-defined
:class:`~typing.NamedTuple` methods. Contributed by Bartosz Sławecki.
Loading