]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-45292: [PEP-654] exception groups and except* documentation (GH-30158)
authorIrit Katriel <1055913+iritkatriel@users.noreply.github.com>
Thu, 6 Jan 2022 19:05:34 +0000 (19:05 +0000)
committerGitHub <noreply@github.com>
Thu, 6 Jan 2022 19:05:34 +0000 (19:05 +0000)
Doc/library/exceptions.rst
Doc/reference/compound_stmts.rst
Doc/tutorial/errors.rst

index 12d7d8abb26504a2a18acda55de30eff70f8dba3..f90b6761154af530c1f9df7d0db22f5fdc368f57 100644 (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
 -------------------
index 03fc2cb962791fa54d9a8e70f164556093e7f52a..473f977a4cd9d8892814d86180ecbe1db356a81f 100644 (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
index 3f09db21040680cc2a3ffc88cc181e5a6f1d9a10..ad1ef841bffc417c6910c28ff4508d3ebc8527f1 100644 (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)
+   ...
+