diff --git a/docs/source/conf.py b/docs/source/conf.py index 6045ffd828..760f8837a2 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -84,8 +84,9 @@ def setup(app): 'local_customization', ] +# FIXME: change the "python" link back to /3 when Python 3.11 is released intersphinx_mapping = { - "python": ('https://docs.python.org/3', None), + "python": ('https://docs.python.org/3.11', None), "outcome": ('https://outcome.readthedocs.io/en/latest/', None), } diff --git a/docs/source/reference-core.rst b/docs/source/reference-core.rst index 658e14d08e..2cc9c0e3f9 100644 --- a/docs/source/reference-core.rst +++ b/docs/source/reference-core.rst @@ -687,6 +687,8 @@ You might wonder why Trio can't just remember "this task should be cancelled in If you want a timeout to apply to one task but not another, then you need to put the cancel scope in that individual task's function -- ``child()``, in this example. +.. _exceptiongroups: + Errors in multiple child tasks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -709,13 +711,46 @@ limitation. Consider code like:: ``broken1`` raises ``KeyError``. ``broken2`` raises ``IndexError``. Obviously ``parent`` should raise some error, but -what? In some sense, the answer should be "both of these at once", but -in Python there can only be one exception at a time. +what? The answer is that both exceptions are grouped in an `ExceptionGroup`. +The `ExceptionGroup` and its parent class `BaseExceptionGroup` are used to encapsulate +multiple exceptions being raised at once. + +To catch individual exceptions encapsulated in an exception group, the ``except*`` +clause was introduced in Python 3.11 (:pep:`654`). Here's how it works:: + + try: + async with trio.open_nursery() as nursery: + nursery.start_soon(broken1) + nursery.start_soon(broken2) + except* KeyError: + ... # handle each KeyError + except* IndexError: + ... # handle each IndexError + +But what if you can't use ``except*`` just yet? Well, for that there is the handy +exceptiongroup_ library which lets you approximate this behavior with exception handler +callbacks:: + + from exceptiongroup import catch + + def handle_keyerror(exc): + ... # handle each KeyError + + def handle_indexerror(exc): + ... # handle each IndexError + + with catch({ + KeyError: handle_keyerror, + IndexError: handle_indexerror + }): + async with trio.open_nursery() as nursery: + nursery.start_soon(broken1) + nursery.start_soon(broken2) -Trio's answer is that it raises a ``BaseExceptionGroup`` object. This is a -special exception which encapsulates multiple exception objects – -either regular exceptions or nested ``BaseExceptionGroup``\s. +.. hint:: If your code, written using ``except*``, would set local variables, you can do + the same with handler callbacks as long as you declare those variables ``nonlocal``. +.. _exceptiongroup: https://pypi.org/project/exceptiongroup/ Spawning tasks without becoming a parent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/newsfragments/2211.breaking.rst b/newsfragments/2211.breaking.rst new file mode 100644 index 0000000000..84ec5427cb --- /dev/null +++ b/newsfragments/2211.breaking.rst @@ -0,0 +1,7 @@ +``trio.MultiError`` has been removed in favor of the built-in :exc:`BaseExceptionGroup` +(and its derivative :exc:`ExceptionGroup`), falling back to the backport_ on +Python < 3.11. +See the :ref:`updated documentation ` on how to deal with exception +groups. + +.. _backport: https://pypi.org/project/exceptiongroup/ diff --git a/newsfragments/2211.removal.rst b/newsfragments/2211.removal.rst deleted file mode 100644 index 728ac2f2ff..0000000000 --- a/newsfragments/2211.removal.rst +++ /dev/null @@ -1,4 +0,0 @@ -``trio.MultiError`` has been removed in favor of the built-in ``BaseExceptionGroup`` -(and its derivative ``ExceptionGroup``), falling back to the backport_ on Python < 3.11. - -.. _backport: https://pypi.org/project/exceptiongroup/