summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Danjou <julien@danjou.info>2020-11-02 15:16:25 +0100
committerGitHub <noreply@github.com>2020-11-02 16:16:25 +0200
commit64366fa9b3ba71b8a503a8719eff433f4ea49eb9 (patch)
tree8aa6d76484553d2f29bfe5d03709a86c0abe547f
parentbpo-42230: Improve asyncio documentation regarding accepting sets vs iterable... (diff)
downloadcpython-64366fa9b3ba71b8a503a8719eff433f4ea49eb9.tar.gz
cpython-64366fa9b3ba71b8a503a8719eff433f4ea49eb9.tar.bz2
cpython-64366fa9b3ba71b8a503a8719eff433f4ea49eb9.zip
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.
-rw-r--r--Doc/library/sys.rst12
-rw-r--r--Include/cpython/pystate.h5
-rw-r--r--Lib/test/test_sys.py67
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst1
-rw-r--r--Python/clinic/sysmodule.c.h22
-rw-r--r--Python/pystate.c63
-rw-r--r--Python/sysmodule.c16
7 files changed, 185 insertions, 1 deletions
diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index 2f0840e2a74..f0acfcfe639 100644
--- a/Doc/library/sys.rst
+++ b/Doc/library/sys.rst
@@ -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()
diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h
index 5d5e4e33197..25522b4dbec 100644
--- a/Include/cpython/pystate.h
+++ b/Include/cpython/pystate.h
@@ -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);
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 30c29a26a99..332ed8f550c 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -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)
diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst b/Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst
new file mode 100644
index 00000000000..d2978f9b4ec
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2020-08-07-13-42-48.bpo-41435.qPWjJA.rst
@@ -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. \ No newline at end of file
diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h
index c1a9a2d69f0..addd58922e7 100644
--- a/Python/clinic/sysmodule.c.h
+++ b/Python/clinic/sysmodule.c.h
@@ -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]*/
diff --git a/Python/pystate.c b/Python/pystate.c
index e88898670cd..e37cbd5a657 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -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
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index 749b96455d6..945e639ca57 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -1838,6 +1838,21 @@ sys__current_frames_impl(PyObject *module)
}
/*[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
func: object
@@ -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