Skip to content

Commit e3b703d

Browse files
committed
gh-111874: Call __set_name__ on objects that define the method inside a typing.NamedTuple class dictionary as part of the creation of that class
1 parent 97c4c06 commit e3b703d

File tree

3 files changed

+64
-2
lines changed

3 files changed

+64
-2
lines changed

Lib/test/test_typing.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7519,6 +7519,55 @@ class GenericNamedTuple(NamedTuple, Generic[T]):
75197519

75207520
self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,))
75217521

7522+
def test_setname_called_on_non_members(self):
7523+
class Vanilla:
7524+
def __set_name__(self, owner, name):
7525+
self.name = name
7526+
7527+
class Foo(NamedTuple):
7528+
attr = Vanilla()
7529+
7530+
foo = Foo()
7531+
self.assertEqual(len(foo), 0)
7532+
self.assertNotIn('attr', Foo._fields)
7533+
self.assertIsInstance(foo.attr, Vanilla)
7534+
self.assertEqual(foo.attr.name, "attr")
7535+
7536+
def test_setname_raises_the_same_as_on_other_classes(self):
7537+
class CustomException(Exception): pass
7538+
7539+
class Annoying:
7540+
def __set_name__(self, owner, name):
7541+
raise CustomException("Cannot do that!")
7542+
7543+
with self.assertRaisesRegex(CustomException, "Cannot do that!") as cm:
7544+
class NormalClass:
7545+
attr = Annoying()
7546+
normal_exception = cm.exception
7547+
7548+
with self.assertRaisesRegex(CustomException, "Cannot do that!") as cm:
7549+
class NamedTupleClass:
7550+
attr = Annoying()
7551+
namedtuple_exception = cm.exception
7552+
7553+
self.assertIs(type(namedtuple_exception), CustomException)
7554+
self.assertIs(type(namedtuple_exception), type(normal_exception))
7555+
7556+
self.assertEqual(len(namedtuple_exception.__notes__), 1)
7557+
self.assertEqual(
7558+
len(namedtuple_exception.__notes__), len(normal_exception.__notes__)
7559+
)
7560+
7561+
expected_note = (
7562+
"Error calling __set_name__ on 'Annoying' instance "
7563+
"'attr' in 'NamedTupleClass'"
7564+
)
7565+
self.assertEqual(namedtuple_exception.__notes__[0], expected_note)
7566+
self.assertEqual(
7567+
namedtuple_exception.__notes__[0],
7568+
normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass")
7569+
)
7570+
75227571

75237572
class TypedDictTests(BaseTestCase):
75247573
def test_basics_functional_syntax(self):

Lib/typing.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2726,11 +2726,20 @@ def __new__(cls, typename, bases, ns):
27262726
class_getitem = _generic_class_getitem
27272727
nm_tpl.__class_getitem__ = classmethod(class_getitem)
27282728
# update from user namespace without overriding special namedtuple attributes
2729-
for key in ns:
2729+
for key, val in ns.items():
27302730
if key in _prohibited:
27312731
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
27322732
elif key not in _special and key not in nm_tpl._fields:
2733-
setattr(nm_tpl, key, ns[key])
2733+
setattr(nm_tpl, key, val)
2734+
if hasattr(type(val), "__set_name__"):
2735+
try:
2736+
type(val).__set_name__(val, nm_tpl, key)
2737+
except Exception as e:
2738+
e.add_note(
2739+
f"Error calling __set_name__ on {type(val).__name__!r} "
2740+
f"instance {key!r} in {typename!r}"
2741+
)
2742+
raise
27342743
if Generic in bases:
27352744
nm_tpl.__init_subclass__()
27362745
return nm_tpl
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When creating a :class:`typing.NamedTuple` class, ensure
2+
:func:`__set_name__` is called on all objects that define ``__set_name__``
3+
and exist in the values of the ``NamedTuple`` class's class dictionary.
4+
Patch by Alex Waygood.

0 commit comments

Comments
 (0)