A couple PEP 654 edits (#1830)
* Markup edits. Use REsT references, fix a couple of typos. * Mention TaskGroups in Motivation; add an example of trio.MultiError API
This commit is contained in:
parent
4ca905e7ff
commit
cef697a03c
196
pep-0654.rst
196
pep-0654.rst
|
@ -1,6 +1,8 @@
|
|||
PEP: 654
|
||||
Title: Exception Groups and except*
|
||||
Author: Irit Katriel <iritkatriel@gmail.com>, Yury Selivanov <yury@edgedb.com>, Guido van Rossum <guido@python.org>
|
||||
Author: Irit Katriel <iritkatriel@gmail.com>,
|
||||
Yury Selivanov <yury@edgedb.com>,
|
||||
Guido van Rossum <guido@python.org>
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
|
@ -23,8 +25,7 @@ Motivation
|
|||
==========
|
||||
|
||||
The interpreter is currently able to propagate at most one exception at a
|
||||
time. The chaining features introduced in
|
||||
`PEP 3134 <https://www.python.org/dev/peps/pep-3134/>`_ link together
|
||||
time. The chaining features introduced in :pep:`3134` link together
|
||||
exceptions that are related to each other as the cause or context, but
|
||||
there are situations where multiple unrelated exceptions need to be propagated
|
||||
together as the stack unwinds. Several real world use cases are listed below.
|
||||
|
@ -32,40 +33,42 @@ together as the stack unwinds. Several real world use cases are listed below.
|
|||
* **Concurrent errors**. Libraries for async concurrency provide APIs to invoke
|
||||
multiple tasks and return their results in aggregate. There isn't currently
|
||||
a good way for such libraries to handle situations where multiple tasks
|
||||
raise exceptions. The Python standard library's
|
||||
`asyncio.gather() <https://docs.python.org/3/library/asyncio-task.html#asyncio.gather>`_
|
||||
raise exceptions. The Python standard library's ``asyncio.gather()`` [1]_
|
||||
function provides two options: raise the first exception, or return the
|
||||
exceptions in the results list. The `Trio <https://trio.readthedocs.io/en/stable/>`_
|
||||
exceptions in the results list. The Trio [2]_
|
||||
library has a ``MultiError`` exception type which it raises to report a
|
||||
collection of errors. Work on this PEP was initially motivated by the
|
||||
difficulties in handling ``MultiErrors``, which are detailed in a design
|
||||
document for an improved version, `MultiError2 <https://github.com/python-trio/trio/issues/611>`_.
|
||||
difficulties in handling ``MultiErrors`` [9]_, which are detailed in a design
|
||||
document for an improved version, ``MultiError2`` [3]_.
|
||||
That document demonstrates how difficult it is to create an effective API
|
||||
for reporting and handling multiple errors without the language changes we
|
||||
are proposing.
|
||||
are proposing (see also the `Programming Without 'except \*'`_ section.)
|
||||
|
||||
Implementing a better task spawning API in asyncio, inspired by Trio
|
||||
nurseries, was the main motivation for this PEP. That work is currently
|
||||
blocked on Python not having native language level support for exception
|
||||
groups.
|
||||
|
||||
* **Multiple failures when retrying an operation.** The Python standard
|
||||
library's ``socket.create_connection`` function may attempt to connect to
|
||||
different addresses, and if all attempts fail it needs to report that to the
|
||||
user. It is an open issue how to aggregate these errors, particularly when
|
||||
they are different [`Python issue 29980 <https://bugs.python.org/issue29980>`_].
|
||||
they are different (see issue 29980 [4]_.)
|
||||
|
||||
* **Multiple user callbacks fail.** Python's ``atexit.register()`` function
|
||||
allows users to register functions that are called on system exit. If any of
|
||||
them raise exceptions, only the last one is reraised, but it would be better
|
||||
to reraised all of them together
|
||||
[`atexit documentation <https://docs.python.org/3/library/atexit.html#atexit.register>`_].
|
||||
to reraised all of them together (see ``atexit`` documentation [5]_.)
|
||||
Similarly, the pytest library allows users to register finalizers which
|
||||
are executed at teardown. If more than one of these finalizers raises an
|
||||
exception, only the first is reported to the user. This can be improved with
|
||||
``ExceptionGroups``, as explained in this issue by pytest developer Ran Benita
|
||||
[`Pytest issue 8217 <https://github.com/pytest-dev/pytest/issues/8217>`_].
|
||||
(see pytest issue 8217 [6]_.)
|
||||
|
||||
* **Multiple errors in a complex calculation.** The Hypothesis library performs
|
||||
automatic bug reduction (simplifying code that demonstrates a bug). In the
|
||||
process it may find variations that generate different errors, and
|
||||
(optionally) reports all of them
|
||||
[`Hypothesis documentation <https://hypothesis.readthedocs.io/en/latest/settings.html#hypothesis.settings.report_multiple_bugs>`_].
|
||||
(optionally) reports all of them (see the Hypothesis documentation [7]_.)
|
||||
An ``ExceptionGroup`` mechanism as we are proposing here can resolve some of
|
||||
the difficulties with debugging that are mentioned in the link above, and
|
||||
which are due to the loss of context/cause information (communicated
|
||||
|
@ -77,7 +80,8 @@ together as the stack unwinds. Several real world use cases are listed below.
|
|||
effectively masked an exception that the user's code raised inside the context
|
||||
manager scope. While the user's exception was chained as the context of the
|
||||
cleanup error, it was not caught by the user's except clause
|
||||
[`Python issue 40857 <https://bugs.python.org/issue40857>`_].
|
||||
(see issue 40857 [8]_.)
|
||||
|
||||
The issue was resolved by making the cleanup code ignore errors, thus
|
||||
sidestepping the multiple exception problem. With the features we propose
|
||||
here, it would be possible for ``__exit__`` to raise an ``ExceptionGroup``
|
||||
|
@ -90,12 +94,11 @@ Rationale
|
|||
|
||||
Grouping several exceptions together can be done without changes to the
|
||||
language, simply by creating a container exception type.
|
||||
`Trio <https://trio.readthedocs.io/en/stable/>`_ is an example of a library
|
||||
that has made use of this technique in its
|
||||
`MultiError <https://trio.readthedocs.io/en/stable/reference-core.html#trio.MultiError>`_
|
||||
type. However, such approaches require calling code to catch the container
|
||||
exception type, and then inspect it to determine the types of errors that had
|
||||
occurred, extract the ones it wants to handle and reraise the rest.
|
||||
Trio [2]_ is an example of a library that has made use of this technique in its
|
||||
``MultiError`` [9]_ type. However, such an approach requires calling code to catch
|
||||
the container exception type, and then to inspect it to determine the types of
|
||||
errors that had occurred, extract the ones it wants to handle, and reraise the
|
||||
rest.
|
||||
|
||||
Changes to the language are required in order to extend support for
|
||||
``ExceptionGroups`` in the style of existing exception handling mechanisms. At
|
||||
|
@ -129,8 +132,8 @@ sequence of the nested exceptions, for example:
|
|||
``ExceptionGroup('issues', [ValueError('bad value'), TypeError('bad type')])``.
|
||||
|
||||
The ``ExceptionGroup`` class exposes these parameters in the fields ``message``
|
||||
and ``errors``. A nested exception can also be an ``ExceptionGroup`` so the class
|
||||
represents a tree of exceptions, where the leaves are plain exceptions and
|
||||
and ``errors``. A nested exception can also be an ``ExceptionGroup`` so the
|
||||
class represents a tree of exceptions, where the leaves are plain exceptions and
|
||||
each internal node represent a time at which the program grouped some
|
||||
unrelated exceptions into a new ``ExceptionGroup`` and raised them together.
|
||||
The ``ExceptionGroup`` class is final, i.e., it cannot be subclassed.
|
||||
|
@ -182,8 +185,8 @@ new copy. Leaf exceptions are not copied, nor are ``ExceptionGroups`` which are
|
|||
fully contained in the result. When it is necessary to partition an
|
||||
``ExceptionGroup`` because the condition holds for some, but not all of its
|
||||
contained exceptions, a new ``ExceptionGroup`` is created but the ``__cause__``,
|
||||
``__context__`` and ``__traceback__`` fields are copied by reference, so are shared
|
||||
with the original ``eg``.
|
||||
``__context__`` and ``__traceback__`` fields are copied by reference, so are
|
||||
shared with the original ``eg``.
|
||||
|
||||
If both the subgroup and its complement are needed, the
|
||||
``ExceptionGroup.split(condition)`` method can be used:
|
||||
|
@ -218,7 +221,12 @@ other side:
|
|||
.. code-block::
|
||||
|
||||
>>> other_errors.split(lambda e: isinstance(e, SyntaxError))
|
||||
(None, ExceptionGroup('one', [ExceptionGroup('two', [ValueError(3)]), ExceptionGroup('three', [OSError(4)])]))
|
||||
(None, ExceptionGroup('one', [
|
||||
ExceptionGroup('two', [
|
||||
ValueError(3)
|
||||
]),
|
||||
ExceptionGroup('three', [
|
||||
OSError(4)])]))
|
||||
|
||||
Since splitting by exception type is a very common use case, ``subgroup`` and
|
||||
``split`` can take an exception type or tuple of exception types and treat it
|
||||
|
@ -814,7 +822,8 @@ It is not possible to use both traditional ``except`` blocks and the new
|
|||
except ValueError:
|
||||
pass
|
||||
except *CancelledError: # <- SyntaxError:
|
||||
pass # combining ``except`` and ``except*`` is prohibited
|
||||
pass # combining ``except`` and ``except*``
|
||||
# is prohibited
|
||||
|
||||
It is possible to catch the ``ExceptionGroup`` type with ``except``, but not
|
||||
with ``except*`` because the latter is ambiguous:
|
||||
|
@ -869,13 +878,13 @@ propose in this PEP will not break any existing code:
|
|||
Once programs begin to use these features, there will be migration issues to
|
||||
consider:
|
||||
|
||||
* An ``except Exception:`` clause will not catch ``ExceptionGroups`` because they
|
||||
are derived from ``BaseException``. Any such clause will need to be replaced
|
||||
* An ``except Exception:`` clause will not catch ``ExceptionGroup`` because it
|
||||
is derived from ``BaseException``. Any such clause will need to be replaced
|
||||
by ``except (Exception, ExceptionGroup):`` or ``except *Exception:``.
|
||||
|
||||
* Similarly, any ``except T:`` clause that wraps code which is now potentially
|
||||
raising ``ExceptionGroup`` needs to become ``except *T:``, and its body may need
|
||||
to be updated.
|
||||
raising ``ExceptionGroup`` needs to become ``except *T:``, and its body may
|
||||
need to be updated.
|
||||
|
||||
* Libraries that need to support older Python versions will not be able to use
|
||||
``except*`` or raise ``ExceptionGroups``.
|
||||
|
@ -885,15 +894,16 @@ How to Teach This
|
|||
=================
|
||||
|
||||
``ExceptionGroups`` and ``except*`` will be documented as part of the language
|
||||
standard. Libraries that raise ``ExceptionGroups`` such as ``asyncio`` will need to
|
||||
specify this in their documentation and clarify which API calls need to be
|
||||
standard. Libraries that raise ``ExceptionGroups`` such as ``asyncio`` will need
|
||||
to specify this in their documentation and clarify which API calls need to be
|
||||
wrapped with ``try-except*`` rather than ``try-except``.
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
We developed these concepts (and the examples for this PEP) with
|
||||
the help of `a reference implementation <https://github.com/iritkatriel/cpython/tree/exceptionGroup-stage5>`_.
|
||||
the help of the reference implementation [10]_.
|
||||
|
||||
It has the builtin ``ExceptionGroup`` along with the changes to the traceback
|
||||
formatting code, in addition to the grammar, compiler and interpreter changes
|
||||
|
@ -1048,72 +1058,104 @@ clauses. We rejected this because it would be less obvious. The fact that we
|
|||
are handling ``ExceptionGroups`` of ``T`` rather than only naked ``Ts`` should be
|
||||
specified in the same place where we state ``T``.
|
||||
|
||||
|
||||
Programming Without 'except \*'
|
||||
===============================
|
||||
|
||||
Consider the following simple example of the ``except *`` syntax (pretending
|
||||
Trio natively supported this proposal):
|
||||
|
||||
.. code-block::
|
||||
|
||||
try:
|
||||
async with trio.open_nursery() as nursery:
|
||||
# Make two concurrent calls to child()
|
||||
nursery.start_soon(child)
|
||||
nursery.start_soon(child)
|
||||
except *ValueError:
|
||||
pass
|
||||
|
||||
Here is how this code would look in Python 3.9:
|
||||
|
||||
.. code-block::
|
||||
|
||||
def handle_ValueError(exc):
|
||||
if isinstance(exc, ValueError):
|
||||
return None
|
||||
else:
|
||||
return exc # reraise exc
|
||||
|
||||
with MultiError.catch(handle_ValueError):
|
||||
async with trio.open_nursery() as nursery:
|
||||
# Make two concurrent calls to child()
|
||||
nursery.start_soon(child)
|
||||
nursery.start_soon(child)
|
||||
|
||||
This example clearly demonstrates how unintuitive and cumbersome handling
|
||||
of multiple errors is in current Python. The exception handling logic has
|
||||
to be in a separate closure and is fairly low level, requiring the writer to
|
||||
have non-trivial understanding of both Python exceptions mechanics and the
|
||||
Trio APIs. Instead of using the ``try..except`` block we have to use a
|
||||
``with`` block. We need to explicitly reraise exceptions we are not handling.
|
||||
Handling more exception types or implementing more complex
|
||||
exception handling logic will only further complicate the code to the point
|
||||
of it being unreadable.
|
||||
|
||||
|
||||
See Also
|
||||
========
|
||||
|
||||
* An analysis of how exception groups will likely be used in asyncio
|
||||
programs:
|
||||
https://github.com/python/exceptiongroups/issues/3#issuecomment-716203284
|
||||
programs: [11]_.
|
||||
|
||||
* The issue where the ``except*`` concept was first formalized: [12]_.
|
||||
|
||||
* ``MultiError2`` design document: [3]_.
|
||||
|
||||
* Reporting Multiple Errors in the Hypothesis library: [7]_.
|
||||
|
||||
* The issue where the ``except*`` concept was first formalized:
|
||||
https://github.com/python/exceptiongroups/issues/4
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
* Reference implementation:
|
||||
.. [1] https://docs.python.org/3/library/asyncio-task.html#asyncio.gather
|
||||
|
||||
Branch: https://github.com/iritkatriel/cpython/tree/exceptionGroup-stage5
|
||||
.. [2] https://trio.readthedocs.io/en/stable/
|
||||
|
||||
PR: https://github.com/iritkatriel/cpython/pull/10
|
||||
.. [3] https://github.com/python-trio/trio/issues/611
|
||||
|
||||
* PEP 3134: Exception Chaining and Embedded Tracebacks
|
||||
.. [4] https://bugs.python.org/issue29980
|
||||
|
||||
https://www.python.org/dev/peps/pep-3134/
|
||||
.. [5] https://docs.python.org/3/library/atexit.html#atexit.register
|
||||
|
||||
* The ``asyncio`` standard library
|
||||
.. [6] https://github.com/pytest-dev/pytest/issues/8217
|
||||
|
||||
https://docs.python.org/3/library/asyncio.html
|
||||
.. [7] https://hypothesis.readthedocs.io/en/latest/settings.html#hypothesis.settings.report_multiple_bugs
|
||||
|
||||
``asyncio.gather()``:
|
||||
https://docs.python.org/3/library/asyncio-task.html#asyncio.gather
|
||||
.. [8] https://bugs.python.org/issue40857
|
||||
|
||||
* The Trio Library
|
||||
.. [9] https://trio.readthedocs.io/en/stable/reference-core.html#trio.MultiError
|
||||
|
||||
Trio: https://trio.readthedocs.io/en/stable/
|
||||
.. [10] https://github.com/iritkatriel/cpython/tree/exceptionGroup-stage5
|
||||
|
||||
``MultiError``:
|
||||
https://trio.readthedocs.io/en/stable/reference-core.html#trio.MultiError
|
||||
.. [11] https://github.com/python/exceptiongroups/issues/3#issuecomment-716203284
|
||||
|
||||
``MultiError2`` design document: https://github.com/python-trio/trio/issues/611.
|
||||
.. [12] https://github.com/python/exceptiongroups/issues/4
|
||||
|
||||
* Python issue 29980: OSError: multiple exceptions should preserve the
|
||||
exception type if it is common
|
||||
|
||||
https://bugs.python.org/issue29980
|
||||
|
||||
* Python issue 40857: ``tempfile.TemporaryDirectory()```` context manager can fail
|
||||
to propagate exceptions generated within its context
|
||||
|
||||
https://bugs.python.org/issue40857
|
||||
|
||||
* ``atexit`` documentation:
|
||||
|
||||
https://docs.python.org/3/library/atexit.html#atexit.register
|
||||
|
||||
* PyTest issue 8217: Improve reporting when multiple teardowns raise an exception
|
||||
|
||||
https://github.com/pytest-dev/pytest/issues/8217
|
||||
|
||||
* The Hypothesis Library
|
||||
|
||||
https://hypothesis.readthedocs.io/en/latest/index.html
|
||||
|
||||
Reporting Multiple Errors:
|
||||
https://hypothesis.readthedocs.io/en/latest/settings.html#hypothesis.settings.report_multiple_bugs
|
||||
.. [13] https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document is placed in the public domain or under the
|
||||
CC0-1.0-Universal license, whichever is more permissive.
|
||||
|
||||
|
||||
..
|
||||
Local Variables:
|
||||
mode: indented-text
|
||||
indent-tabs-mode: nil
|
||||
sentence-end-double-space: t
|
||||
fill-column: 70
|
||||
coding: utf-8
|
||||
End:
|
||||
|
|
Loading…
Reference in New Issue