◐ Shell
clean mode source ↗

bpo-36829: Add test.support.catch_unraisable_exception() by vstinner · Pull Request #13490 · python/cpython

Choose a reason for hiding this comment

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

this try/with/finally is somewhat tricky, how about:

try:
    with support.throw_unraisable_exceptions():
        ...
except Exception as e:
    ... # the exception is now here

eg:

@contextlib.contextmanager
def throw_unraisable_exceptions():
    unraisable = None
    old_hook = sys.unraisablehook

    def hook(exc):
        nonlocal unraisable
        unraisable = exc

    sys.unraisablehook = hook
    try:
        yield
        if unraisable is not None:
            raise unraisable
    finally:
        unraisable = None
        sys.unraisablehook = old_hook

Choose a reason for hiding this comment

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

or recommend usage like this:

    with support.catch_unraisable_exceptions() as cm:
        ...
        cm.unraisable  # only available here
    assert cm.unraisable is None  # now it's gone

by updating __exit__:

   def __exit__(self, *exc_info):
        self.unraisable = None  # clear unraisable here
        sys.unraisablehook = self._old_hook

Choose a reason for hiding this comment

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

Good idea :-) I implemented your __exit__ idea to avoid the need for try/finally.

throw_unraisable_exceptions() might be useful, but modified tests needs to access the 'obj' attribute of the unraisable hook. Later, they might also want to get access to the 'err_msg' attribute: #13488

Choose a reason for hiding this comment

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

There's still a sharp edge here:

how about making cm.unraisable throw if accessed outside the context instead of being None, eg:

def __exit__(self, *exc_info):
    del self.unraisable
    sys.unraisablehook = self._old_hook

Choose a reason for hiding this comment

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

and if you want to access additional unraisablehook info using throw_unraisable_exceptions:

class UnraisableException(Exception):
    def __init__(self, unraisable):
        self.unraisable = unraisable
        super().__init__(self, unraisable)


@contextlib.contextmanager
def throw_unraisable_exceptions():
    unraisable = None
    old_hook = sys.unraisablehook

    def hook(unraisable_):
        nonlocal unraisable
        unraisable = unraisable_

    sys.unraisablehook = hook
    try:
        yield
        if unraisable is not None:
            raise UnraisableException(unraisable) from unraisable.exc_value
    finally:
        sys.unraisablehook = old_hook

then you can use it with:

try:
    with throw_unraisable_exceptions():
        ...
except UnraisableException as e:
    print(repr(e.__cause__))
    err_msg = e.unraisable.err_msg

Choose a reason for hiding this comment

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

I wrote PR #13554 to implement your "del self.unraisable" idea.