diff --git a/pep-0654.rst b/pep-0654.rst index aa91d70e4..cd6c4b3e6 100644 --- a/pep-0654.rst +++ b/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 "", line 3, in g + ValueError: 1 + The complete traceback for Exception #1 + File "", line 2, in + File "", line 2, in f + File "", line 3, in g + + >>> Exception #2: + Traceback (most recent call last): + File "", line 3, in g + ValueError: 2 + The complete traceback for Exception #2: + File "", line 2, in + File "", line 2, in f + File "", 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