PEP-654: Add example of subgroup callback with side effects (#1852)

This commit is contained in:
Irit Katriel 2021-03-01 23:24:59 +00:00 committed by GitHub
parent f69d426700
commit 274670b330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 86 additions and 12 deletions

View File

@ -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