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 condition holds (using ``subgroup`` or ``split``) or format the exception
(using the ``traceback`` module's methods). (using the ``traceback`` module's methods).
It is unlikely to be useful to inspect the individual leaf exceptions. To see It is less likely to be useful to iterate over the individual leaf exceptions.
why, suppose that an application caught an ``ExceptionGroup`` raised in an To see why, suppose that an application caught an ``ExceptionGroup`` raised in
``asyncio.gather()`` call. At this stage, the context for each specific an ``asyncio.gather()`` call. At this stage, the context for each specific
exception is lost. Any recovery for this exception should have been performed exception is lost. Any recovery for this exception should have been performed
before it was grouped with other exceptions into the ``ExceptionGroup`` [10]_. 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 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 want to know whether ``eg.subgroup(T)`` is None or not, than we are to be
interested in the number of ``Ts`` in ``eg``. interested in the number of ``Ts`` in ``eg``.
If it does turn out to be necessary for an application to iterate over the However, there are situations where it is necessary to inspect the
individual exceptions of an ``ExceptionGroup`` ``eg``, this can be done by individual leaf exceptions. For example, suppose that we have an
calling ``traverse(eg)``, where ``traverse`` is defined as follows: ``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:: .. 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: if tbs is None:
tbs = [] tbs = []
tbs.append(exc.__traceback__) tbs.append(exc.__traceback__)
if isinstance(exc, ExceptionGroup): if isinstance(exc, ExceptionGroup):
for e in exc.errors: for e in exc.errors:
traverse(e, tbs) yield from leaf_generator(e, tbs)
else: else:
# exc is a leaf exception and its traceback # exc is a leaf exception and its traceback
# is the concatenation of the traceback in tbs # is the concatenation of the traceback
process_leaf(exc, tbs) # segments in tbs
yield exc, tbs
tbs.pop() 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* except*
------- -------
@ -416,7 +490,7 @@ Exceptions are matched using a subclass check. For example:
try: try:
low_level_os_operation() low_level_os_operation()
except *OSerror as eg: except *OSError as eg:
for e in eg.errors: for e in eg.errors:
print(type(e).__name__) print(type(e).__name__)
@ -864,7 +938,7 @@ while letting all other exceptions propagate.
try: try:
low_level_os_operation() low_level_os_operation()
except *OSerror as errors: except *OSError as errors:
raise errors.subgroup(lambda e: e.errno != errno.EPIPE) from None raise errors.subgroup(lambda e: e.errno != errno.EPIPE) from None