Coverage for src/lilbee/runtime/cancellation.py: 100%
21 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Task cancellation sentinel shared between core library and TUI.
3Raised by progress callbacks when the UI signals a long-running
4background task should abort. Lives in a neutral module so that
5``lilbee.catalog`` (core) can let it propagate without importing TUI code.
6"""
8from __future__ import annotations
10import signal
11import threading
12from collections.abc import Iterator
13from contextlib import contextmanager
14from typing import Protocol
17class TaskCancelledError(Exception):
18 """Raised inside a progress callback to abort a long-running task."""
21class CancelSignal(Protocol):
22 """Anything a long-running run polls to learn it should stop.
24 Structural so a run driven from another process can be cancelled by a
25 ``multiprocessing`` event as readily as by a ``threading`` one.
26 """
28 def is_set(self) -> bool: ...
31@contextmanager
32def sigint_cancel() -> Iterator[threading.Event]:
33 """Turn Ctrl-C into a token the wiki pass polls, not a mid-page abort.
35 A build runs for hours and writes pages as it goes, so the default
36 KeyboardInterrupt drops it wherever the interpreter happened to be. Setting
37 a token instead lets it stop at a source boundary with what it wrote intact.
38 The previous handler is restored as soon as it fires, so a second Ctrl-C
39 still hard-exits a pass that is not checking the token.
41 signal.signal only works on the main thread; off it (pytest-xdist workers)
42 the token is simply never set and Ctrl-C keeps its default behaviour.
43 """
44 token = threading.Event()
45 if threading.current_thread() is not threading.main_thread():
46 yield token
47 return
48 previous = signal.getsignal(signal.SIGINT)
50 def _on_sigint(_signum: int, _frame: object) -> None:
51 signal.signal(signal.SIGINT, previous)
52 token.set()
54 signal.signal(signal.SIGINT, _on_sigint)
55 try:
56 yield token
57 finally:
58 signal.signal(signal.SIGINT, previous)