Skip to content

Commit 55c99d9

Browse files
authored
pythongh-77757: replace exception wrapping by PEP-678 notes in typeobject's __set_name__ (python#103402)
1 parent e071f00 commit 55c99d9

File tree

8 files changed

+55
-41
lines changed

8 files changed

+55
-41
lines changed

Diff for: Doc/whatsnew/3.12.rst

+4
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ Other Language Changes
192192
* :class:`slice` objects are now hashable, allowing them to be used as dict keys and
193193
set items. (Contributed by Will Bradshaw and Furkan Onder in :gh:`101264`.)
194194

195+
* Exceptions raised in a typeobject's ``__set_name__`` method are no longer
196+
wrapped by a :exc:`RuntimeError`. Context information is added to the
197+
exception as a :pep:`678` note. (Contributed by Irit Katriel in :gh:`77757`.)
198+
195199
New Modules
196200
===========
197201

Diff for: Include/internal/pycore_pyerrors.h

+2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ extern PyObject* _Py_Offer_Suggestions(PyObject* exception);
109109
PyAPI_FUNC(Py_ssize_t) _Py_UTF8_Edit_Cost(PyObject *str_a, PyObject *str_b,
110110
Py_ssize_t max_cost);
111111

112+
void _PyErr_FormatNote(const char *format, ...);
113+
112114
#ifdef __cplusplus
113115
}
114116
#endif

Diff for: Lib/test/test_functools.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2980,7 +2980,7 @@ class MyClass(metaclass=MyMeta):
29802980

29812981
def test_reuse_different_names(self):
29822982
"""Disallow this case because decorated function a would not be cached."""
2983-
with self.assertRaises(RuntimeError) as ctx:
2983+
with self.assertRaises(TypeError) as ctx:
29842984
class ReusedCachedProperty:
29852985
@py_functools.cached_property
29862986
def a(self):
@@ -2989,7 +2989,7 @@ def a(self):
29892989
b = a
29902990

29912991
self.assertEqual(
2992-
str(ctx.exception.__context__),
2992+
str(ctx.exception),
29932993
str(TypeError("Cannot assign the same cached_property to two different names ('a' and 'b')."))
29942994
)
29952995

Diff for: Lib/test/test_subclassinit.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -134,30 +134,28 @@ class Descriptor:
134134
def __set_name__(self, owner, name):
135135
1/0
136136

137-
with self.assertRaises(RuntimeError) as cm:
137+
with self.assertRaises(ZeroDivisionError) as cm:
138138
class NotGoingToWork:
139139
attr = Descriptor()
140140

141-
exc = cm.exception
142-
self.assertRegex(str(exc), r'\bNotGoingToWork\b')
143-
self.assertRegex(str(exc), r'\battr\b')
144-
self.assertRegex(str(exc), r'\bDescriptor\b')
145-
self.assertIsInstance(exc.__cause__, ZeroDivisionError)
141+
notes = cm.exception.__notes__
142+
self.assertRegex(str(notes), r'\bNotGoingToWork\b')
143+
self.assertRegex(str(notes), r'\battr\b')
144+
self.assertRegex(str(notes), r'\bDescriptor\b')
146145

147146
def test_set_name_wrong(self):
148147
class Descriptor:
149148
def __set_name__(self):
150149
pass
151150

152-
with self.assertRaises(RuntimeError) as cm:
151+
with self.assertRaises(TypeError) as cm:
153152
class NotGoingToWork:
154153
attr = Descriptor()
155154

156-
exc = cm.exception
157-
self.assertRegex(str(exc), r'\bNotGoingToWork\b')
158-
self.assertRegex(str(exc), r'\battr\b')
159-
self.assertRegex(str(exc), r'\bDescriptor\b')
160-
self.assertIsInstance(exc.__cause__, TypeError)
155+
notes = cm.exception.__notes__
156+
self.assertRegex(str(notes), r'\bNotGoingToWork\b')
157+
self.assertRegex(str(notes), r'\battr\b')
158+
self.assertRegex(str(notes), r'\bDescriptor\b')
161159

162160
def test_set_name_lookup(self):
163161
resolved = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Exceptions raised in a typeobject's ``__set_name__`` method are no longer
2+
wrapped by a :exc:`RuntimeError`. Context information is added to the
3+
exception as a :pep:`678` note.

Diff for: Objects/typeobject.c

+4-2
Original file line numberDiff line numberDiff line change
@@ -9137,13 +9137,15 @@ type_new_set_names(PyTypeObject *type)
91379137
Py_DECREF(set_name);
91389138

91399139
if (res == NULL) {
9140-
_PyErr_FormatFromCause(PyExc_RuntimeError,
9140+
_PyErr_FormatNote(
91419141
"Error calling __set_name__ on '%.100s' instance %R "
91429142
"in '%.100s'",
91439143
Py_TYPE(value)->tp_name, key, type->tp_name);
91449144
goto error;
91459145
}
9146-
Py_DECREF(res);
9146+
else {
9147+
Py_DECREF(res);
9148+
}
91479149
}
91489150

91499151
Py_DECREF(names_to_set);

Diff for: Python/codecs.c

+3-25
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Copyright (c) Corporation for National Research Initiatives.
1111
#include "Python.h"
1212
#include "pycore_call.h" // _PyObject_CallNoArgs()
1313
#include "pycore_interp.h" // PyInterpreterState.codec_search_path
14+
#include "pycore_pyerrors.h" // _PyErr_FormatNote()
1415
#include "pycore_pystate.h" // _PyInterpreterState_GET()
1516
#include "pycore_ucnhash.h" // _PyUnicode_Name_CAPI
1617
#include <ctype.h>
@@ -382,29 +383,6 @@ PyObject *PyCodec_StreamWriter(const char *encoding,
382383
return codec_getstreamcodec(encoding, stream, errors, 3);
383384
}
384385

385-
static void
386-
add_note_to_codec_error(const char *operation,
387-
const char *encoding)
388-
{
389-
PyObject *exc = PyErr_GetRaisedException();
390-
if (exc == NULL) {
391-
return;
392-
}
393-
PyObject *note = PyUnicode_FromFormat("%s with '%s' codec failed",
394-
operation, encoding);
395-
if (note == NULL) {
396-
_PyErr_ChainExceptions1(exc);
397-
return;
398-
}
399-
int res = _PyException_AddNote(exc, note);
400-
Py_DECREF(note);
401-
if (res < 0) {
402-
_PyErr_ChainExceptions1(exc);
403-
return;
404-
}
405-
PyErr_SetRaisedException(exc);
406-
}
407-
408386
/* Encode an object (e.g. a Unicode object) using the given encoding
409387
and return the resulting encoded object (usually a Python string).
410388
@@ -425,7 +403,7 @@ _PyCodec_EncodeInternal(PyObject *object,
425403

426404
result = PyObject_Call(encoder, args, NULL);
427405
if (result == NULL) {
428-
add_note_to_codec_error("encoding", encoding);
406+
_PyErr_FormatNote("%s with '%s' codec failed", "encoding", encoding);
429407
goto onError;
430408
}
431409

@@ -470,7 +448,7 @@ _PyCodec_DecodeInternal(PyObject *object,
470448

471449
result = PyObject_Call(decoder, args, NULL);
472450
if (result == NULL) {
473-
add_note_to_codec_error("decoding", encoding);
451+
_PyErr_FormatNote("%s with '%s' codec failed", "decoding", encoding);
474452
goto onError;
475453
}
476454
if (!PyTuple_Check(result) ||

Diff for: Python/errors.c

+27
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,33 @@ PyErr_Format(PyObject *exception, const char *format, ...)
12001200
}
12011201

12021202

1203+
/* Adds a note to the current exception (if any) */
1204+
void
1205+
_PyErr_FormatNote(const char *format, ...)
1206+
{
1207+
PyObject *exc = PyErr_GetRaisedException();
1208+
if (exc == NULL) {
1209+
return;
1210+
}
1211+
va_list vargs;
1212+
va_start(vargs, format);
1213+
PyObject *note = PyUnicode_FromFormatV(format, vargs);
1214+
va_end(vargs);
1215+
if (note == NULL) {
1216+
goto error;
1217+
}
1218+
int res = _PyException_AddNote(exc, note);
1219+
Py_DECREF(note);
1220+
if (res < 0) {
1221+
goto error;
1222+
}
1223+
PyErr_SetRaisedException(exc);
1224+
return;
1225+
error:
1226+
_PyErr_ChainExceptions1(exc);
1227+
}
1228+
1229+
12031230
PyObject *
12041231
PyErr_NewException(const char *name, PyObject *base, PyObject *dict)
12051232
{

0 commit comments

Comments
 (0)