bpo-45292: [PEP-654] exception groups and except* documentation (GH-30158)

This commit is contained in:
Irit Katriel 2022-01-06 19:05:34 +00:00 committed by GitHub
parent 68c76d9766
commit 9925e70e48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 208 additions and 1 deletions

View file

@ -851,6 +851,78 @@ The following exceptions are used as warning categories; see the
.. versionadded:: 3.2
Exception groups
----------------
The following are used when it is necessary to raise multiple unrelated
exceptions. They are part of the exception hierarchy so they can be
handled with :keyword:`except` like all other exceptions. In addition,
they are recognised by :keyword:`except*<except_star>`, which matches
their subgroups based on the types of the contained exceptions.
.. exception:: ExceptionGroup(msg, excs)
.. exception:: BaseExceptionGroup(msg, excs)
Both of these exception types wrap the exceptions in the sequence ``excs``.
The ``msg`` parameter must be a string. The difference between the two
classes is that :exc:`BaseExceptionGroup` extends :exc:`BaseException` and
it can wrap any exception, while :exc:`ExceptionGroup` extends :exc:`Exception`
and it can only wrap subclasses of :exc:`Exception`. This design is so that
``except Exception`` catches an :exc:`ExceptionGroup` but not
:exc:`BaseExceptionGroup`.
The :exc:`BaseExceptionGroup` constructor returns an :exc:`ExceptionGroup`
rather than a :exc:`BaseExceptionGroup` if all contained exceptions are
:exc:`Exception` instances, so it can be used to make the selection
automatic. The :exc:`ExceptionGroup` constructor, on the other hand,
raises a :exc:`TypeError` if any contained exception is not an
:exc:`Exception` subclass.
.. method:: subgroup(condition)
Returns an exception group that contains only the exceptions from the
current group that match *condition*, or ``None`` if the result is empty.
The condition can be either a function that accepts an exception and returns
true for those that should be in the subgroup, or it can be an exception type
or a tuple of exception types, which is used to check for a match using the
same check that is used in an ``except`` clause.
The nesting structure of the current exception is preserved in the result,
as are the values of its :attr:`message`, :attr:`__traceback__`,
:attr:`__cause__`, :attr:`__context__` and :attr:`__note__` fields.
Empty nested groups are omitted from the result.
The condition is checked for all exceptions in the nested exception group,
including the top-level and any nested exception groups. If the condition is
true for such an exception group, it is included in the result in full.
.. method:: split(condition)
Like :meth:`subgroup`, but returns the pair ``(match, rest)`` where ``match``
is ``subgroup(condition)`` and ``rest`` is the remaining non-matching
part.
.. method:: derive(excs)
Returns an exception group with the same :attr:`message`,
:attr:`__traceback__`, :attr:`__cause__`, :attr:`__context__`
and :attr:`__note__` but which wraps the exceptions in ``excs``.
This method is used by :meth:`subgroup` and :meth:`split`. A
subclass needs to override it in order to make :meth:`subgroup`
and :meth:`split` return instances of the subclass rather
than :exc:`ExceptionGroup`. ::
>>> class MyGroup(ExceptionGroup):
... def derive(self, exc):
... return MyGroup(self.message, exc)
...
>>> MyGroup("eg", [ValueError(1), TypeError(2)]).split(TypeError)
(MyGroup('eg', [TypeError(2)]), MyGroup('eg', [ValueError(1)]))
.. versionadded:: 3.11
Exception hierarchy
-------------------

View file

@ -199,6 +199,7 @@ returns the list ``[0, 1, 2]``.
.. _try:
.. _except:
.. _except_star:
.. _finally:
The :keyword:`!try` statement
@ -216,12 +217,16 @@ The :keyword:`try` statement specifies exception handlers and/or cleanup code
for a group of statements:
.. productionlist:: python-grammar
try_stmt: `try1_stmt` | `try2_stmt`
try_stmt: `try1_stmt` | `try2_stmt` | `try3_stmt`
try1_stmt: "try" ":" `suite`
: ("except" [`expression` ["as" `identifier`]] ":" `suite`)+
: ["else" ":" `suite`]
: ["finally" ":" `suite`]
try2_stmt: "try" ":" `suite`
: ("except" "*" `expression` ["as" `identifier`] ":" `suite`)+
: ["else" ":" `suite`]
: ["finally" ":" `suite`]
try3_stmt: "try" ":" `suite`
: "finally" ":" `suite`
@ -304,6 +309,47 @@ when leaving an exception handler::
>>> print(sys.exc_info())
(None, None, None)
.. index::
keyword: except_star
The :keyword:`except*<except_star>` clause(s) are used for handling
:exc:`ExceptionGroup`s. The exception type for matching is interpreted as in
the case of :keyword:`except`, but in the case of exception groups we can have
partial matches when the type matches some of the exceptions in the group.
This means that multiple except* clauses can execute, each handling part of
the exception group. Each clause executes once and handles an exception group
of all matching exceptions. Each exception in the group is handled by at most
one except* clause, the first that matches it. ::
>>> try:
... raise ExceptionGroup("eg",
... [ValueError(1), TypeError(2), OSError(3), OSError(4)])
... except* TypeError as e:
... print(f'caught {type(e)} with nested {e.exceptions}')
... except* OSError as e:
... print(f'caught {type(e)} with nested {e.exceptions}')
...
caught <class 'ExceptionGroup'> with nested (TypeError(2),)
caught <class 'ExceptionGroup'> with nested (OSError(3), OSError(4))
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg
+-+---------------- 1 ----------------
| ValueError: 1
+------------------------------------
>>>
Any remaining exceptions that were not handled by any except* clause
are re-raised at the end, combined into an exception group along with
all exceptions that were raised from within except* clauses.
An except* clause must have a matching type, and this type cannot be a
subclass of :exc:`BaseExceptionGroup`. It is not possible to mix except
and except* in the same :keyword:`try`. :keyword:`break`,
:keyword:`continue` and :keyword:`return` cannot appear in an except*
clause.
.. index::
keyword: else
statement: return

View file

@ -462,3 +462,92 @@ used in a way that ensures they are always cleaned up promptly and correctly. ::
After the statement is executed, the file *f* is always closed, even if a
problem was encountered while processing the lines. Objects which, like files,
provide predefined clean-up actions will indicate this in their documentation.
.. _tut-exception-groups:
Raising and Handling Multiple Unrelated Exceptions
==================================================
There are situations where it is necessary to report several exceptions that
have occurred. This it often the case in concurrency frameworks, when several
tasks may have failed in parallel, but there are also other use cases where
it is desirable to continue execution and collect multiple errors rather than
raise the first exception.
The builtin :exc:`ExceptionGroup` wraps a list of exception instances so
that they can be raised together. It is an exception itself, so it can be
caught like any other exception. ::
>>> def f():
... excs = [OSError('error 1'), SystemError('error 2')]
... raise ExceptionGroup('there were problems', excs)
...
>>> f()
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| File "<stdin>", line 3, in f
| ExceptionGroup: there were problems
+-+---------------- 1 ----------------
| OSError: error 1
+---------------- 2 ----------------
| SystemError: error 2
+------------------------------------
>>> try:
... f()
... except Exception as e:
... print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>
By using ``except*`` instead of ``except``, we can selectively
handle only the exceptions in the group that match a certain
type. In the following example, which shows a nested exception
group, each ``except*`` clause extracts from the group exceptions
of a certain type while letting all other exceptions propagate to
other clauses and eventually to be reraised. ::
>>> def f():
... raise ExceptionGroup("group1",
... [OSError(1),
... SystemError(2),
... ExceptionGroup("group2",
... [OSError(3), RecursionError(4)])])
...
>>> try:
... f()
... except* OSError as e:
... print("There were OSErrors")
... except* SystemError as e:
... print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| File "<stdin>", line 2, in f
| ExceptionGroup: group1
+-+---------------- 1 ----------------
| ExceptionGroup: group2
+-+---------------- 1 ----------------
| RecursionError: 4
+------------------------------------
>>>
Note that the exceptions nested in an exception group must be instances,
not types. This is because in practice the exceptions would typically
be ones that have already been raised and caught by the program, along
the following pattern::
>>> excs = []
... for test in tests:
... try:
... test.run()
... except Exception as e:
... excs.append(e)
...
>>> if excs:
... raise ExceptionGroup("Test Failures", excs)
...