Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP-654: add example of subgroup callback with side effects #1852

Merged
merged 5 commits into from
Mar 1, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 86 additions & 12 deletions pep-0654.rst
Original file line number Diff line number Diff line change
Expand Up @@ -335,37 +335,111 @@ 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
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could show a little code fragment that uses this as well? It would have to include at least

try:
    . . .
except *OSError as eg:
    for exc, tbs in leaf_generator(eg):
        if exc.errno != ENOENT:
            print(exc)
            <show how to print the traceback using the traceback module>

I think the last line would have to call extract_tb() for each item of tbs, concatenate the lists (in which order?), and then call print_list() on the concatenation. I'm too lazy to research how exactly.

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}:")
Copy link
Member Author

@iritkatriel iritkatriel Mar 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

">>>" was a poor choice here, it gets mixed up with the interactive prompt. I'll replace it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I already merged. Awaiting your next PR...

... 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*
-------

Expand Down Expand Up @@ -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__)

Expand Down Expand Up @@ -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


Expand Down