bpo-41435: Add sys._current_exceptions() function (GH-21689)

This adds a new function named sys._current_exceptions() which is equivalent ot
sys._current_frames() except that it returns the exceptions currently handled
by other threads. It is equivalent to calling sys.exc_info() for each running
thread.
This commit is contained in:
Julien Danjou 2020-11-02 15:16:25 +01:00 committed by GitHub
parent 3d86d090dc
commit 64366fa9b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 1 deletions

View File

@ -196,6 +196,18 @@ always available.
.. audit-event:: sys._current_frames "" sys._current_frames
.. function:: _current_exceptions()
Return a dictionary mapping each thread's identifier to the topmost exception
currently active in that thread at the time the function is called.
If a thread is not currently handling an exception, it is not included in
the result dictionary.
This is most useful for statistical profiling.
This function should be used for internal and specialized purposes only.
.. audit-event:: sys._current_exceptions "" sys._current_exceptions
.. function:: breakpointhook()

View File

@ -167,6 +167,11 @@ PyAPI_FUNC(PyInterpreterState *) _PyGILState_GetInterpreterStateUnsafe(void);
*/
PyAPI_FUNC(PyObject *) _PyThread_CurrentFrames(void);
/* The implementation of sys._current_exceptions() Returns a dict mapping
thread id to that thread's current exception.
*/
PyAPI_FUNC(PyObject *) _PyThread_CurrentExceptions(void);
/* Routines for advanced debuggers, requested by David Beazley.
Don't use unless you know what you are doing! */
PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void);

View File

@ -432,6 +432,73 @@ class SysModuleTest(unittest.TestCase):
leave_g.set()
t.join()
@threading_helper.reap_threads
def test_current_exceptions(self):
import threading
import traceback
# Spawn a thread that blocks at a known place. Then the main
# thread does sys._current_frames(), and verifies that the frames
# returned make sense.
entered_g = threading.Event()
leave_g = threading.Event()
thread_info = [] # the thread's id
def f123():
g456()
def g456():
thread_info.append(threading.get_ident())
entered_g.set()
while True:
try:
raise ValueError("oops")
except ValueError:
if leave_g.wait(timeout=support.LONG_TIMEOUT):
break
t = threading.Thread(target=f123)
t.start()
entered_g.wait()
# At this point, t has finished its entered_g.set(), although it's
# impossible to guess whether it's still on that line or has moved on
# to its leave_g.wait().
self.assertEqual(len(thread_info), 1)
thread_id = thread_info[0]
d = sys._current_exceptions()
for tid in d:
self.assertIsInstance(tid, int)
self.assertGreater(tid, 0)
main_id = threading.get_ident()
self.assertIn(main_id, d)
self.assertIn(thread_id, d)
self.assertEqual((None, None, None), d.pop(main_id))
# Verify that the captured thread frame is blocked in g456, called
# from f123. This is a litte tricky, since various bits of
# threading.py are also in the thread's call stack.
exc_type, exc_value, exc_tb = d.pop(thread_id)
stack = traceback.extract_stack(exc_tb.tb_frame)
for i, (filename, lineno, funcname, sourceline) in enumerate(stack):
if funcname == "f123":
break
else:
self.fail("didn't find f123() on thread's call stack")
self.assertEqual(sourceline, "g456()")
# And the next record must be for g456().
filename, lineno, funcname, sourceline = stack[i+1]
self.assertEqual(funcname, "g456")
self.assertTrue(sourceline.startswith("if leave_g.wait("))
# Reap the spawned thread.
leave_g.set()
t.join()
def test_attributes(self):
self.assertIsInstance(sys.api_version, int)
self.assertIsInstance(sys.argv, list)

View File

@ -0,0 +1 @@
Add `sys._current_exceptions()` function to retrieve a dictionary mapping each thread's identifier to the topmost exception currently active in that thread at the time the function is called.

View File

@ -801,6 +801,26 @@ sys__current_frames(PyObject *module, PyObject *Py_UNUSED(ignored))
return sys__current_frames_impl(module);
}
PyDoc_STRVAR(sys__current_exceptions__doc__,
"_current_exceptions($module, /)\n"
"--\n"
"\n"
"Return a dict mapping each thread\'s identifier to its current raised exception.\n"
"\n"
"This function should be used for specialized purposes only.");
#define SYS__CURRENT_EXCEPTIONS_METHODDEF \
{"_current_exceptions", (PyCFunction)sys__current_exceptions, METH_NOARGS, sys__current_exceptions__doc__},
static PyObject *
sys__current_exceptions_impl(PyObject *module);
static PyObject *
sys__current_exceptions(PyObject *module, PyObject *Py_UNUSED(ignored))
{
return sys__current_exceptions_impl(module);
}
PyDoc_STRVAR(sys_call_tracing__doc__,
"call_tracing($module, func, args, /)\n"
"--\n"
@ -945,4 +965,4 @@ sys_getandroidapilevel(PyObject *module, PyObject *Py_UNUSED(ignored))
#ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
#define SYS_GETANDROIDAPILEVEL_METHODDEF
#endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
/*[clinic end generated code: output=87baa3357293ea65 input=a9049054013a1b77]*/
/*[clinic end generated code: output=bbc4963fe86a29d9 input=a9049054013a1b77]*/

View File

@ -1222,6 +1222,69 @@ done:
return result;
}
PyObject *
_PyThread_CurrentExceptions(void)
{
PyThreadState *tstate = _PyThreadState_GET();
_Py_EnsureTstateNotNULL(tstate);
if (_PySys_Audit(tstate, "sys._current_exceptions", NULL) < 0) {
return NULL;
}
PyObject *result = PyDict_New();
if (result == NULL) {
return NULL;
}
/* for i in all interpreters:
* for t in all of i's thread states:
* if t's frame isn't NULL, map t's id to its frame
* Because these lists can mutate even when the GIL is held, we
* need to grab head_mutex for the duration.
*/
_PyRuntimeState *runtime = tstate->interp->runtime;
HEAD_LOCK(runtime);
PyInterpreterState *i;
for (i = runtime->interpreters.head; i != NULL; i = i->next) {
PyThreadState *t;
for (t = i->tstate_head; t != NULL; t = t->next) {
_PyErr_StackItem *err_info = _PyErr_GetTopmostException(t);
if (err_info == NULL) {
continue;
}
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
if (id == NULL) {
goto fail;
}
PyObject *exc_info = PyTuple_Pack(
3,
err_info->exc_type != NULL ? err_info->exc_type : Py_None,
err_info->exc_value != NULL ? err_info->exc_value : Py_None,
err_info->exc_traceback != NULL ? err_info->exc_traceback : Py_None);
if (exc_info == NULL) {
Py_DECREF(id);
goto fail;
}
int stat = PyDict_SetItem(result, id, exc_info);
Py_DECREF(id);
Py_DECREF(exc_info);
if (stat < 0) {
goto fail;
}
}
}
goto done;
fail:
Py_CLEAR(result);
done:
HEAD_UNLOCK(runtime);
return result;
}
/* Python "auto thread state" API. */
/* Keep this as a static, as it is not reliable! It can only

View File

@ -1837,6 +1837,21 @@ sys__current_frames_impl(PyObject *module)
return _PyThread_CurrentFrames();
}
/*[clinic input]
sys._current_exceptions
Return a dict mapping each thread's identifier to its current raised exception.
This function should be used for specialized purposes only.
[clinic start generated code]*/
static PyObject *
sys__current_exceptions_impl(PyObject *module)
/*[clinic end generated code: output=2ccfd838c746f0ba input=0e91818fbf2edc1f]*/
{
return _PyThread_CurrentExceptions();
}
/*[clinic input]
sys.call_tracing
@ -1953,6 +1968,7 @@ static PyMethodDef sys_methods[] = {
METH_FASTCALL | METH_KEYWORDS, breakpointhook_doc},
SYS__CLEAR_TYPE_CACHE_METHODDEF
SYS__CURRENT_FRAMES_METHODDEF
SYS__CURRENT_EXCEPTIONS_METHODDEF
SYS_DISPLAYHOOK_METHODDEF
SYS_EXC_INFO_METHODDEF
SYS_EXCEPTHOOK_METHODDEF