Skip to content

Commit 64366fa

Browse files
authored
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.
1 parent 3d86d09 commit 64366fa

File tree

7 files changed

+185
-1
lines changed

7 files changed

+185
-1
lines changed

Doc/library/sys.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ always available.
196196

197197
.. audit-event:: sys._current_frames "" sys._current_frames
198198

199+
.. function:: _current_exceptions()
200+
201+
Return a dictionary mapping each thread's identifier to the topmost exception
202+
currently active in that thread at the time the function is called.
203+
If a thread is not currently handling an exception, it is not included in
204+
the result dictionary.
205+
206+
This is most useful for statistical profiling.
207+
208+
This function should be used for internal and specialized purposes only.
209+
210+
.. audit-event:: sys._current_exceptions "" sys._current_exceptions
199211

200212
.. function:: breakpointhook()
201213

Include/cpython/pystate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ PyAPI_FUNC(PyInterpreterState *) _PyGILState_GetInterpreterStateUnsafe(void);
167167
*/
168168
PyAPI_FUNC(PyObject *) _PyThread_CurrentFrames(void);
169169

170+
/* The implementation of sys._current_exceptions() Returns a dict mapping
171+
thread id to that thread's current exception.
172+
*/
173+
PyAPI_FUNC(PyObject *) _PyThread_CurrentExceptions(void);
174+
170175
/* Routines for advanced debuggers, requested by David Beazley.
171176
Don't use unless you know what you are doing! */
172177
PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void);

Lib/test/test_sys.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,73 @@ def g456():
432432
leave_g.set()
433433
t.join()
434434

435+
@threading_helper.reap_threads
436+
def test_current_exceptions(self):
437+
import threading
438+
import traceback
439+
440+
# Spawn a thread that blocks at a known place. Then the main
441+
# thread does sys._current_frames(), and verifies that the frames
442+
# returned make sense.
443+
entered_g = threading.Event()
444+
leave_g = threading.Event()
445+
thread_info = [] # the thread's id
446+
447+
def f123():
448+
g456()
449+
450+
def g456():
451+
thread_info.append(threading.get_ident())
452+
entered_g.set()
453+
while True:
454+
try:
455+
raise ValueError("oops")
456+
except ValueError:
457+
if leave_g.wait(timeout=support.LONG_TIMEOUT):
458+
break
459+
460+
t = threading.Thread(target=f123)
461+
t.start()
462+
entered_g.wait()
463+
464+
# At this point, t has finished its entered_g.set(), although it's
465+
# impossible to guess whether it's still on that line or has moved on
466+
# to its leave_g.wait().
467+
self.assertEqual(len(thread_info), 1)
468+
thread_id = thread_info[0]
469+
470+
d = sys._current_exceptions()
471+
for tid in d:
472+
self.assertIsInstance(tid, int)
473+
self.assertGreater(tid, 0)
474+
475+
main_id = threading.get_ident()
476+
self.assertIn(main_id, d)
477+
self.assertIn(thread_id, d)
478+
self.assertEqual((None, None, None), d.pop(main_id))
479+
480+
# Verify that the captured thread frame is blocked in g456, called
481+
# from f123. This is a litte tricky, since various bits of
482+
# threading.py are also in the thread's call stack.
483+
exc_type, exc_value, exc_tb = d.pop(thread_id)
484+
stack = traceback.extract_stack(exc_tb.tb_frame)
485+
for i, (filename, lineno, funcname, sourceline) in enumerate(stack):
486+
if funcname == "f123":
487+
break
488+
else:
489+
self.fail("didn't find f123() on thread's call stack")
490+
491+
self.assertEqual(sourceline, "g456()")
492+
493+
# And the next record must be for g456().
494+
filename, lineno, funcname, sourceline = stack[i+1]
495+
self.assertEqual(funcname, "g456")
496+
self.assertTrue(sourceline.startswith("if leave_g.wait("))
497+
498+
# Reap the spawned thread.
499+
leave_g.set()
500+
t.join()
501+
435502
def test_attributes(self):
436503
self.assertIsInstance(sys.api_version, int)
437504
self.assertIsInstance(sys.argv, list)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

Python/clinic/sysmodule.c.h

Lines changed: 21 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/pystate.c

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,69 @@ _PyThread_CurrentFrames(void)
12221222
return result;
12231223
}
12241224

1225+
PyObject *
1226+
_PyThread_CurrentExceptions(void)
1227+
{
1228+
PyThreadState *tstate = _PyThreadState_GET();
1229+
1230+
_Py_EnsureTstateNotNULL(tstate);
1231+
1232+
if (_PySys_Audit(tstate, "sys._current_exceptions", NULL) < 0) {
1233+
return NULL;
1234+
}
1235+
1236+
PyObject *result = PyDict_New();
1237+
if (result == NULL) {
1238+
return NULL;
1239+
}
1240+
1241+
/* for i in all interpreters:
1242+
* for t in all of i's thread states:
1243+
* if t's frame isn't NULL, map t's id to its frame
1244+
* Because these lists can mutate even when the GIL is held, we
1245+
* need to grab head_mutex for the duration.
1246+
*/
1247+
_PyRuntimeState *runtime = tstate->interp->runtime;
1248+
HEAD_LOCK(runtime);
1249+
PyInterpreterState *i;
1250+
for (i = runtime->interpreters.head; i != NULL; i = i->next) {
1251+
PyThreadState *t;
1252+
for (t = i->tstate_head; t != NULL; t = t->next) {
1253+
_PyErr_StackItem *err_info = _PyErr_GetTopmostException(t);
1254+
if (err_info == NULL) {
1255+
continue;
1256+
}
1257+
PyObject *id = PyLong_FromUnsignedLong(t->thread_id);
1258+
if (id == NULL) {
1259+
goto fail;
1260+
}
1261+
PyObject *exc_info = PyTuple_Pack(
1262+
3,
1263+
err_info->exc_type != NULL ? err_info->exc_type : Py_None,
1264+
err_info->exc_value != NULL ? err_info->exc_value : Py_None,
1265+
err_info->exc_traceback != NULL ? err_info->exc_traceback : Py_None);
1266+
if (exc_info == NULL) {
1267+
Py_DECREF(id);
1268+
goto fail;
1269+
}
1270+
int stat = PyDict_SetItem(result, id, exc_info);
1271+
Py_DECREF(id);
1272+
Py_DECREF(exc_info);
1273+
if (stat < 0) {
1274+
goto fail;
1275+
}
1276+
}
1277+
}
1278+
goto done;
1279+
1280+
fail:
1281+
Py_CLEAR(result);
1282+
1283+
done:
1284+
HEAD_UNLOCK(runtime);
1285+
return result;
1286+
}
1287+
12251288
/* Python "auto thread state" API. */
12261289

12271290
/* Keep this as a static, as it is not reliable! It can only

Python/sysmodule.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,6 +1837,21 @@ sys__current_frames_impl(PyObject *module)
18371837
return _PyThread_CurrentFrames();
18381838
}
18391839

1840+
/*[clinic input]
1841+
sys._current_exceptions
1842+
1843+
Return a dict mapping each thread's identifier to its current raised exception.
1844+
1845+
This function should be used for specialized purposes only.
1846+
[clinic start generated code]*/
1847+
1848+
static PyObject *
1849+
sys__current_exceptions_impl(PyObject *module)
1850+
/*[clinic end generated code: output=2ccfd838c746f0ba input=0e91818fbf2edc1f]*/
1851+
{
1852+
return _PyThread_CurrentExceptions();
1853+
}
1854+
18401855
/*[clinic input]
18411856
sys.call_tracing
18421857
@@ -1953,6 +1968,7 @@ static PyMethodDef sys_methods[] = {
19531968
METH_FASTCALL | METH_KEYWORDS, breakpointhook_doc},
19541969
SYS__CLEAR_TYPE_CACHE_METHODDEF
19551970
SYS__CURRENT_FRAMES_METHODDEF
1971+
SYS__CURRENT_EXCEPTIONS_METHODDEF
19561972
SYS_DISPLAYHOOK_METHODDEF
19571973
SYS_EXC_INFO_METHODDEF
19581974
SYS_EXCEPTHOOK_METHODDEF

0 commit comments

Comments
 (0)