diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 0fee782121b0af..7c742d9ad9c620 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -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:: next + 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 `. diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 78229ac54b80da..3bcc0a495a80d9 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -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): @@ -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) @@ -521,10 +507,10 @@ 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: @@ -532,6 +518,31 @@ def __getnewargs__(self): 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 diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f002d28df60e9c..ee56442983f64d 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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 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, diff --git a/Lib/typing.py b/Lib/typing.py index 66570db7a5bd74..9c62a979131085 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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 @@ -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 diff --git a/Misc/NEWS.d/next/Library/2025-01-27-12-55-40.gh-issue-85795.fnGbGS.rst b/Misc/NEWS.d/next/Library/2025-01-27-12-55-40.gh-issue-85795.fnGbGS.rst new file mode 100644 index 00000000000000..1095cd8d915203 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-27-12-55-40.gh-issue-85795.fnGbGS.rst @@ -0,0 +1,2 @@ +Added support for :func:`super` calls in user-defined +:class:`~typing.NamedTuple` methods. Contributed by Bartosz Sławecki.