Skip to content

Commit 67b1370

Browse files
committed
revert RaiseCancel change, improve tests
1 parent f37d993 commit 67b1370

3 files changed

Lines changed: 73 additions & 55 deletions

File tree

src/trio/_core/_run.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,18 +1648,7 @@ def _attempt_delivery_of_any_pending_cancel(self) -> None:
16481648
if not self._cancel_status.effectively_cancelled:
16491649
return
16501650

1651-
def raise_cancel() -> NoReturn:
1652-
reason = self._cancel_status._scope._cancel_reason
1653-
if reason is None:
1654-
raise Cancelled._create(source="unknown", reason="misnesting")
1655-
1656-
raise Cancelled._create(
1657-
source=reason.source,
1658-
reason=reason.reason,
1659-
source_task=reason.source_task,
1660-
)
1661-
1662-
self._attempt_abort(raise_cancel)
1651+
self._attempt_abort(RaiseCancel(self._cancel_status._scope._cancel_reason))
16631652

16641653
def _attempt_delivery_of_pending_ki(self) -> None:
16651654
assert self._runner.ki_pending
@@ -1673,6 +1662,24 @@ def raise_cancel() -> NoReturn:
16731662
self._attempt_abort(raise_cancel)
16741663

16751664

1665+
class RaiseCancel:
1666+
def __init__(self, reason: CancelReason | None) -> None:
1667+
if reason is None:
1668+
self.cancelled = Cancelled._create(source="unknown", reason="misnesting")
1669+
else:
1670+
self.cancelled = Cancelled._create(
1671+
source=reason.source,
1672+
reason=reason.reason,
1673+
source_task=reason.source_task,
1674+
)
1675+
1676+
def __call__(self) -> NoReturn:
1677+
try:
1678+
raise self.cancelled
1679+
finally:
1680+
del self.cancelled
1681+
1682+
16761683
################################################################
16771684
# The central Runner object
16781685
################################################################

src/trio/_tests/test_threads.py

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -993,19 +993,26 @@ async def async_time_bomb() -> None:
993993
assert cancel_scope.cancelled_caught
994994

995995

996-
async def test_from_thread_check_cancelled() -> None:
997-
q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue()
996+
async def child(
997+
abandon_on_cancel: bool,
998+
scope: CancelScope,
999+
record: list[str],
1000+
f: Callable[[], None],
1001+
) -> None:
1002+
with scope:
1003+
record.append("start")
1004+
try:
1005+
return await to_thread_run_sync(f, abandon_on_cancel=abandon_on_cancel)
1006+
except _core.Cancelled as e:
1007+
record.append(str(e))
1008+
raise
1009+
finally:
1010+
record.append("exit")
9981011

999-
async def child(abandon_on_cancel: bool, scope: CancelScope) -> None:
1000-
with scope:
1001-
record.append("start")
1002-
try:
1003-
return await to_thread_run_sync(f, abandon_on_cancel=abandon_on_cancel)
1004-
except _core.Cancelled as e:
1005-
record.append(str(e))
1006-
raise
1007-
finally:
1008-
record.append("exit")
1012+
1013+
@pytest.mark.parametrize("cancel_the_scope", [False, True])
1014+
async def test_from_thread_check_cancelled_no_abandon(cancel_the_scope: bool) -> None:
1015+
q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue()
10091016

10101017
def f() -> None:
10111018
try:
@@ -1017,42 +1024,42 @@ def f() -> None:
10171024
ev.wait()
10181025
return from_thread_check_cancelled()
10191026

1020-
# Base case: nothing cancelled so we shouldn't see cancels anywhere
10211027
record: list[str] = []
10221028
ev = threading.Event()
1023-
async with _core.open_nursery() as nursery:
1024-
nursery.start_soon(child, False, _core.CancelScope())
1025-
await wait_all_tasks_blocked()
1026-
assert record[0] == "start"
1027-
assert q.get(timeout=1) == "Not Cancelled"
1028-
ev.set()
1029-
# implicit assertion, Cancelled not raised via nursery
1030-
assert record[1] == "exit"
1031-
1032-
# abandon_on_cancel=False case: a cancel will pop out but be handled by
1033-
# the appropriate cancel scope
1034-
record = []
1035-
ev = threading.Event()
10361029
scope = _core.CancelScope() # Nursery cancel scope gives false positives
1030+
10371031
async with _core.open_nursery() as nursery:
1038-
nursery.start_soon(child, False, scope)
1032+
nursery.start_soon(child, False, scope, record, f)
10391033
await wait_all_tasks_blocked()
10401034
assert record[0] == "start"
10411035
assert q.get(timeout=1) == "Not Cancelled"
1042-
scope.cancel()
1036+
if cancel_the_scope:
1037+
scope.cancel()
10431038
ev.set()
1044-
assert scope.cancelled_caught
1045-
assert re.fullmatch(
1046-
r"cancelled due to explicit from task "
1047-
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled' at 0x\w*>",
1048-
record[1],
1049-
), record[1]
1050-
assert record[2] == "exit"
1051-
assert len(record) == 3
1039+
# Base case: nothing cancelled so we shouldn't see cancels anywhere
1040+
if not cancel_the_scope:
1041+
# implicit assertion, Cancelled not raised via nursery
1042+
assert record[1] == "exit"
1043+
else:
1044+
# abandon_on_cancel=False case: a cancel will pop out but be handled by
1045+
# the appropriate cancel scope
1046+
1047+
assert scope.cancelled_caught
1048+
assert re.fullmatch(
1049+
r"cancelled due to explicit from task "
1050+
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_no_abandon' at 0x\w*>",
1051+
record[1],
1052+
), record[1]
1053+
assert record[2] == "exit"
1054+
assert len(record) == 3
1055+
10521056

1057+
async def test_from_thread_check_cancelled_abandon_on_cancel() -> None:
1058+
q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue()
10531059
# abandon_on_cancel=True case: slightly different thread behavior needed
10541060
# check thread is cancelled "soon" after abandonment
1055-
def f() -> None: # type: ignore[no-redef] # noqa: F811
1061+
1062+
def f() -> None:
10561063
ev.wait()
10571064
try:
10581065
from_thread_check_cancelled()
@@ -1065,19 +1072,22 @@ def f() -> None: # type: ignore[no-redef] # noqa: F811
10651072
else: # pragma: no cover, test failure path
10661073
q.put("Not Cancelled")
10671074

1068-
record = []
1075+
record: list[str] = []
10691076
ev = threading.Event()
10701077
scope = _core.CancelScope()
10711078
async with _core.open_nursery() as nursery:
1072-
nursery.start_soon(child, True, scope)
1079+
nursery.start_soon(child, True, scope, record, f)
10731080
await wait_all_tasks_blocked()
10741081
assert record[0] == "start"
10751082
scope.cancel()
1076-
ev.set()
1083+
# In the worst case the nursery fully exits before the threaded function
1084+
# checks for cancellation.
1085+
ev.set()
1086+
10771087
assert scope.cancelled_caught
10781088
assert re.fullmatch(
10791089
r"cancelled due to explicit from task "
1080-
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled' at 0x\w*>",
1090+
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_abandon_on_cancel' at 0x\w*>",
10811091
record[1],
10821092
), record[1]
10831093
assert record[-1] == "exit"
@@ -1087,7 +1097,7 @@ def f() -> None: # type: ignore[no-redef] # noqa: F811
10871097
else:
10881098
assert re.fullmatch(
10891099
r"cancelled due to explicit from task "
1090-
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled' at 0x\w*>",
1100+
r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_abandon_on_cancel' at 0x\w*>",
10911101
res,
10921102
), res
10931103

src/trio/_threads.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import contextlib
44
import contextvars
5+
import copy
56
import inspect
67
import queue as stdlib_queue
78
import threading
@@ -432,7 +433,7 @@ def abort(raise_cancel: RaiseCancelT) -> trio.lowlevel.Abort:
432433
# fill so from_thread_check_cancelled can raise
433434
# 'raise_cancel' will immediately delete its reason object, so we make
434435
# a copy in each thread
435-
cancel_register[0] = raise_cancel
436+
cancel_register[0] = copy.copy(raise_cancel)
436437
if abandon_bool:
437438
# empty so report_back_in_trio_thread_fn cannot reschedule
438439
task_register[0] = None

0 commit comments

Comments
 (0)