Skip to content

bpo-1230540: Add threading.excepthook() #13515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,11 @@ always available.
before the program exits. The handling of such top-level exceptions can be
customized by assigning another three-argument function to ``sys.excepthook``.

See also :func:`unraisablehook` which handles unraisable exceptions.
.. seealso::

The :func:`sys.unraisablehook` function handles unraisable exceptions
and the :func:`threading.excepthook` function handles exception raised
by :func:`threading.Thread.run`.


.. data:: __breakpointhook__
Expand Down
30 changes: 30 additions & 0 deletions Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,32 @@ This module defines the following functions:
returned.


.. function:: excepthook(args, /)

Handle uncaught exception raised by :func:`Thread.run`.

The *args* argument has the following attributes:

* *exc_type*: Exception type.
* *exc_value*: Exception value, can be ``None``.
* *exc_traceback*: Exception traceback, can be ``None``.
* *thread*: Thread which raised the exception, can be ``None``.

If *exc_type* is :exc:`SystemExit`, the exception is silently ignored.
Otherwise, the exception is printed out on :data:`sys.stderr`.

If this function raises an exception, :func:`sys.excepthook` is called to
handle it.

:func:`threading.excepthook` can be overridden to control how uncaught
exceptions raised by :func:`Thread.run` are handled.

.. seealso::
:func:`sys.excepthook` handles uncaught exceptions.

.. versionadded:: 3.8


.. function:: get_ident()

Return the 'thread identifier' of the current thread. This is a nonzero
Expand Down Expand Up @@ -191,6 +217,10 @@ called is terminated.
A thread has a name. The name can be passed to the constructor, and read or
changed through the :attr:`~Thread.name` attribute.

If the :meth:`~Thread.run` method raises an exception,
:func:`threading.excepthook` is called to handle it. By default,
:func:`threading.excepthook` ignores silently :exc:`SystemExit`.

A thread can be flagged as a "daemon thread". The significance of this flag is
that the entire Python program exits when only daemon threads are left. The
initial value is inherited from the creating thread. The flag can be set
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,15 @@ in a standardized and extensible format, and offers several other benefits.
(Contributed by C.A.M. Gerlach in :issue:`36268`.)


threading
---------

Add a new :func:`threading.excepthook` function which handles uncaught
:meth:`threading.Thread.run` exception. It can be overridden to control how
uncaught :meth:`threading.Thread.run` exceptions are handled.
(Contributed by Victor Stinner in :issue:`1230540`.)


tokenize
--------

Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_pylifecycle.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ PyAPI_FUNC(int) _Py_HandleSystemExit(int *exitcode_p);
PyAPI_FUNC(PyObject*) _PyErr_WriteUnraisableDefaultHook(PyObject *unraisable);

PyAPI_FUNC(void) _PyErr_Print(PyThreadState *tstate);
PyAPI_FUNC(void) _PyErr_Display(PyObject *file, PyObject *exception,
PyObject *value, PyObject *tb);

#ifdef __cplusplus
}
Expand Down
92 changes: 92 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,98 @@ def run(self):
# explicitly break the reference cycle to not leak a dangling thread
thread.exc = None


class ThreadRunFail(threading.Thread):
def run(self):
raise ValueError("run failed")


class ExceptHookTests(BaseTestCase):
def test_excepthook(self):
with support.captured_output("stderr") as stderr:
thread = ThreadRunFail(name="excepthook thread")
thread.start()
thread.join()

stderr = stderr.getvalue().strip()
self.assertIn(f'Exception in thread {thread.name}:\n', stderr)
self.assertIn('Traceback (most recent call last):\n', stderr)
self.assertIn(' raise ValueError("run failed")', stderr)
self.assertIn('ValueError: run failed', stderr)

@support.cpython_only
def test_excepthook_thread_None(self):
# threading.excepthook called with thread=None: log the thread
# identifier in this case.
with support.captured_output("stderr") as stderr:
try:
raise ValueError("bug")
except Exception as exc:
args = threading.ExceptHookArgs([*sys.exc_info(), None])
threading.excepthook(args)

stderr = stderr.getvalue().strip()
self.assertIn(f'Exception in thread {threading.get_ident()}:\n', stderr)
self.assertIn('Traceback (most recent call last):\n', stderr)
self.assertIn(' raise ValueError("bug")', stderr)
self.assertIn('ValueError: bug', stderr)

def test_system_exit(self):
class ThreadExit(threading.Thread):
def run(self):
sys.exit(1)

# threading.excepthook() silently ignores SystemExit
with support.captured_output("stderr") as stderr:
thread = ThreadExit()
thread.start()
thread.join()

self.assertEqual(stderr.getvalue(), '')

def test_custom_excepthook(self):
args = None

def hook(hook_args):
nonlocal args
args = hook_args

try:
with support.swap_attr(threading, 'excepthook', hook):
thread = ThreadRunFail()
thread.start()
thread.join()

self.assertEqual(args.exc_type, ValueError)
self.assertEqual(str(args.exc_value), 'run failed')
self.assertEqual(args.exc_traceback, args.exc_value.__traceback__)
self.assertIs(args.thread, thread)
finally:
# Break reference cycle
args = None

def test_custom_excepthook_fail(self):
def threading_hook(args):
raise ValueError("threading_hook failed")

err_str = None

def sys_hook(exc_type, exc_value, exc_traceback):
nonlocal err_str
err_str = str(exc_value)

with support.swap_attr(threading, 'excepthook', threading_hook), \
support.swap_attr(sys, 'excepthook', sys_hook), \
support.captured_output('stderr') as stderr:
thread = ThreadRunFail()
thread.start()
thread.join()

self.assertEqual(stderr.getvalue(),
'Exception in threading.excepthook:\n')
self.assertEqual(err_str, 'threading_hook failed')


class TimerTests(BaseTestCase):

def setUp(self):
Expand Down
155 changes: 103 additions & 52 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import _thread

from time import monotonic as _time
from traceback import format_exc as _format_exc
from _weakrefset import WeakSet
from itertools import islice as _islice, count as _count
try:
Expand All @@ -27,7 +26,8 @@
'enumerate', 'main_thread', 'TIMEOUT_MAX',
'Event', 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
'Barrier', 'BrokenBarrierError', 'Timer', 'ThreadError',
'setprofile', 'settrace', 'local', 'stack_size']
'setprofile', 'settrace', 'local', 'stack_size',
'excepthook', 'ExceptHookArgs']

# Rename some stuff so "from threading import *" is safe
_start_new_thread = _thread.start_new_thread
Expand Down Expand Up @@ -752,14 +752,6 @@ class Thread:
"""

_initialized = False
# Need to store a reference to sys.exc_info for printing
# out exceptions when a thread tries to use a global var. during interp.
# shutdown and thus raises an exception about trying to perform some
# operation on/with a NoneType
_exc_info = _sys.exc_info
# Keep sys.exc_clear too to clear the exception just before
# allowing .join() to return.
#XXX __exc_clear = _sys.exc_clear

def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
Expand Down Expand Up @@ -802,9 +794,9 @@ class is implemented.
self._started = Event()
self._is_stopped = False
self._initialized = True
# sys.stderr is not stored in the class like
# sys.exc_info since it can be changed between instances
# Copy of sys.stderr used by self._invoke_excepthook()
self._stderr = _sys.stderr
self._invoke_excepthook = _make_invoke_excepthook()
# For debugging and _after_fork()
_dangling.add(self)

Expand Down Expand Up @@ -929,47 +921,8 @@ def _bootstrap_inner(self):

try:
self.run()
except SystemExit:
pass
except:
# If sys.stderr is no more (most likely from interpreter
# shutdown) use self._stderr. Otherwise still use sys (as in
# _sys) in case sys.stderr was redefined since the creation of
# self.
if _sys and _sys.stderr is not None:
print("Exception in thread %s:\n%s" %
(self.name, _format_exc()), file=_sys.stderr)
elif self._stderr is not None:
# Do the best job possible w/o a huge amt. of code to
# approximate a traceback (code ideas from
# Lib/traceback.py)
exc_type, exc_value, exc_tb = self._exc_info()
try:
print((
"Exception in thread " + self.name +
" (most likely raised during interpreter shutdown):"), file=self._stderr)
print((
"Traceback (most recent call last):"), file=self._stderr)
while exc_tb:
print((
' File "%s", line %s, in %s' %
(exc_tb.tb_frame.f_code.co_filename,
exc_tb.tb_lineno,
exc_tb.tb_frame.f_code.co_name)), file=self._stderr)
exc_tb = exc_tb.tb_next
print(("%s: %s" % (exc_type, exc_value)), file=self._stderr)
self._stderr.flush()
# Make sure that exc_tb gets deleted since it is a memory
# hog; deleting everything else is just for thoroughness
finally:
del exc_type, exc_value, exc_tb
finally:
# Prevent a race in
# test_threading.test_no_refcycle_through_target when
# the exception keeps the target alive past when we
# assert that it's dead.
#XXX self._exc_clear()
pass
self._invoke_excepthook(self)
finally:
with _active_limbo_lock:
try:
Expand Down Expand Up @@ -1163,6 +1116,104 @@ def getName(self):
def setName(self, name):
self.name = name


try:
from _thread import (_excepthook as excepthook,
_ExceptHookArgs as ExceptHookArgs)
except ImportError:
# Simple Python implementation if _thread._excepthook() is not available
from traceback import print_exception as _print_exception
from collections import namedtuple

_ExceptHookArgs = namedtuple(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is namedtuple for possible future extending a list of provided arguments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. See sys.unraisablehook: I used the same approach and today I added a new "err_msg" attribute ;-)

'ExceptHookArgs',
'exc_type exc_value exc_traceback thread')

def ExceptHookArgs(args):
return _ExceptHookArgs(*args)

def excepthook(args, /):
"""
Handle uncaught Thread.run() exception.
"""
if args.exc_type == SystemExit:
# silently ignore SystemExit
return

if _sys is not None and _sys.stderr is not None:
stderr = _sys.stderr
elif args.thread is not None:
stderr = args.thread._stderr
if stderr is None:
# do nothing if sys.stderr is None and sys.stderr was None
# when the thread was created
return
else:
# do nothing if sys.stderr is None and args.thread is None
return

if args.thread is not None:
name = args.thread.name
else:
name = get_ident()
print(f"Exception in thread {name}:",
file=stderr, flush=True)
_print_exception(args.exc_type, args.exc_value, args.exc_traceback,
file=stderr)
stderr.flush()


def _make_invoke_excepthook():
# Create a local namespace to ensure that variables remain alive
# when _invoke_excepthook() is called, even if it is called late during
# Python shutdown. It is mostly needed for daemon threads.

old_excepthook = excepthook
old_sys_excepthook = _sys.excepthook
if old_excepthook is None:
raise RuntimeError("threading.excepthook is None")
if old_sys_excepthook is None:
raise RuntimeError("sys.excepthook is None")

sys_exc_info = _sys.exc_info
local_print = print
local_sys = _sys

def invoke_excepthook(thread):
global excepthook
try:
hook = excepthook
if hook is None:
hook = old_excepthook

args = ExceptHookArgs([*sys_exc_info(), thread])

hook(args)
except Exception as exc:
exc.__suppress_context__ = True
del exc

if local_sys is not None and local_sys.stderr is not None:
stderr = local_sys.stderr
else:
stderr = thread._stderr

local_print("Exception in threading.excepthook:",
file=stderr, flush=True)

if local_sys is not None and local_sys.excepthook is not None:
sys_excepthook = local_sys.excepthook
else:
sys_excepthook = old_sys_excepthook

sys_excepthook(*sys_exc_info())
finally:
# Break reference cycle (exception stored in a variable)
args = None

return invoke_excepthook


# The timer class was contributed by Itamar Shtull-Trauring

class Timer(Thread):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add a new :func:`threading.excepthook` function which handles uncaught
:meth:`threading.Thread.run` exception. It can be overridden to control how
uncaught :meth:`threading.Thread.run` exceptions are handled.
Loading