PEP-654: Add example of subgroup callback with side effects (#1852)
This commit is contained in:
parent
f69d426700
commit
274670b330
98
pep-0654.rst
98
pep-0654.rst
|
@ -335,9 +335,9 @@ typically either query to check if it has leaf exceptions for which some
|
|||
condition holds (using ``subgroup`` or ``split``) or format the exception
|
||||
(using the ``traceback`` module's methods).
|
||||
|
||||
It is unlikely to be useful to inspect the individual leaf exceptions. To see
|
||||
why, suppose that an application caught an ``ExceptionGroup`` raised in an
|
||||
``asyncio.gather()`` call. At this stage, the context for each specific
|
||||
It is less likely to be useful to iterate over the individual leaf exceptions.
|
||||
To see why, suppose that an application caught an ``ExceptionGroup`` raised in
|
||||
an ``asyncio.gather()`` call. At this stage, the context for each specific
|
||||
exception is lost. Any recovery for this exception should have been performed
|
||||
before it was grouped with other exceptions into the ``ExceptionGroup`` [10]_.
|
||||
Furthermore, the application is likely to react in the same way to any number
|
||||
|
@ -345,27 +345,101 @@ of instances of a certain exception type, so it is more likely that we will
|
|||
want to know whether ``eg.subgroup(T)`` is None or not, than we are to be
|
||||
interested in the number of ``Ts`` in ``eg``.
|
||||
|
||||
If it does turn out to be necessary for an application to iterate over the
|
||||
individual exceptions of an ``ExceptionGroup`` ``eg``, this can be done by
|
||||
calling ``traverse(eg)``, where ``traverse`` is defined as follows:
|
||||
However, there are situations where it is necessary to inspect the
|
||||
individual leaf exceptions. For example, suppose that we have an
|
||||
``ExceptionGroup`` ``eg`` and we want to log the ``OSErrors`` that have a
|
||||
specific error code and reraise everything else. We can do this by passing
|
||||
a function with side effects to ``subgroup``, as follows:
|
||||
|
||||
.. code-block::
|
||||
|
||||
def traverse(exc, tbs=None):
|
||||
def log_and_ignore_ENOENT(err):
|
||||
if isinstance(err, OSError) and err.errno == ENOENT:
|
||||
log(err)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
try:
|
||||
. . .
|
||||
except ExceptionGroup as eg:
|
||||
eg = eg.subgroup(log_and_ignore_ENOENT)
|
||||
if eg is not None:
|
||||
raise eg
|
||||
|
||||
|
||||
In the previous example, when ``log_and_ignore_ENOENT`` is invoked on a leaf
|
||||
exception, only part of this exception's traceback is accessible -- the part
|
||||
referenced from its ``__traceback__`` field. If we need the full traceback,
|
||||
we need to look at the concatenation of the tracebacks of the exceptions on
|
||||
the path from the root to this leaf. We can get that with direct iteration,
|
||||
recursively, as follows:
|
||||
|
||||
.. code-block::
|
||||
|
||||
def leaf_generator(exc, tbs=None):
|
||||
if tbs is None:
|
||||
tbs = []
|
||||
|
||||
tbs.append(exc.__traceback__)
|
||||
if isinstance(exc, ExceptionGroup):
|
||||
for e in exc.errors:
|
||||
traverse(e, tbs)
|
||||
yield from leaf_generator(e, tbs)
|
||||
else:
|
||||
# exc is a leaf exception and its traceback
|
||||
# is the concatenation of the traceback in tbs
|
||||
process_leaf(exc, tbs)
|
||||
# is the concatenation of the traceback
|
||||
# segments in tbs
|
||||
yield exc, tbs
|
||||
tbs.pop()
|
||||
|
||||
|
||||
We can then process the full tracebacks of the leaf exceptions:
|
||||
|
||||
.. code-block::
|
||||
|
||||
>>> import traceback
|
||||
>>>
|
||||
>>> def g(v):
|
||||
... try:
|
||||
... raise ValueError(v)
|
||||
... except Exception as e:
|
||||
... return e
|
||||
...
|
||||
>>> def f():
|
||||
... raise ExceptionGroup("eg", [g(1), g(2)])
|
||||
...
|
||||
>>> try:
|
||||
... f()
|
||||
... except BaseException as e:
|
||||
... eg = e
|
||||
...
|
||||
>>> for (i, (exc, tbs)) in enumerate(leaf_generator(eg)):
|
||||
... print(f"\n>>> Exception #{i+1}:")
|
||||
... traceback.print_exception(exc)
|
||||
... print(f"The complete traceback for Exception #{i+1}:")
|
||||
... for tb in tbs:
|
||||
... traceback.print_tb(tb)
|
||||
...
|
||||
|
||||
>>> Exception #1:
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 3, in g
|
||||
ValueError: 1
|
||||
The complete traceback for Exception #1
|
||||
File "<stdin>", line 2, in <module>
|
||||
File "<stdin>", line 2, in f
|
||||
File "<stdin>", line 3, in g
|
||||
|
||||
>>> Exception #2:
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 3, in g
|
||||
ValueError: 2
|
||||
The complete traceback for Exception #2:
|
||||
File "<stdin>", line 2, in <module>
|
||||
File "<stdin>", line 2, in f
|
||||
File "<stdin>", line 3, in g
|
||||
>>>
|
||||
|
||||
except*
|
||||
-------
|
||||
|
||||
|
@ -416,7 +490,7 @@ Exceptions are matched using a subclass check. For example:
|
|||
|
||||
try:
|
||||
low_level_os_operation()
|
||||
except *OSerror as eg:
|
||||
except *OSError as eg:
|
||||
for e in eg.errors:
|
||||
print(type(e).__name__)
|
||||
|
||||
|
@ -864,7 +938,7 @@ while letting all other exceptions propagate.
|
|||
|
||||
try:
|
||||
low_level_os_operation()
|
||||
except *OSerror as errors:
|
||||
except *OSError as errors:
|
||||
raise errors.subgroup(lambda e: e.errno != errno.EPIPE) from None
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue