Skip to content

Commit

Permalink
gh-111874: Call __set_name__ on objects that define the method insi…
Browse files Browse the repository at this point in the history
…de a `typing.NamedTuple` class dictionary as part of the creation of that class (#111876)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
AlexWaygood and JelleZijlstra committed Nov 27, 2023
1 parent ffe1b2d commit 22e411e
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 3 deletions.
77 changes: 77 additions & 0 deletions Lib/test/test_typing.py
Expand Up @@ -7535,6 +7535,83 @@ class GenericNamedTuple(NamedTuple, Generic[T]):

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

def test_setname_called_on_values_in_class_dictionary(self):
class Vanilla:
def __set_name__(self, owner, name):
self.name = name

class Foo(NamedTuple):
attr = Vanilla()

foo = Foo()
self.assertEqual(len(foo), 0)
self.assertNotIn('attr', Foo._fields)
self.assertIsInstance(foo.attr, Vanilla)
self.assertEqual(foo.attr.name, "attr")

class Bar(NamedTuple):
attr: Vanilla = Vanilla()

bar = Bar()
self.assertEqual(len(bar), 1)
self.assertIn('attr', Bar._fields)
self.assertIsInstance(bar.attr, Vanilla)
self.assertEqual(bar.attr.name, "attr")

def test_setname_raises_the_same_as_on_other_classes(self):
class CustomException(BaseException): pass

class Annoying:
def __set_name__(self, owner, name):
raise CustomException

annoying = Annoying()

with self.assertRaises(CustomException) as cm:
class NormalClass:
attr = annoying
normal_exception = cm.exception

with self.assertRaises(CustomException) as cm:
class NamedTupleClass(NamedTuple):
attr = annoying
namedtuple_exception = cm.exception

self.assertIs(type(namedtuple_exception), CustomException)
self.assertIs(type(namedtuple_exception), type(normal_exception))

self.assertEqual(len(namedtuple_exception.__notes__), 1)
self.assertEqual(
len(namedtuple_exception.__notes__), len(normal_exception.__notes__)
)

expected_note = (
"Error calling __set_name__ on 'Annoying' instance "
"'attr' in 'NamedTupleClass'"
)
self.assertEqual(namedtuple_exception.__notes__[0], expected_note)
self.assertEqual(
namedtuple_exception.__notes__[0],
normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass")
)

def test_strange_errors_when_accessing_set_name_itself(self):
class CustomException(Exception): pass

class Meta(type):
def __getattribute__(self, attr):
if attr == "__set_name__":
raise CustomException
return object.__getattribute__(self, attr)

class VeryAnnoying(metaclass=Meta): pass

very_annoying = VeryAnnoying()

with self.assertRaises(CustomException):
class Foo(NamedTuple):
attr = very_annoying


class TypedDictTests(BaseTestCase):
def test_basics_functional_syntax(self):
Expand Down
21 changes: 18 additions & 3 deletions Lib/typing.py
Expand Up @@ -2743,11 +2743,26 @@ def __new__(cls, typename, bases, ns):
class_getitem = _generic_class_getitem
nm_tpl.__class_getitem__ = classmethod(class_getitem)
# update from user namespace without overriding special namedtuple attributes
for key in ns:
for key, val in ns.items():
if key in _prohibited:
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
elif key not in _special and key not in nm_tpl._fields:
setattr(nm_tpl, key, ns[key])
elif key not in _special:
if key not in nm_tpl._fields:
setattr(nm_tpl, key, val)
try:
set_name = type(val).__set_name__
except AttributeError:
pass
else:
try:
set_name(val, nm_tpl, key)
except BaseException as e:
e.add_note(
f"Error calling __set_name__ on {type(val).__name__!r} "
f"instance {key!r} in {typename!r}"
)
raise

if Generic in bases:
nm_tpl.__init_subclass__()
return nm_tpl
Expand Down
@@ -0,0 +1,4 @@
When creating a :class:`typing.NamedTuple` class, ensure
:func:`~object.__set_name__` is called on all objects that define
``__set_name__`` and exist in the values of the ``NamedTuple`` class's class
dictionary. Patch by Alex Waygood.

0 comments on commit 22e411e

Please sign in to comment.