mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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>
This commit is contained in:
parent
ffe1b2d07b
commit
22e411e1d1
3 changed files with 99 additions and 3 deletions
|
@ -7535,6 +7535,83 @@ class NamedTupleTests(BaseTestCase):
|
||||||
|
|
||||||
self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,))
|
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):
|
class TypedDictTests(BaseTestCase):
|
||||||
def test_basics_functional_syntax(self):
|
def test_basics_functional_syntax(self):
|
||||||
|
|
|
@ -2743,11 +2743,26 @@ class NamedTupleMeta(type):
|
||||||
class_getitem = _generic_class_getitem
|
class_getitem = _generic_class_getitem
|
||||||
nm_tpl.__class_getitem__ = classmethod(class_getitem)
|
nm_tpl.__class_getitem__ = classmethod(class_getitem)
|
||||||
# update from user namespace without overriding special namedtuple attributes
|
# update from user namespace without overriding special namedtuple attributes
|
||||||
for key in ns:
|
for key, val in ns.items():
|
||||||
if key in _prohibited:
|
if key in _prohibited:
|
||||||
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
|
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
|
||||||
elif key not in _special and key not in nm_tpl._fields:
|
elif key not in _special:
|
||||||
setattr(nm_tpl, key, ns[key])
|
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:
|
if Generic in bases:
|
||||||
nm_tpl.__init_subclass__()
|
nm_tpl.__init_subclass__()
|
||||||
return nm_tpl
|
return nm_tpl
|
||||||
|
|
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue