Skip to content

Commit af71289

Browse files
authored
Merge branch 'master' into eliminate-unbounded-queue
2 parents a28490e + 0204d04 commit af71289

26 files changed

+341
-238
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
strategy:
9595
fail-fast: false
9696
matrix:
97-
python: ['pypy-3.9', 'pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12-dev', 'pypy-3.9-nightly', 'pypy-3.10-nightly']
97+
python: ['pypy-3.9', 'pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.9-nightly', 'pypy-3.10-nightly']
9898
check_formatting: ['0']
9999
no_test_requirements: ['0']
100100
extra_name: ['']

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
hooks:
2424
- id: black
2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: v0.1.11
26+
rev: v0.1.14
2727
hooks:
2828
- id: ruff
2929
types: [file]

docs-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ importlib-resources==6.1.1
4242
# via towncrier
4343
incremental==22.10.0
4444
# via towncrier
45-
jinja2==3.1.2
45+
jinja2==3.1.3
4646
# via
4747
# -r docs-requirements.in
4848
# sphinx

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: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,26 +91,29 @@ allowed-confusables = ["–"]
9191
src = ["src/trio", "notes-to-self"]
9292

9393
select = [
94-
"RUF", # Ruff-specific rules
95-
"F", # pyflakes
96-
"E", # Error
97-
"W", # Warning
98-
"I", # isort
99-
"UP", # pyupgrade
100-
"B", # flake8-bugbear
101-
"YTT", # flake8-2020
102-
"ASYNC", # flake8-async
103-
"PYI", # flake8-pyi
104-
"SIM", # flake8-simplify
105-
"TCH", # flake8-type-checking
106-
"PT", # flake8-pytest-style
94+
"ASYNC", # flake8-async
95+
"B", # flake8-bugbear
96+
"C4", # flake8-comprehensions
97+
"E", # Error
98+
"F", # pyflakes
99+
"FA", # flake8-future-annotations
100+
"I", # isort
101+
"PT", # flake8-pytest-style
102+
"PYI", # flake8-pyi
103+
"RUF", # Ruff-specific rules
104+
"SIM", # flake8-simplify
105+
"TCH", # flake8-type-checking
106+
"UP", # pyupgrade
107+
"W", # Warning
108+
"YTT", # flake8-2020
107109
]
108110
extend-ignore = [
109-
'F403', # undefined-local-with-import-star
110-
'F405', # undefined-local-with-import-star-usage
111-
'E402', # module-import-not-at-top-of-file (usually OS-specific)
112-
'E501', # line-too-long
113-
'SIM117', # multiple-with-statements (messes up lots of context-based stuff and looks bad)
111+
'E402', # module-import-not-at-top-of-file (usually OS-specific)
112+
'E501', # line-too-long
113+
'F403', # undefined-local-with-import-star
114+
'F405', # undefined-local-with-import-star-usage
115+
'PT012', # multiple statements in pytest.raises block
116+
'SIM117', # multiple-with-statements (messes up lots of context-based stuff and looks bad)
114117
]
115118

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

src/trio/_core/_run.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,6 @@
8080

8181
PosArgT = TypeVarTuple("PosArgT")
8282

83-
# Needs to be guarded, since Unpack[] would be evaluated at runtime.
84-
class _NurseryStartFunc(Protocol[Unpack[PosArgT], StatusT_co]):
85-
"""Type of functions passed to `nursery.start() <trio.Nursery.start>`."""
86-
87-
def __call__(
88-
self, *args: Unpack[PosArgT], task_status: TaskStatus[StatusT_co]
89-
) -> Awaitable[object]:
90-
...
91-
9283

9384
DEADLINE_HEAP_MIN_PRUNE_THRESHOLD: Final = 1000
9485

@@ -937,7 +928,7 @@ class NurseryManager:
937928
938929
"""
939930

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

942933
@enable_ki_protection
943934
async def __aenter__(self) -> Nursery:
@@ -1004,9 +995,10 @@ def open_nursery(
1004995
have exited.
1005996
1006997
Args:
1007-
strict_exception_groups (bool): If true, even a single raised exception will be
1008-
wrapped in an exception group. This will eventually become the default
1009-
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.
10101002
10111003
"""
10121004
if strict_exception_groups is None:
@@ -2171,7 +2163,7 @@ def run(
21712163
clock: Clock | None = None,
21722164
instruments: Sequence[Instrument] = (),
21732165
restrict_keyboard_interrupt_to_checkpoints: bool = False,
2174-
strict_exception_groups: bool = False,
2166+
strict_exception_groups: bool = True,
21752167
) -> RetT:
21762168
"""Run a Trio-flavored async function, and return the result.
21772169
@@ -2228,9 +2220,10 @@ def run(
22282220
main thread (this is a Python limitation), or if you use
22292221
:func:`open_signal_receiver` to catch SIGINT.
22302222
2231-
strict_exception_groups (bool): If true, nurseries will always wrap even a single
2232-
raised exception in an exception group. This can be overridden on the level of
2233-
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.
22342227
22352228
Returns:
22362229
Whatever ``async_fn`` returns.
@@ -2288,7 +2281,7 @@ def start_guest_run(
22882281
clock: Clock | None = None,
22892282
instruments: Sequence[Instrument] = (),
22902283
restrict_keyboard_interrupt_to_checkpoints: bool = False,
2291-
strict_exception_groups: bool = False,
2284+
strict_exception_groups: bool = True,
22922285
) -> None:
22932286
"""Start a "guest" run of Trio on top of some other "host" event loop.
22942287

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)