8000 gh-111874: Call `__set_name__` on objects that define the method insi… · python/cpython@22e411e · GitHub
[go: up one dir, main page]

Skip to content

Commit 22e411e

Browse files
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 (#111876)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent ffe1b2d commit 22e411e

File tree

3 files changed

+99
-3
lines changed

3 files changed

+99
-3
lines changed

Lib/test/test_typing.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7535,6 +7535,83 @@ class GenericNamedTuple(NamedTuple, Generic[T]):
75357535

75367536
self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,))
75377537

7538+
def test_setname_called_on_values_in_class_dictionary(self):
7539+
class Vanilla:
7540+
def __set_name__(self, owner, name):
7541+
self.name = name
7542+
7543+
class Foo(NamedTuple):
7544+
attr = Vanilla()
7545+
7546+
foo = Foo()
7547+
self.assertEqual(len(foo), 0)
7548+
self.assertNotIn('attr', Foo._fields)
7549+
self.assertIsInstance(foo.attr, Vanilla)
7550+
self.assertEqual(foo.attr.name, "attr")
7551+
7552+
class Bar(NamedTuple):
7553+
attr: Vanilla = Vanilla()
7554+
7555+
bar = Bar()
7556+
self.assertEqual(len(bar), 1)
7557+
self.assertIn('attr', Bar._fields)
7558+
self.assertIsInstance(bar.attr, Vanilla)
7559+
self.assertEqual(bar.attr.name, "attr")
7560+
7561+
def test_setname_raises_the_same_as_on_other_classes(self):
7562+
class CustomException(BaseException): pass
7563+
7564+
class Annoying:
7565+
def __set_name__(self, owner, name):
7566+
raise CustomException
7567+
7568+
annoying = Annoying()
7569+
7570+
with self.assertRaises(CustomException) as cm:
7571+
class NormalClass:
7572+
attr = annoying
7573+
normal_exception = cm.exception
7574+
7575+
with self.assertRaises(CustomException) as cm:
7576+
class NamedTupleClass(NamedTuple):
7577+
attr = annoying
7578+
namedtuple_exception = cm.exception
7579+
7580+
self.assertIs(type(namedtuple_exception), CustomException)
7581+
self.assertIs(type(namedtuple_exception), type(normal_exception))
7582+
7583+
self.assertEqual(len(namedtuple_exception.__notes__), 1)
7584+
self.assertEqual(
7585+
len(namedtuple_exception.__notes__), len(normal_exception.__notes__)
7586+
)
7587+
7588+
expected_note = (
7589+
"Error calling __set_name__ on 'Annoying' instance "
7590+
"'attr' in 'NamedTupleClass'"
7591+
)
7592+
self.assertEqual(namedtuple_exception.__notes__[0], expected_note)
7593+
self.assertEqual(
7594+
namedtuple_exception.__notes__[0],
7595+
normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass")
7596+
)
7597+
7598+
def test_strange_errors_when_accessing_set_name_itself(self):
7599+
class CustomException(Exception): pass
7600+
7601+
class Meta(type):
7602+
def __getattribute__(self, attr):
7603+
if attr == "__set_name__":
7604+
raise CustomException
7605+
return object.__getattribute__(self, attr)
7606+
7607+
class VeryAnnoying(metaclass=Meta): pass
7608+
7609+
very_annoying = VeryAnnoying()
7610+
7611+
with self.assertRaises(CustomException):
7612+
class Foo(NamedTuple):
7613+
attr = very_annoying
7614+
75387615

75397616
class TypedDictTests(BaseTestCase):
75407617
def test_basics_functional_syntax(self):

Lib/typing.py

Lines changed: 18 additions & 3 deletions
< 57AE tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -2743,11 +2743,26 @@ def __new__(cls, typename, bases, ns):
27432743
class_getitem = _generic_class_getitem
27442744
nm_tpl.__class_getitem__ = classmethod(class_getitem)
27452745
# update from user namespace without overriding special namedtuple attributes
2746-
for key in ns:
2746+
for key, val in ns.items():
27472747
if key in _prohibited:
27482748
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
2749-
elif key not in _special and key not in nm_tpl._fields:
2750-
setattr(nm_tpl, key, ns[key])
2749+
elif key not in _special:
2750+
if key not in nm_tpl._fields:
2751+
setattr(nm_tpl, key, val)
2752+
try:
2753+
set_name = type(val).__set_name__
2754+
except AttributeError:
2755+
pass
2756+
else:
2757+
try:
2758+
set_name(val, nm_tpl, key)
2759+
except BaseException as e:
2760+
e.add_note(
2761+
f"Error calling __set_name__ on {type(val).__name__!r} "
2762+
f"instance {key!r} in {typename!r}"
2763+
)
2764+
raise
2765+
27512766
if Generic in bases:
27522767
nm_tpl.__init_subclass__()
27532768
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:`~object.__set_name__` is called on all objects that define
3+
``__set_name__`` and exist in the values of the ``NamedTuple`` class's class
4+
dictionary. Patch by Alex Waygood.

0 commit comments

Comments
 (0)
0