summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/test/test_typing.py77
-rw-r--r--Lib/typing.py21
-rw-r--r--Misc/NEWS.d/next/Library/2023-11-09-11-07-34.gh-issue-111874.dzYc3j.rst4
3 files changed, 99 insertions, 3 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 31d7fda..6698031 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -7535,6 +7535,83 @@ class NamedTupleTests(BaseTestCase):
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):
diff --git a/Lib/typing.py b/Lib/typing.py
index 872aca8..216f0c1 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2743,11 +2743,26 @@ class NamedTupleMeta(type):
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
diff --git a/Misc/NEWS.d/next/Library/2023-11-09-11-07-34.gh-issue-111874.dzYc3j.rst b/Misc/NEWS.d/next/Library/2023-11-09-11-07-34.gh-issue-111874.dzYc3j.rst
new file mode 100644
index 0000000..5040820
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-11-09-11-07-34.gh-issue-111874.dzYc3j.rst
@@ -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.