Skip to content

Commit d7e04e7

Browse files
[3.14] gh-146480: Override the exception in _PyErr_SetKeyError() (GH-146486) (#146511)
gh-146480: Override the exception in _PyErr_SetKeyError() (GH-146486) If _PyErr_SetKeyError() is called with an exception set, it now replaces the current exception with KeyError (as expected), instead of setting a SystemError or failing with a fatal error (in debug mode). (cherry picked from commit d4153a9) Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent cba5497 commit d7e04e7

File tree

3 files changed

+50
-4
lines changed

3 files changed

+50
-4
lines changed

Lib/test/test_capi/test_exceptions.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
from .test_misc import decode_stderr
1515

16-
# Skip this test if the _testcapi module isn't available.
16+
# Skip this test if the _testcapi or _testinternalcapi module isn't available.
1717
_testcapi = import_helper.import_module('_testcapi')
18+
_testinternalcapi = import_helper.import_module('_testinternalcapi')
1819

1920
NULL = None
2021

@@ -108,6 +109,26 @@ def __del__(self):
108109
b'<string>:7: RuntimeWarning: Testing PyErr_WarnEx',
109110
])
110111

112+
def test__pyerr_setkeyerror(self):
113+
# Test _PyErr_SetKeyError()
114+
_pyerr_setkeyerror = _testinternalcapi._pyerr_setkeyerror
115+
for arg in (
116+
"key",
117+
# check that a tuple argument is not unpacked
118+
(1, 2, 3),
119+
# PyErr_SetObject(exc_type, exc_value) uses exc_value if it's
120+
# already an exception, but _PyErr_SetKeyError() always creates a
121+
# new KeyError.
122+
KeyError('arg'),
123+
):
124+
with self.subTest(arg=arg):
125+
with self.assertRaises(KeyError) as cm:
126+
# Test calling _PyErr_SetKeyError() with an exception set
127+
# to check that the function overrides the current
128+
# exception.
129+
_pyerr_setkeyerror(arg)
130+
self.assertEqual(cm.exception.args, (arg,))
131+
111132

112133
class Test_FatalError(unittest.TestCase):
113134

Modules/_testinternalcapi.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,20 @@ test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
24672467
}
24682468

24692469

2470+
static PyObject *
2471+
_pyerr_setkeyerror(PyObject *self, PyObject *arg)
2472+
{
2473+
// Test that _PyErr_SetKeyError() overrides the current exception
2474+
// if an exception is set
2475+
PyErr_NoMemory();
2476+
2477+
_PyErr_SetKeyError(arg);
2478+
2479+
assert(PyErr_Occurred());
2480+
return NULL;
2481+
}
2482+
2483+
24702484
static PyMethodDef module_functions[] = {
24712485
{"get_configs", get_configs, METH_NOARGS},
24722486
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2578,6 +2592,7 @@ static PyMethodDef module_functions[] = {
25782592
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
25792593
{"test_threadstate_set_stack_protection",
25802594
test_threadstate_set_stack_protection, METH_NOARGS},
2595+
{"_pyerr_setkeyerror", _pyerr_setkeyerror, METH_O},
25812596
{NULL, NULL} /* sentinel */
25822597
};
25832598

Python/errors.c

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,23 @@ PyErr_SetObject(PyObject *exception, PyObject *value)
247247
_PyErr_SetObject(tstate, exception, value);
248248
}
249249

250-
/* Set a key error with the specified argument, wrapping it in a
251-
* tuple automatically so that tuple keys are not unpacked as the
252-
* exception arguments. */
250+
/* Set a key error with the specified argument. This function should be used to
251+
* raise a KeyError with an argument instead of PyErr_SetObject(PyExc_KeyError,
252+
* arg) which has a special behavior. PyErr_SetObject() unpacks arg if it's a
253+
* tuple, and it uses arg instead of creating a new exception if arg is an
254+
* exception.
255+
*
256+
* If an exception is already set, override the exception. */
253257
void
254258
_PyErr_SetKeyError(PyObject *arg)
255259
{
256260
PyThreadState *tstate = _PyThreadState_GET();
261+
262+
// PyObject_CallOneArg() must not be called with an exception set,
263+
// otherwise _Py_CheckFunctionResult() can fail if the function returned
264+
// a result with an excception set.
265+
_PyErr_Clear(tstate);
266+
257267
PyObject *exc = PyObject_CallOneArg(PyExc_KeyError, arg);
258268
if (!exc) {
259269
/* caller will expect error to be set anyway */

0 commit comments

Comments
 (0)