Skip to content
This repository was archived by the owner on Nov 2, 2022. It is now read-only.

Commit 91a286f

Browse files
committed
First draft
This is totally incomplete E.g. the tests are ripped out of Trio's current MultiError tests and probably just wrong because of that But I want to get something up...
1 parent f14bdf7 commit 91a286f

File tree

10 files changed

+399
-0
lines changed

10 files changed

+399
-0
lines changed

exceptiongroup/__init__.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,46 @@
11
"""Top-level package for exceptiongroup."""
22

33
from ._version import __version__
4+
5+
__all__ = ["ExceptionGroup", "split", "catch"]
6+
7+
8+
class ExceptionGroup(BaseException):
9+
"""An exception that contains other exceptions.
10+
11+
Its main use is to represent the situation when multiple child tasks all
12+
raise errors "in parallel".
13+
14+
Args:
15+
message (str): A description of the overall exception.
16+
exceptions (list): The exceptions.
17+
sources (list): For each exception, a string describing where it came
18+
from.
19+
20+
Raises:
21+
TypeError: if any of the passed in objects are not instances of
22+
:exc:`BaseException`.
23+
ValueError: if the exceptions and sources lists don't have the same
24+
length.
25+
26+
"""
27+
28+
def __init__(self, message, exceptions, sources):
29+
super().__init__(message)
30+
self.exceptions = list(exceptions)
31+
for exc in self.exceptions:
32+
if not isinstance(exc, BaseException):
33+
raise TypeError(
34+
"Expected an exception object, not {!r}".format(exc)
35+
)
36+
self.sources = list(sources)
37+
if len(self.sources) != len(self.exceptions):
38+
raise ValueError(
39+
"different number of sources ({}) and exceptions ({})".format(
40+
len(self.sources), len(self.exceptions)
41+
)
42+
)
43+
44+
45+
from . import _monkeypatch
46+
from ._tools import split, catch

exceptiongroup/_monkeypatch.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
################################################################
2+
# ExceptionGroup traceback formatting
3+
#
4+
# This file contains the terrible, terrible monkey patching of various things,
5+
# especially the traceback module, to add support for handling
6+
# ExceptionGroups.
7+
################################################################
8+
9+
import sys
10+
import textwrap
11+
import traceback
12+
import warnings
13+
14+
from . import ExceptionGroup
15+
16+
traceback_exception_original_init = traceback.TracebackException.__init__
17+
18+
19+
def traceback_exception_init(
20+
self,
21+
exc_type,
22+
exc_value,
23+
exc_traceback,
24+
*,
25+
limit=None,
26+
lookup_lines=True,
27+
capture_locals=False,
28+
_seen=None
29+
):
30+
if _seen is None:
31+
_seen = set()
32+
33+
# Capture the original exception and its cause and context as
34+
# TracebackExceptions
35+
traceback_exception_original_init(
36+
self,
37+
exc_type,
38+
exc_value,
39+
exc_traceback,
40+
limit=limit,
41+
lookup_lines=lookup_lines,
42+
capture_locals=capture_locals,
43+
_seen=_seen,
44+
)
45+
46+
# Capture each of the exceptions in the ExceptionGroup along with each of
47+
# their causes and contexts
48+
if isinstance(exc_value, ExceptionGroup):
49+
exceptions = []
50+
sources = []
51+
for exc, source in zip(exc_value.exceptions, exc_value.sources):
52+
if exc not in _seen:
53+
exceptions.append(
54+
traceback.TracebackException.from_exception(
55+
exc,
56+
limit=limit,
57+
lookup_lines=lookup_lines,
58+
capture_locals=capture_locals,
59+
# copy the set of _seen exceptions so that duplicates
60+
# shared between sub-exceptions are not omitted
61+
_seen=set(_seen),
62+
)
63+
)
64+
sources.append(source)
65+
self.exceptions = exceptions
66+
self.sources = sources
67+
else:
68+
self.exceptions = []
69+
self.sources = []
70+
71+
72+
def traceback_exception_format(self, *, chain=True):
73+
yield from traceback_exception_original_format(self, chain=chain)
74+
75+
for exc, source in zip(self.exceptions, self.sources):
76+
yield "\n {}:\n\n".format(source)
77+
yield from (
78+
textwrap.indent(line, " " * 4) for line in exc.format(chain=chain)
79+
)
80+
81+
82+
def exceptiongroup_excepthook(etype, value, tb):
83+
sys.stderr.write("".join(traceback.format_exception(etype, value, tb)))
84+
85+
86+
traceback.TracebackException.__init__ = traceback_exception_init
87+
traceback_exception_original_format = traceback.TracebackException.format
88+
traceback.TracebackException.format = traceback_exception_format
89+
90+
IPython_handler_installed = False
91+
warning_given = False
92+
if "IPython" in sys.modules:
93+
import IPython
94+
95+
ip = IPython.get_ipython()
96+
if ip is not None:
97+
if ip.custom_exceptions != ():
98+
warnings.warn(
99+
"IPython detected, but you already have a custom exception "
100+
"handler installed. I'll skip installing exceptiongroup's "
101+
"custom handler, but this means you won't see full tracebacks "
102+
"for ExceptionGroups.",
103+
category=RuntimeWarning,
104+
)
105+
warning_given = True
106+
else:
107+
108+
def trio_show_traceback(self, etype, value, tb, tb_offset=None):
109+
# XX it would be better to integrate with IPython's fancy
110+
# exception formatting stuff (and not ignore tb_offset)
111+
exceptiongroup_excepthook(etype, value, tb)
112+
113+
ip.set_custom_exc((ExceptionGroup,), trio_show_traceback)
114+
IPython_handler_installed = True
115+
116+
if sys.excepthook is sys.__excepthook__:
117+
sys.excepthook = exceptiongroup_excepthook
118+
else:
119+
if not IPython_handler_installed and not warning_given:
120+
warnings.warn(
121+
"You seem to already have a custom sys.excepthook handler "
122+
"installed. I'll skip installing exceptiongroup's custom handler, "
123+
"but this means you won't see full tracebacks for "
124+
"ExceptionGroups.",
125+
category=RuntimeWarning,
126+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import traceback
2+
from exceptiongroup import ExceptionGroup, split, catch
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This isn't really a package, everything in here is a standalone script. This
2+
# __init__.py is just to fool setup.py into actually installing the things.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# https://coverage.readthedocs.io/en/latest/subprocess.html
2+
try:
3+
import coverage
4+
except ImportError: # pragma: no cover
5+
pass
6+
else:
7+
coverage.process_startup()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import _common
2+
3+
import sys
4+
5+
6+
def custom_excepthook(*args):
7+
print("custom running!")
8+
return sys.__excepthook__(*args)
9+
10+
11+
sys.excepthook = custom_excepthook
12+
13+
# Should warn that we'll get kinda-broken tracebacks
14+
import exceptiongroup
15+
16+
# The custom excepthook should run, because we were polite and didn't
17+
# override it
18+
raise exceptiongroup.ExceptionGroup(
19+
"demo", [ValueError(), KeyError()], ["a", "b"]
20+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import _common
2+
3+
# Override the regular excepthook too -- it doesn't change anything either way
4+
# because ipython doesn't use it, but we want to make sure exceptiongroup
5+
# doesn't warn about it.
6+
import sys
7+
8+
9+
def custom_excepthook(*args):
10+
print("custom running!")
11+
return sys.__excepthook__(*args)
12+
13+
14+
sys.excepthook = custom_excepthook
15+
16+
import IPython
17+
ip = IPython.get_ipython()
18+
19+
20+
# Set this to some random nonsense
21+
class SomeError(Exception):
22+
pass
23+
24+
25+
def custom_exc_hook(etype, value, tb, tb_offset=None):
26+
ip.showtraceback()
27+
28+
29+
ip.set_custom_exc((SomeError,), custom_exc_hook)
30+
31+
import exceptiongroup
32+
33+
# The custom excepthook should run, because we were polite and didn't
34+
# override it
35+
raise exceptiongroup.ExceptionGroup(
36+
"demo", [ValueError(), KeyError()], ["a", "b"]
37+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import _common
2+
3+
import exceptiongroup
4+
5+
6+
def exc1_fn():
7+
try:
8+
raise ValueError
9+
except Exception as exc:
10+
return exc
11+
12+
13+
def exc2_fn():
14+
try:
15+
raise KeyError
16+
except Exception as exc:
17+
return exc
18+
19+
20+
# This should be printed nicely, because we overrode sys.excepthook
21+
raise exceptiongroup.ExceptionGroup(
22+
"demo", [exc1_fn(), exc2_fn()], ["a", "b"],
23+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import _common
2+
3+
# To tickle the "is IPython loaded?" logic, make sure that our package
4+
# tolerates IPython loaded but not actually in use
5+
import IPython
6+
7+
import simple_excepthook

0 commit comments

Comments
 (0)