Skip to content

Commit 4f91502

Browse files
authored
Backport recent change to NamedTuple classes regarding __set_name__ (#303)
1 parent 7af82f9 commit 4f91502

File tree

3 files changed

+166
-3
lines changed

3 files changed

+166
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
argument to the `msg` parameter. Patch by Alex Waygood.
1010
- Exclude `__match_args__` from `Protocol` members,
1111
this is a backport of https://github.com/python/cpython/pull/110683
12+
- When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__`
13+
is called on all objects that define `__set_name__` and exist in the values
14+
of the `NamedTuple` class's class dictionary. Patch by Alex Waygood,
15+
backporting https://github.com/python/cpython/pull/111876.
1216

1317
# Release 4.8.0 (September 17, 2023)
1418

src/test_typing_extensions.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@
5656
# versions, but not all
5757
HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters
5858

59+
skip_if_early_py313_alpha = skipIf(
60+
sys.version_info[:4] == (3, 13, 0, 'alpha') and sys.version_info.serial < 3,
61+
"Bugfixes will be released in 3.13.0a3"
62+
)
63+
5964
ANN_MODULE_SOURCE = '''\
6065
from typing import Optional
6166
from functools import wraps
@@ -5548,6 +5553,136 @@ class GenericNamedTuple(NamedTuple, Generic[T]):
55485553

55495554
self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,))
55505555

5556+
@skip_if_early_py313_alpha
5557+
def test_setname_called_on_values_in_class_dictionary(self):
5558+
class Vanilla:
5559+
def __set_name__(self, owner, name):
5560+
self.name = name
5561+
5562+
class Foo(NamedTuple):
5563+
attr = Vanilla()
5564+
5565+
foo = Foo()
5566+
self.assertEqual(len(foo), 0)
5567+
self.assertNotIn('attr', Foo._fields)
5568+
self.assertIsInstance(foo.attr, Vanilla)
5569+
self.assertEqual(foo.attr.name, "attr")
5570+
5571+
class Bar(NamedTuple):
5572+
attr: Vanilla = Vanilla()
5573+
5574+
bar = Bar()
5575+
self.assertEqual(len(bar), 1)
5576+
self.assertIn('attr', Bar._fields)
5577+
self.assertIsInstance(bar.attr, Vanilla)
5578+
self.assertEqual(bar.attr.name, "attr")
5579+
5580+
@skipIf(
5581+
TYPING_3_12_0,
5582+
"__set_name__ behaviour changed on py312+ to use BaseException.add_note()"
5583+
)
5584+
def test_setname_raises_the_same_as_on_other_classes_py311_minus(self):
5585+
class CustomException(BaseException): pass
5586+
5587+
class Annoying:
5588+
def __set_name__(self, owner, name):
5589+
raise CustomException
5590+
5591+
annoying = Annoying()
5592+
5593+
with self.assertRaises(RuntimeError) as cm:
5594+
class NormalClass:
5595+
attr = annoying
5596+
normal_exception = cm.exception
5597+
5598+
with self.assertRaises(RuntimeError) as cm:
5599+
class NamedTupleClass(NamedTuple):
5600+
attr = annoying
5601+
namedtuple_exception = cm.exception
5602+
5603+
expected_note = (
5604+
"Error calling __set_name__ on 'Annoying' instance "
5605+
"'attr' in 'NamedTupleClass'"
5606+
)
5607+
5608+
self.assertIs(type(namedtuple_exception), RuntimeError)
5609+
self.assertIs(type(namedtuple_exception), type(normal_exception))
5610+
self.assertEqual(len(namedtuple_exception.args), len(normal_exception.args))
5611+
self.assertEqual(
5612+
namedtuple_exception.args[0],
5613+
normal_exception.args[0].replace("NormalClass", "NamedTupleClass")
5614+
)
5615+
5616+
self.assertIs(type(namedtuple_exception.__cause__), CustomException)
5617+
self.assertIs(
5618+
type(namedtuple_exception.__cause__), type(normal_exception.__cause__)
5619+
)
5620+
self.assertEqual(
5621+
namedtuple_exception.__cause__.args, normal_exception.__cause__.args
5622+
)
5623+
5624+
@skipUnless(
5625+
TYPING_3_12_0,
5626+
"__set_name__ behaviour changed on py312+ to use BaseException.add_note()"
5627+
)
5628+
@skip_if_early_py313_alpha
5629+
def test_setname_raises_the_same_as_on_other_classes_py312_plus(self):
5630+
class CustomException(BaseException): pass
5631+
5632+
class Annoying:
5633+
def __set_name__(self, owner, name):
5634+
raise CustomException
5635+
5636+
annoying = Annoying()
5637+
5638+
with self.assertRaises(CustomException) as cm:
5639+
class NormalClass:
5640+
attr = annoying
5641+
normal_exception = cm.exception
5642+
5643+
with self.assertRaises(CustomException) as cm:
5644+
class NamedTupleClass(NamedTuple):
5645+
attr = annoying
5646+
namedtuple_exception = cm.exception
5647+
5648+
expected_note = (
5649+
"Error calling __set_name__ on 'Annoying' instance "
5650+
"'attr' in 'NamedTupleClass'"
5651+
)
5652+
5653+
self.assertIs(type(namedtuple_exception), CustomException)
5654+
self.assertIs(type(namedtuple_exception), type(normal_exception))
5655+
self.assertEqual(namedtuple_exception.args, normal_exception.args)
5656+
5657+
self.assertEqual(len(namedtuple_exception.__notes__), 1)
5658+
self.assertEqual(
5659+
len(namedtuple_exception.__notes__), len(normal_exception.__notes__)
5660+
)
5661+
5662+
self.assertEqual(namedtuple_exception.__notes__[0], expected_note)
5663+
self.assertEqual(
5664+
namedtuple_exception.__notes__[0],
5665+
normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass")
5666+
)
5667+
5668+
@skip_if_early_py313_alpha
5669+
def test_strange_errors_when_accessing_set_name_itself(self):
5670+
class CustomException(Exception): pass
5671+
5672+
class Meta(type):
5673+
def __getattribute__(self, attr):
5674+
if attr == "__set_name__":
5675+
raise CustomException
5676+
return object.__getattribute__(self, attr)
5677+
5678+
class VeryAnnoying(metaclass=Meta): pass
5679+
5680+
very_annoying = VeryAnnoying()
5681+
5682+
with self.assertRaises(CustomException):
5683+
class Foo(NamedTuple):
5684+
attr = very_annoying
5685+
55515686

55525687
class TypeVarTests(BaseTestCase):
55535688
def test_basic_plain(self):

src/typing_extensions.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,11 +2467,35 @@ def __new__(cls, typename, bases, ns):
24672467
class_getitem = typing.Generic.__class_getitem__.__func__
24682468
nm_tpl.__class_getitem__ = classmethod(class_getitem)
24692469
# update from user namespace without overriding special namedtuple attributes
2470-
for key in ns:
2470+
for key, val in ns.items():
24712471
if key in _prohibited_namedtuple_fields:
24722472
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
2473-
elif key not in _special_namedtuple_fields and key not in nm_tpl._fields:
2474-
setattr(nm_tpl, key, ns[key])
2473+
elif key not in _special_namedtuple_fields:
2474+
if key not in nm_tpl._fields:
2475+
setattr(nm_tpl, key, ns[key])
2476+
try:
2477+
set_name = type(val).__set_name__
2478+
except AttributeError:
2479+
pass
2480+
else:
2481+
try:
2482+
set_name(val, nm_tpl, key)
2483+
except BaseException as e:
2484+
msg = (
2485+
f"Error calling __set_name__ on {type(val).__name__!r} "
2486+
f"instance {key!r} in {typename!r}"
2487+
)
2488+
# BaseException.add_note() existed on py311,
2489+
# but the __set_name__ machinery didn't start
2490+
# using add_note() until py312.
2491+
# Making sure exceptions are raised in the same way
2492+
# as in "normal" classes seems most important here.
2493+
if sys.version_info >= (3, 12):
2494+
e.add_note(msg)
2495+
raise
2496+
else:
2497+
raise RuntimeError(msg) from e
2498+
24752499
if typing.Generic in bases:
24762500
nm_tpl.__init_subclass__()
24772501
return nm_tpl

0 commit comments

Comments
 (0)