Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 17 commits into from Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 66 additions & 0 deletions Lib/test/test_typing.py
Expand Up @@ -7519,6 +7519,55 @@ class GenericNamedTuple(NamedTuple, Generic[T]):

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

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

class Foo(NamedTuple):
attr = Vanilla()
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved

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

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

class Annoying:
def __set_name__(self, owner, name):
raise CustomException("Cannot do that!")

with self.assertRaisesRegex(CustomException, "Cannot do that!") as cm:
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
class NormalClass:
attr = Annoying()
normal_exception = cm.exception

with self.assertRaisesRegex(CustomException, "Cannot do that!") as cm:
class NamedTupleClass:
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
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")
)


class TypedDictTests(BaseTestCase):
def test_basics_functional_syntax(self):
Expand Down Expand Up @@ -8052,6 +8101,23 @@ class T4(TypedDict, Generic[S]): pass
self.assertEqual(klass.__optional_keys__, set())
self.assertIsInstance(klass(), dict)

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

class Annoying:
def __set_name__(self, owner, name):
raise CustomException("Cannot do that!")

with self.assertRaisesRegex(CustomException, "Cannot do that!") as cm:
class Foo(TypedDict):
attr = Annoying()
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved

expected_note = (
"Error calling __set_name__ on 'Annoying' instance "
"'attr' in 'Foo'"
)
self.assertIn(expected_note, cm.exception.__notes__)
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved


class RequiredTests(BaseTestCase):

Expand Down
13 changes: 11 additions & 2 deletions Lib/typing.py
Expand Up @@ -2726,11 +2726,20 @@ 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])
setattr(nm_tpl, key, val)
if hasattr(type(val), "__set_name__"):
try:
type(val).__set_name__(val, nm_tpl, key)
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
except Exception 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.