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:
parent
3d86d090dc
commit
64366fa9b3
|
@ -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()
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
|
@ -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]*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue