Skip to content

Commit e718fc4

Browse files
richardsheridanoremanjjakkdl
authored
In to_thread_run_sync(), add abandon_on_cancel= as an alias for the cancellable= flag (#2841)
* add abandon_on_cancel to to_thread_run_sync: new name for cancellable flag Replaced in code and docs * add newsfragment * thoroughly deprecate applied suggestions from code review * clean mypy and ruff * Use TrioDeprecationWarning * Apply suggestions from code review Co-authored-by: Joshua Oreman <[email protected]> * fix newsfragment type --------- Co-authored-by: Joshua Oreman <[email protected]> Co-authored-by: John Litborn <[email protected]>
1 parent 3b6b167 commit e718fc4

File tree

7 files changed

+102
-37
lines changed

7 files changed

+102
-37
lines changed

docs/source/reference-core.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,16 +1827,16 @@ to spawn a child thread, and then use a :ref:`memory channel
18271827

18281828
The ``from_thread.run*`` functions reuse the host task that called
18291829
:func:`trio.to_thread.run_sync` to run your provided function, as long as you're
1830-
using the default ``cancellable=False`` so Trio can be sure that the task will remain
1831-
around to perform the work. If you pass ``cancellable=True`` at the outset, or if
1830+
using the default ``abandon_on_cancel=False`` so Trio can be sure that the task will remain
1831+
around to perform the work. If you pass ``abandon_on_cancel=True`` at the outset, or if
18321832
you provide a :class:`~trio.lowlevel.TrioToken` when calling back in to Trio, your
18331833
functions will be executed in a new system task. Therefore, the
18341834
:func:`~trio.lowlevel.current_task`, :func:`current_effective_deadline`, or other
18351835
task-tree specific values may differ depending on keyword argument values.
18361836

18371837
You can also use :func:`trio.from_thread.check_cancelled` to check for cancellation from
18381838
a thread that was spawned by :func:`trio.to_thread.run_sync`. If the call to
1839-
:func:`~trio.to_thread.run_sync` was cancelled (even if ``cancellable=False``!), then
1839+
:func:`~trio.to_thread.run_sync` was cancelled, then
18401840
:func:`~trio.from_thread.check_cancelled` will raise :func:`trio.Cancelled`.
18411841
It's like ``trio.from_thread.run(trio.sleep, 0)``, but much faster.
18421842

newsfragments/2841.deprecated.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
To better reflect the underlying thread handling semantics,
2+
the keyword argument for `trio.to_thread.run_sync` that was
3+
previously called ``cancellable`` is now named ``abandon_on_cancel``.
4+
It still does the same thing -- allow the thread to be abandoned
5+
if the call to `trio.to_thread.run_sync` is cancelled -- but since we now
6+
have other ways to propagate a cancellation without abandoning
7+
the thread, "cancellable" has become somewhat of a misnomer.
8+
The old ``cancellable`` name is now deprecated.

trio/_socket.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def numeric_only_failure(exc: BaseException) -> bool:
240240
type,
241241
proto,
242242
flags,
243-
cancellable=True,
243+
abandon_on_cancel=True,
244244
)
245245

246246

@@ -261,7 +261,7 @@ async def getnameinfo(
261261
return await hr.getnameinfo(sockaddr, flags)
262262
else:
263263
return await trio.to_thread.run_sync(
264-
_stdlib_socket.getnameinfo, sockaddr, flags, cancellable=True
264+
_stdlib_socket.getnameinfo, sockaddr, flags, abandon_on_cancel=True
265265
)
266266

267267

@@ -272,7 +272,7 @@ async def getprotobyname(name: str) -> int:
272272
273273
"""
274274
return await trio.to_thread.run_sync(
275-
_stdlib_socket.getprotobyname, name, cancellable=True
275+
_stdlib_socket.getprotobyname, name, abandon_on_cancel=True
276276
)
277277

278278

trio/_subprocess_platform/waitid.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ async def _waitid_system_task(pid: int, event: Event) -> None:
7272
"""Spawn a thread that waits for ``pid`` to exit, then wake any tasks
7373
that were waiting on it.
7474
"""
75-
# cancellable=True: if this task is cancelled, then we abandon the
75+
# abandon_on_cancel=True: if this task is cancelled, then we abandon the
7676
# thread to keep running waitpid in the background. Since this is
7777
# always run as a system task, this will only happen if the whole
7878
# call to trio.run is shutting down.
7979

8080
try:
8181
await to_thread_run_sync(
82-
sync_wait_reapable, pid, cancellable=True, limiter=waitid_limiter
82+
sync_wait_reapable, pid, abandon_on_cancel=True, limiter=waitid_limiter
8383
)
8484
except OSError:
8585
# If waitid fails, waitpid will fail too, so it still makes

trio/_tests/test_threads.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
CancelScope,
3030
CapacityLimiter,
3131
Event,
32+
TrioDeprecationWarning,
3233
_core,
3334
fail_after,
3435
move_on_after,
@@ -337,10 +338,10 @@ def f(q: stdlib_queue.Queue[str]) -> None:
337338
q.get()
338339
register[0] = "finished"
339340

340-
async def child(q: stdlib_queue.Queue[None], cancellable: bool) -> None:
341+
async def child(q: stdlib_queue.Queue[None], abandon_on_cancel: bool) -> None:
341342
record.append("start")
342343
try:
343-
return await to_thread_run_sync(f, q, cancellable=cancellable)
344+
return await to_thread_run_sync(f, q, abandon_on_cancel=abandon_on_cancel)
344345
finally:
345346
record.append("exit")
346347

@@ -402,7 +403,7 @@ def thread_fn() -> None:
402403

403404
async def main() -> None:
404405
async def child() -> None:
405-
await to_thread_run_sync(thread_fn, cancellable=True)
406+
await to_thread_run_sync(thread_fn, abandon_on_cancel=True)
406407

407408
async with _core.open_nursery() as nursery:
408409
nursery.start_soon(child)
@@ -491,7 +492,10 @@ def thread_fn(cancel_scope: CancelScope) -> None:
491492
async def run_thread(event: Event) -> None:
492493
with _core.CancelScope() as cancel_scope:
493494
await to_thread_run_sync(
494-
thread_fn, cancel_scope, limiter=limiter_arg, cancellable=cancel
495+
thread_fn,
496+
cancel_scope,
497+
abandon_on_cancel=cancel,
498+
limiter=limiter_arg,
495499
)
496500
print("run_thread finished, cancelled:", cancel_scope.cancelled_caught)
497501
event.set()
@@ -553,7 +557,7 @@ def release_on_behalf_of(self, borrower: Task) -> None:
553557

554558
# TODO: should CapacityLimiter have an abc or protocol so users can modify it?
555559
# because currently it's `final` so writing code like this is not allowed.
556-
await to_thread_run_sync(lambda: None, limiter=CustomLimiter()) # type: ignore[arg-type]
560+
await to_thread_run_sync(lambda: None, limiter=CustomLimiter()) # type: ignore[call-overload]
557561
assert record == ["acquire", "release"]
558562

559563

@@ -571,7 +575,7 @@ def release_on_behalf_of(self, borrower: Task) -> NoReturn:
571575
bs = BadCapacityLimiter()
572576

573577
with pytest.raises(ValueError) as excinfo:
574-
await to_thread_run_sync(lambda: None, limiter=bs) # type: ignore[arg-type]
578+
await to_thread_run_sync(lambda: None, limiter=bs) # type: ignore[call-overload]
575579
assert excinfo.value.__context__ is None
576580
assert record == ["acquire", "release"]
577581
record = []
@@ -580,7 +584,7 @@ def release_on_behalf_of(self, borrower: Task) -> NoReturn:
580584
# chains with it
581585
d: dict[str, object] = {}
582586
with pytest.raises(ValueError) as excinfo:
583-
await to_thread_run_sync(lambda: d["x"], limiter=bs) # type: ignore[arg-type]
587+
await to_thread_run_sync(lambda: d["x"], limiter=bs) # type: ignore[call-overload]
584588
assert isinstance(excinfo.value.__context__, KeyError)
585589
assert record == ["acquire", "release"]
586590

@@ -881,15 +885,15 @@ async def test_trio_token_weak_referenceable() -> None:
881885
assert token is weak_reference()
882886

883887

884-
async def test_unsafe_cancellable_kwarg() -> None:
888+
async def test_unsafe_abandon_on_cancel_kwarg() -> None:
885889
# This is a stand in for a numpy ndarray or other objects
886890
# that (maybe surprisingly) lack a notion of truthiness
887891
class BadBool:
888892
def __bool__(self) -> bool:
889893
raise NotImplementedError
890894

891895
with pytest.raises(NotImplementedError):
892-
await to_thread_run_sync(int, cancellable=BadBool()) # type: ignore[arg-type]
896+
await to_thread_run_sync(int, abandon_on_cancel=BadBool()) # type: ignore[call-overload]
893897

894898

895899
async def test_from_thread_reuses_task() -> None:
@@ -933,7 +937,7 @@ def sync_check() -> None:
933937
assert not queue.get_nowait()
934938

935939
with _core.CancelScope() as cancel_scope:
936-
await to_thread_run_sync(sync_check, cancellable=True)
940+
await to_thread_run_sync(sync_check, abandon_on_cancel=True)
937941

938942
assert cancel_scope.cancelled_caught
939943
assert not await to_thread_run_sync(partial(queue.get, timeout=1))
@@ -957,7 +961,7 @@ def async_check() -> None:
957961
assert not queue.get_nowait()
958962

959963
with _core.CancelScope() as cancel_scope:
960-
await to_thread_run_sync(async_check, cancellable=True)
964+
await to_thread_run_sync(async_check, abandon_on_cancel=True)
961965

962966
assert cancel_scope.cancelled_caught
963967
assert not await to_thread_run_sync(partial(queue.get, timeout=1))
@@ -976,11 +980,11 @@ async def async_time_bomb() -> None:
976980
async def test_from_thread_check_cancelled() -> None:
977981
q: stdlib_queue.Queue[str] = stdlib_queue.Queue()
978982

979-
async def child(cancellable: bool, scope: CancelScope) -> None:
983+
async def child(abandon_on_cancel: bool, scope: CancelScope) -> None:
980984
with scope:
981985
record.append("start")
982986
try:
983-
return await to_thread_run_sync(f, cancellable=cancellable)
987+
return await to_thread_run_sync(f, abandon_on_cancel=abandon_on_cancel)
984988
except _core.Cancelled:
985989
record.append("cancel")
986990
raise
@@ -1009,7 +1013,7 @@ def f() -> None:
10091013
# implicit assertion, Cancelled not raised via nursery
10101014
assert record[1] == "exit"
10111015

1012-
# cancellable=False case: a cancel will pop out but be handled by
1016+
# abandon_on_cancel=False case: a cancel will pop out but be handled by
10131017
# the appropriate cancel scope
10141018
record = []
10151019
ev = threading.Event()
@@ -1025,7 +1029,7 @@ def f() -> None:
10251029
assert "cancel" in record
10261030
assert record[-1] == "exit"
10271031

1028-
# cancellable=True case: slightly different thread behavior needed
1032+
# abandon_on_cancel=True case: slightly different thread behavior needed
10291033
# check thread is cancelled "soon" after abandonment
10301034
def f() -> None: # type: ignore[no-redef] # noqa: F811
10311035
ev.wait()
@@ -1068,9 +1072,25 @@ async def test_reentry_doesnt_deadlock() -> None:
10681072

10691073
async def child() -> None:
10701074
while True:
1071-
await to_thread_run_sync(from_thread_run, sleep, 0, cancellable=False)
1075+
await to_thread_run_sync(from_thread_run, sleep, 0, abandon_on_cancel=False)
10721076

10731077
with move_on_after(2):
10741078
async with _core.open_nursery() as nursery:
10751079
for _ in range(4):
10761080
nursery.start_soon(child)
1081+
1082+
1083+
async def test_cancellable_and_abandon_raises() -> None:
1084+
with pytest.raises(ValueError):
1085+
await to_thread_run_sync(bool, cancellable=True, abandon_on_cancel=False) # type: ignore[call-overload]
1086+
1087+
with pytest.raises(ValueError):
1088+
await to_thread_run_sync(bool, cancellable=True, abandon_on_cancel=True) # type: ignore[call-overload]
1089+
1090+
1091+
async def test_cancellable_warns() -> None:
1092+
with pytest.warns(TrioDeprecationWarning):
1093+
await to_thread_run_sync(bool, cancellable=False)
1094+
1095+
with pytest.warns(TrioDeprecationWarning):
1096+
await to_thread_run_sync(bool, cancellable=True)

trio/_threads.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
import threading
99
from collections.abc import Awaitable, Callable
1010
from itertools import count
11-
from typing import Generic, TypeVar
11+
from typing import Generic, TypeVar, overload
1212

1313
import attr
1414
import outcome
1515
from sniffio import current_async_library_cvar
1616

1717
import trio
18-
from trio._core._traps import RaiseCancelT
1918

2019
from ._core import (
2120
RunVar,
@@ -24,6 +23,8 @@
2423
enable_ki_protection,
2524
start_thread_soon,
2625
)
26+
from ._core._traps import RaiseCancelT
27+
from ._deprecate import warn_deprecated
2728
from ._sync import CapacityLimiter
2829
from ._util import coroutine_or_error
2930

@@ -171,13 +172,36 @@ def run_in_system_nursery(self, token: TrioToken) -> None:
171172
token.run_sync_soon(self.run_sync)
172173

173174

174-
@enable_ki_protection # Decorator used on function with Coroutine[Any, Any, RetT]
175+
@overload # Decorator used on function with Coroutine[Any, Any, RetT]
176+
async def to_thread_run_sync( # type: ignore[misc]
177+
sync_fn: Callable[..., RetT],
178+
*args: object,
179+
thread_name: str | None = None,
180+
abandon_on_cancel: bool = False,
181+
limiter: CapacityLimiter | None = None,
182+
) -> RetT:
183+
...
184+
185+
186+
@overload # Decorator used on function with Coroutine[Any, Any, RetT]
175187
async def to_thread_run_sync( # type: ignore[misc]
176188
sync_fn: Callable[..., RetT],
177189
*args: object,
178190
thread_name: str | None = None,
179191
cancellable: bool = False,
180192
limiter: CapacityLimiter | None = None,
193+
) -> RetT:
194+
...
195+
196+
197+
@enable_ki_protection # Decorator used on function with Coroutine[Any, Any, RetT]
198+
async def to_thread_run_sync( # type: ignore[misc]
199+
sync_fn: Callable[..., RetT],
200+
*args: object,
201+
thread_name: str | None = None,
202+
abandon_on_cancel: bool | None = None,
203+
cancellable: bool | None = None,
204+
limiter: CapacityLimiter | None = None,
181205
) -> RetT:
182206
"""Convert a blocking operation into an async operation using a thread.
183207
@@ -198,8 +222,8 @@ async def to_thread_run_sync( # type: ignore[misc]
198222
sync_fn: An arbitrary synchronous callable.
199223
*args: Positional arguments to pass to sync_fn. If you need keyword
200224
arguments, use :func:`functools.partial`.
201-
cancellable (bool): Whether to allow cancellation of this operation. See
202-
discussion below.
225+
abandon_on_cancel (bool): Whether to abandon this thread upon
226+
cancellation of this operation. See discussion below.
203227
thread_name (str): Optional string to set the name of the thread.
204228
Will always set `threading.Thread.name`, but only set the os name
205229
if pthread.h is available (i.e. most POSIX installations).
@@ -225,17 +249,17 @@ async def to_thread_run_sync( # type: ignore[misc]
225249
starting the thread. But once the thread is running, there are two ways it
226250
can handle being cancelled:
227251
228-
* If ``cancellable=False``, the function ignores the cancellation and
252+
* If ``abandon_on_cancel=False``, the function ignores the cancellation and
229253
keeps going, just like if we had called ``sync_fn`` synchronously. This
230254
is the default behavior.
231255
232-
* If ``cancellable=True``, then this function immediately raises
256+
* If ``abandon_on_cancel=True``, then this function immediately raises
233257
`~trio.Cancelled`. In this case **the thread keeps running in
234258
background** – we just abandon it to do whatever it's going to do, and
235259
silently discard any return value or errors that it raises. Only use
236260
this if you know that the operation is safe and side-effect free. (For
237261
example: :func:`trio.socket.getaddrinfo` uses a thread with
238-
``cancellable=True``, because it doesn't really affect anything if a
262+
``abandon_on_cancel=True``, because it doesn't really affect anything if a
239263
stray hostname lookup keeps running in the background.)
240264
241265
The ``limiter`` is only released after the thread has *actually*
@@ -263,7 +287,20 @@ async def to_thread_run_sync( # type: ignore[misc]
263287
264288
"""
265289
await trio.lowlevel.checkpoint_if_cancelled()
266-
abandon_on_cancel = bool(cancellable) # raise early if cancellable.__bool__ raises
290+
if cancellable is not None:
291+
if abandon_on_cancel is not None:
292+
raise ValueError(
293+
"Cannot set `cancellable` and `abandon_on_cancel` simultaneously."
294+
)
295+
warn_deprecated(
296+
"The `cancellable=` keyword argument to `trio.to_thread.run_sync`",
297+
"0.23.0",
298+
issue=2841,
299+
instead="`abandon_on_cancel=`",
300+
)
301+
abandon_on_cancel = cancellable
302+
# raise early if abandon_on_cancel.__bool__ raises
303+
abandon_on_cancel = bool(abandon_on_cancel)
267304
if limiter is None:
268305
limiter = current_default_thread_limiter()
269306

@@ -381,14 +418,14 @@ def from_thread_check_cancelled() -> None:
381418
"""Raise `trio.Cancelled` if the associated Trio task entered a cancelled status.
382419
383420
Only applicable to threads spawned by `trio.to_thread.run_sync`. Poll to allow
384-
``cancellable=False`` threads to raise :exc:`~trio.Cancelled` at a suitable
385-
place, or to end abandoned ``cancellable=True`` threads sooner than they may
421+
``abandon_on_cancel=False`` threads to raise :exc:`~trio.Cancelled` at a suitable
422+
place, or to end abandoned ``abandon_on_cancel=True`` threads sooner than they may
386423
otherwise.
387424
388425
Raises:
389426
Cancelled: If the corresponding call to `trio.to_thread.run_sync` has had a
390427
delivery of cancellation attempted against it, regardless of the value of
391-
``cancellable`` supplied as an argument to it.
428+
``abandon_on_cancel`` supplied as an argument to it.
392429
RuntimeError: If this thread is not spawned from `trio.to_thread.run_sync`.
393430
394431
.. note::

trio/_wait_for_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async def WaitForSingleObject(obj: int | CData) -> None:
4545
WaitForMultipleObjects_sync,
4646
handle,
4747
cancel_handle,
48-
cancellable=True,
48+
abandon_on_cancel=True,
4949
limiter=trio.CapacityLimiter(math.inf),
5050
)
5151
finally:

0 commit comments

Comments
 (0)