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

Commit 5d623bc

Browse files
author
Robert Schindler
committed
Implement API for handling ExceptionGroup
1 parent b3e0d6c commit 5d623bc

File tree

1 file changed

+129
-7
lines changed

1 file changed

+129
-7
lines changed

exceptiongroup/__init__.py

+129-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Top-level package for exceptiongroup."""
22

3+
import sys
4+
35
from ._version import __version__
46

57
__all__ = ["ExceptionGroup", "split", "catch"]
@@ -42,26 +44,146 @@ def __init__(self, message, exceptions, sources):
4244
)
4345
)
4446

47+
def __bool__(self):
48+
return bool(self.exceptions)
49+
50+
def __contains__(self, exception):
51+
return exception in self.exceptions
52+
4553
# copy.copy doesn't work for ExceptionGroup, because BaseException have
4654
# rewrite __reduce_ex__ method. We need to add __copy__ method to
4755
# make it can be copied.
4856
def __copy__(self):
4957
new_group = self.__class__(self.message, self.exceptions, self.sources)
50-
new_group.__traceback__ = self.__traceback__
51-
new_group.__context__ = self.__context__
52-
new_group.__cause__ = self.__cause__
53-
# Setting __cause__ also implicitly sets the __suppress_context__
54-
# attribute to True. So we should copy __suppress_context__ attribute
55-
# last, after copying __cause__.
56-
new_group.__suppress_context__ = self.__suppress_context__
58+
self._copy_magic_attrs(new_group)
5759
return new_group
5860

61+
def __iter__(self):
62+
return zip(self.exceptions, self.sources)
63+
64+
def __len__(self):
65+
return len(self.exceptions)
66+
5967
def __str__(self):
6068
return ", ".join(repr(exc) for exc in self.exceptions)
6169

6270
def __repr__(self):
6371
return "<ExceptionGroup: {}>".format(self)
6472

73+
def _copy_magic_attrs(self, dst):
74+
"""Copy exception-specific attributes to another :class:`ExceptionGroup`."""
75+
dst.__traceback__ = self.__traceback__
76+
dst.__context__ = self.__context__
77+
dst.__cause__ = self.__cause__
78+
# Setting __cause__ also implicitly sets the __suppress_context__
79+
# attribute to True. So we should copy __suppress_context__ attribute
80+
# last, after copying __cause__.
81+
dst.__suppress_context__ = self.__suppress_context__
82+
83+
def add(self, exception, source=""):
84+
"""Return a new group with exceptions of this group + another exception.
85+
86+
:param exception: exception to add
87+
:type exception: BaseException
88+
:param source: string describing where the exception originated from
89+
:type source: str
90+
:rtype: ExceptionGroup
91+
"""
92+
new = type(self)(
93+
self.message,
94+
self.exceptions + [exception],
95+
self.sources + [source],
96+
)
97+
self._copy_magic_attrs(new)
98+
return new
99+
100+
def find(self, predicate, with_source=False):
101+
"""Return the first exception that fulfills some predicate or ``None``.
102+
103+
:param predicate: see :meth:`findall`
104+
:type predicate: callable, type, (type)
105+
:param with_source: see :meth:`findall`
106+
:type with_source: bool
107+
:rtype: BaseException, None
108+
"""
109+
for item in self.findall(predicate, with_source):
110+
return item
111+
112+
def findall(self, predicate, with_source=False):
113+
"""Yield only exceptions that fulfill some predicate.
114+
115+
:param predicate:
116+
Callable that takes a :class:`BaseException` object and returns whether it
117+
fulfills some criteria (``True``) or not (``False``).
118+
If a type or tuple of types is given instead of a callable, :func:`isinstance`
119+
is automatically used as the predicate function.
120+
:type predicate: callable, type, (type)
121+
:param with_source:
122+
Normally, only the matching :class:`BaseException` objects are
123+
yielded. However, when this is set to ``True``, two-element tuples are
124+
yielded whose first element is the :class:`BaseException` and the second
125+
is the associated source (:class:`str`).
126+
:type with_source: bool
127+
"""
128+
if isinstance(predicate, (type, tuple)):
129+
exc_type = predicate
130+
predicate = lambda _exc: isinstance(_exc, exc_type)
131+
if with_source:
132+
for exception, source in zip(self.exceptions, self.sources):
133+
if predicate(exception):
134+
yield exception, source
135+
else:
136+
yield from filter(predicate, self.exceptions)
137+
138+
def maybe_reraise(self, from_exception=0, unwrap=True):
139+
"""(Re-)raise this exception group if it contains any exception.
140+
141+
If the group is empty, this returns without doing anything.
142+
143+
:param from_exception:
144+
This has the same meaning as the ``from`` keyword of the ``raise``
145+
statement. The default value of ``0`` causes the exception originally
146+
caught by the current ``except`` block to be used. This is retrieved using
147+
``sys.exc_info()[1]``.
148+
:type from_exception: BaseException, None
149+
:param unwrap:
150+
Normally, when there is just one exception left in the group, it is
151+
unwrapped and raised as is. With this option, you can prevent the
152+
unwrapping.
153+
:type unwrap: bool
154+
"""
155+
if not self.exceptions:
156+
return
157+
if unwrap and len(self.exceptions) == 1:
158+
exc = self.exceptions[0]
159+
else:
160+
exc = self
161+
if from_exception == 0:
162+
from_exception = sys.exc_info()[1]
163+
raise exc from from_exception
164+
165+
def remove(self, exception):
166+
"""Return a new group without a particular exception.
167+
168+
:param exception: exception to remove
169+
:type exception: BaseException
170+
:rtype: ExceptionGroup
171+
:raises ValueError: if exception not contained in this group
172+
"""
173+
try:
174+
idx = self.exceptions.index(exception)
175+
except ValueError:
176+
raise ValueError(
177+
f"{exception!r} not contained in {self!r}"
178+
) from None
179+
new = type(self)(
180+
self.message,
181+
self.exceptions[:idx] + self.exceptions[idx + 1 :],
182+
self.sources[:idx] + self.sources[idx + 1 :],
183+
)
184+
self._copy_magic_attrs(new)
185+
return new
186+
65187

66188
from . import _monkeypatch
67189
from ._tools import split, catch

0 commit comments

Comments
 (0)