Skip to content

Commit e0d8edf

Browse files
authored
Merge pull request #2886 from jakkdl/remove_multierror
change strict_exception_groups default to True
2 parents d3bb19b + 0b45ec9 commit e0d8edf

17 files changed

+277
-178
lines changed

docs/source/reference-core.rst

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -768,9 +768,14 @@ inside the handler function(s) with the ``nonlocal`` keyword::
768768
async with trio.open_nursery() as nursery:
769769
nursery.start_soon(broken1)
770770

771+
.. _strict_exception_groups:
772+
771773
"Strict" versus "loose" ExceptionGroup semantics
772774
++++++++++++++++++++++++++++++++++++++++++++++++
773775

776+
..
777+
TODO: rewrite this (and possible other) sections from the new strict-by-default perspective, under the heading "Deprecated: non-strict ExceptionGroups" - to explain that it only exists for backwards-compatibility, will be removed in future, and that we recommend against it for all new code.
778+
774779
Ideally, in some abstract sense we'd want everything that *can* raise an
775780
`ExceptionGroup` to *always* raise an `ExceptionGroup` (rather than, say, a single
776781
`ValueError`). Otherwise, it would be easy to accidentally write something like ``except
@@ -796,9 +801,10 @@ to set the default behavior for any nursery in your program that doesn't overrid
796801
wrapping, so you'll get maximum compatibility with code that was written to
797802
support older versions of Trio.
798803

799-
To maintain backwards compatibility, the default is ``strict_exception_groups=False``.
800-
The default will eventually change to ``True`` in a future version of Trio, once
801-
Python 3.11 and later versions are in wide use.
804+
The default is set to ``strict_exception_groups=True``, in line with the default behaviour
805+
of ``TaskGroup`` in asyncio and anyio. We've also found that non-strict mode makes it
806+
too easy to neglect the possibility of several exceptions being raised concurrently,
807+
causing nasty latent bugs when errors occur under load.
802808

803809
.. _exceptiongroup: https://pypi.org/project/exceptiongroup/
804810

newsfragments/2786.breaking.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
The :ref:`strict_exception_groups <strict_exception_groups>` parameter now defaults to `True` in `trio.run` and `trio.lowlevel.start_guest_run`. `trio.open_nursery` still defaults to the same value as was specified in `trio.run`/`trio.lowlevel.start_guest_run`, but if you didn't specify it there then all subsequent calls to `trio.open_nursery` will change.
2+
This is unfortunately very tricky to change with a deprecation period, as raising a `DeprecationWarning` whenever :ref:`strict_exception_groups <strict_exception_groups>` is not specified would raise a lot of unnecessary warnings.
3+
4+
Notable side effects of changing code to run with ``strict_exception_groups==True``
5+
6+
* If an iterator raises `StopAsyncIteration` or `StopIteration` inside a nursery, then python will not recognize wrapped instances of those for stopping iteration.
7+
* `trio.run_process` is now documented that it can raise an `ExceptionGroup`. It previously could do this in very rare circumstances, but with :ref:`strict_exception_groups <strict_exception_groups>` set to `True` it will now do so whenever exceptions occur in ``deliver_cancel`` or with problems communicating with the subprocess.
8+
9+
* Errors in opening the process is now done outside the internal nursery, so if code previously ran with ``strict_exception_groups=True`` there are cases now where an `ExceptionGroup` is *no longer* added.
10+
* `trio.TrioInternalError` ``.__cause__`` might be wrapped in one or more `ExceptionGroups <ExceptionGroup>`

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ extend-ignore = [
111111
'E402', # module-import-not-at-top-of-file (usually OS-specific)
112112
'E501', # line-too-long
113113
'SIM117', # multiple-with-statements (messes up lots of context-based stuff and looks bad)
114+
'PT012', # multiple statements in pytest.raises block
114115
]
115116

116117
include = ["*.py", "*.pyi", "**/pyproject.toml"]

src/trio/_core/_run.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ class NurseryManager:
928928
929929
"""
930930

931-
strict_exception_groups: bool = attr.ib(default=False)
931+
strict_exception_groups: bool = attr.ib(default=True)
932932

933933
@enable_ki_protection
934934
async def __aenter__(self) -> Nursery:
@@ -995,9 +995,10 @@ def open_nursery(
995995
have exited.
996996
997997
Args:
998-
strict_exception_groups (bool): If true, even a single raised exception will be
999-
wrapped in an exception group. This will eventually become the default
1000-
behavior. If not specified, uses the value passed to :func:`run`.
998+
strict_exception_groups (bool): Unless set to False, even a single raised exception
999+
will be wrapped in an exception group. If not specified, uses the value passed
1000+
to :func:`run`, which defaults to true. Setting it to False will be deprecated
1001+
and ultimately removed in a future version of Trio.
10011002
10021003
"""
10031004
if strict_exception_groups is None:
@@ -2162,7 +2163,7 @@ def run(
21622163
clock: Clock | None = None,
21632164
instruments: Sequence[Instrument] = (),
21642165
restrict_keyboard_interrupt_to_checkpoints: bool = False,
2165-
strict_exception_groups: bool = False,
2166+
strict_exception_groups: bool = True,
21662167
) -> RetT:
21672168
"""Run a Trio-flavored async function, and return the result.
21682169
@@ -2219,9 +2220,10 @@ def run(
22192220
main thread (this is a Python limitation), or if you use
22202221
:func:`open_signal_receiver` to catch SIGINT.
22212222
2222-
strict_exception_groups (bool): If true, nurseries will always wrap even a single
2223-
raised exception in an exception group. This can be overridden on the level of
2224-
individual nurseries. This will eventually become the default behavior.
2223+
strict_exception_groups (bool): Unless set to False, nurseries will always wrap
2224+
even a single raised exception in an exception group. This can be overridden
2225+
on the level of individual nurseries. Setting it to False will be deprecated
2226+
and ultimately removed in a future version of Trio.
22252227
22262228
Returns:
22272229
Whatever ``async_fn`` returns.
@@ -2279,7 +2281,7 @@ def start_guest_run(
22792281
clock: Clock | None = None,
22802282
instruments: Sequence[Instrument] = (),
22812283
restrict_keyboard_interrupt_to_checkpoints: bool = False,
2282-
strict_exception_groups: bool = False,
2284+
strict_exception_groups: bool = True,
22832285
) -> None:
22842286
"""Start a "guest" run of Trio on top of some other "host" event loop.
22852287

src/trio/_core/_tests/test_ki.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import outcome
1010
import pytest
1111

12+
from trio.testing import RaisesGroup
13+
1214
try:
1315
from async_generator import async_generator, yield_
1416
except ImportError: # pragma: no cover
@@ -293,7 +295,8 @@ async def check_unprotected_kill() -> None:
293295
nursery.start_soon(sleeper, "s2", record_set)
294296
nursery.start_soon(raiser, "r1", record_set)
295297

296-
with pytest.raises(KeyboardInterrupt):
298+
# raises inside a nursery, so the KeyboardInterrupt is wrapped in an ExceptionGroup
299+
with RaisesGroup(KeyboardInterrupt):
297300
_core.run(check_unprotected_kill)
298301
assert record_set == {"s1 ok", "s2 ok", "r1 raise ok"}
299302

@@ -309,7 +312,8 @@ async def check_protected_kill() -> None:
309312
nursery.start_soon(_core.enable_ki_protection(raiser), "r1", record_set)
310313
# __aexit__ blocks, and then receives the KI
311314

312-
with pytest.raises(KeyboardInterrupt):
315+
# raises inside a nursery, so the KeyboardInterrupt is wrapped in an ExceptionGroup
316+
with RaisesGroup(KeyboardInterrupt):
313317
_core.run(check_protected_kill)
314318
assert record_set == {"s1 ok", "s2 ok", "r1 cancel ok"}
315319

@@ -331,6 +335,7 @@ def kill_during_shutdown() -> None:
331335

332336
token.run_sync_soon(kill_during_shutdown)
333337

338+
# no nurseries involved, so the KeyboardInterrupt isn't wrapped
334339
with pytest.raises(KeyboardInterrupt):
335340
_core.run(check_kill_during_shutdown)
336341

@@ -344,6 +349,7 @@ def before_run(self) -> None:
344349
async def main_1() -> None:
345350
await _core.checkpoint()
346351

352+
# no nurseries involved, so the KeyboardInterrupt isn't wrapped
347353
with pytest.raises(KeyboardInterrupt):
348354
_core.run(main_1, instruments=[InstrumentOfDeath()])
349355

0 commit comments

Comments
 (0)