interruptible-threading 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from ._version import get_versions
4
+
5
+ __version__ = get_versions()["version"]
6
+ del get_versions
7
+
8
+
9
+ def make_version_tuple(vstr: str | None = None) -> tuple[int, ...]:
10
+ if vstr is None:
11
+ vstr = __version__
12
+ if vstr[0] == "v":
13
+ vstr = vstr[1:]
14
+ components = []
15
+ for component in vstr.split("+")[0].split("."):
16
+ try:
17
+ components.append(int(component))
18
+ except ValueError:
19
+ break
20
+ return tuple(components)
21
+
22
+
23
+ version = ".".join(str(d) for d in make_version_tuple())
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: interruptible-threading
3
+ Version: 0.0.1
4
+ Summary: Cooperative, prompt thread interruption for CPython (Linux / Darwin)
5
+ Home-page: https://github.com/smacke/python-interruptible-threads
6
+ Author: Stephen Macke
7
+ Author-email: stephen.macke@gmail.com
8
+ License: BSD-3-Clause
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Programming Language :: Python :: Implementation :: CPython
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown; charset=UTF-8
24
+ License-File: docs/LICENSE.txt
25
+ Provides-Extra: test
26
+ Requires-Dist: black<24; extra == "test"
27
+ Requires-Dist: isort; extra == "test"
28
+ Requires-Dist: mypy; extra == "test"
29
+ Requires-Dist: pytest; extra == "test"
30
+ Requires-Dist: pytest-cov; extra == "test"
31
+ Requires-Dist: pytest-timeout; extra == "test"
32
+ Requires-Dist: ruff; extra == "test"
33
+ Provides-Extra: dev
34
+ Requires-Dist: build; extra == "dev"
35
+ Requires-Dist: pycln; extra == "dev"
36
+ Requires-Dist: twine; extra == "dev"
37
+ Requires-Dist: versioneer; extra == "dev"
38
+ Requires-Dist: black<24; extra == "dev"
39
+ Requires-Dist: isort; extra == "dev"
40
+ Requires-Dist: mypy; extra == "dev"
41
+ Requires-Dist: pytest; extra == "dev"
42
+ Requires-Dist: pytest-cov; extra == "dev"
43
+ Requires-Dist: pytest-timeout; extra == "dev"
44
+ Requires-Dist: ruff; extra == "dev"
45
+ Dynamic: license-file
46
+
47
+ # Python Interruptible Threads
48
+
49
+ Cooperative, prompt thread interruption for CPython. Call `thread.interrupt()` and a
50
+ target thread raises an exception (`ThreadInterrupted` by default) — even when it is
51
+ parked in `time.sleep`, `select`, `asyncio.sleep`, an `Event`/`Queue`/`Condition` wait,
52
+ or a (helper-wrapped) blocking socket read.
53
+
54
+ It uses `ctypes` to reach `PyThreadState_SetAsyncExc` and patches a curated set of
55
+ stdlib blocking primitives, so it works only on **CPython** and **POSIX (Linux / Darwin)**.
56
+
57
+ ```python
58
+ from interruptible_threading import InterruptibleThread, ThreadInterrupted
59
+ import time
60
+
61
+ InterruptibleThread.install_patches()
62
+
63
+ def sleep_forever():
64
+ try:
65
+ while True:
66
+ time.sleep(10)
67
+ except ThreadInterrupted:
68
+ print("interrupted")
69
+
70
+ t = InterruptibleThread(target=sleep_forever, daemon=True)
71
+ t.start()
72
+ ...
73
+ t.interrupt() # output: "interrupted"
74
+ ```
75
+
76
+ > **Back-compat:** earlier versions raised `KeyboardInterrupt`. To keep that behavior,
77
+ > `InterruptibleThread.install_patches(interrupt_exc=KeyboardInterrupt)`, or
78
+ > `install_patches(legacy_keyboardinterrupt=True)` to deliver an exception caught by
79
+ > *both* `ThreadInterrupted` and `KeyboardInterrupt` handlers.
80
+
81
+ ## How it works
82
+
83
+ Pure-Python code is interrupted with `PyThreadState_SetAsyncExc`, an async exception
84
+ that fires at the next bytecode boundary. That cannot break a thread sitting in a
85
+ C-level blocking call, so `install_patches()` replaces `time.sleep`,
86
+ `selectors.DefaultSelector`, `select.select`, and `threading.Condition.wait` (which
87
+ also covers `Event.wait` and `queue.Queue`) with versions that wake promptly via a
88
+ per-thread self-pipe / chunked polling.
89
+
90
+ The design rests on a single **durable per-thread "interrupt pending" flag**.
91
+ `interrupt()` sets the flag first (under one lock), then issues a wakeup nudge; every
92
+ blocking primitive checks the flag *before* parking and *again* after waking. This
93
+ removes the time-of-check/time-of-use races inherent in choosing a delivery path from
94
+ transient state, and makes interrupts maskable and pollable.
95
+
96
+ ### Why not `signal.pthread_kill`?
97
+
98
+ `pthread_kill` can unblock a syscall but cannot deliver an *exception* to a worker
99
+ thread: CPython runs Python-level signal handlers only on the main thread, and PEP 475's
100
+ EINTR auto-retry loops call `PyErr_CheckSignals()` — a no-op off the main thread — without
101
+ consulting `tstate->async_exc`, so the syscall is transparently retried. The self-pipe +
102
+ cooperative-primitive approach is the only way to get prompt, exception-bearing
103
+ interruption of worker threads on CPython.
104
+
105
+ ## API
106
+
107
+ | Name | Purpose |
108
+ | --- | --- |
109
+ | `InterruptibleThread(...)` | `threading.Thread` subclass with `.interrupt(recursive=False)`. |
110
+ | `InterruptibleThread.install_patches(interrupt_exc=ThreadInterrupted, legacy_keyboardinterrupt=False, monkeypatch_socket=False)` | Install the stdlib patches (process-wide). |
111
+ | `InterruptibleThread.uninstall_patches()` | Restore the originals. |
112
+ | `InterruptibleThread.run_interruptible(coro)` | Run a coroutine via `asyncio.run` with clean, cancellation-based interruption. |
113
+ | `ThreadInterrupted` | Default interrupt exception (subclass of `BaseException`). |
114
+ | `is_interrupted(thread=None)` | Whether an interrupt is pending (non-consuming). |
115
+ | `clear_interrupt()` | Consume a pending interrupt without raising; returns whether one was pending. |
116
+ | `check_interrupt()` / `interruptible_checkpoint()` | Raise if pending and not masked — for CPU-bound loops. |
117
+ | `periodic_checkpoint(every=N)` | Context manager yielding a `.tick()` that checkpoints every N calls. |
118
+ | `critical_section()` / `interrupts_disabled()` | Defer delivery during cleanup; deliver on exit. |
119
+ | `interruptible_recv/send/accept(sock, ...)` | Interruptible blocking socket ops. |
120
+ | `set_poll_interval(seconds)` | Tune the chunked-poll wakeup latency (default 50 ms). |
121
+
122
+ ### Masking critical sections
123
+
124
+ ```python
125
+ from interruptible_threading import critical_section
126
+
127
+ with critical_section():
128
+ commit() # interrupts that arrive here are deferred...
129
+ release_resources() # ...and delivered exactly once when the block exits
130
+ ```
131
+
132
+ ### CPU-bound loops
133
+
134
+ ```python
135
+ from interruptible_threading import periodic_checkpoint
136
+
137
+ with periodic_checkpoint(every=1000) as ck:
138
+ for row in huge_iterable:
139
+ ck.tick() # raises ThreadInterrupted promptly once interrupted
140
+ crunch(row)
141
+ ```
142
+
143
+ ### asyncio
144
+
145
+ ```python
146
+ import asyncio
147
+ from interruptible_threading import InterruptibleThread
148
+
149
+ def worker():
150
+ try:
151
+ InterruptibleThread.run_interruptible(main_coro())
152
+ except ThreadInterrupted:
153
+ print("cancelled cleanly")
154
+ ```
155
+
156
+ `run_interruptible` cancels the loop's tasks via `call_soon_threadsafe` (proper
157
+ `finally` / `async with` unwind) instead of injecting an exception into the selector.
158
+
159
+ ## Limitations
160
+
161
+ - **CPython + POSIX only.** Relies on `PyThreadState_SetAsyncExc`, `os.pipe`, and `select`.
162
+ - **Uncovered blocking calls** stay blocked until they return: synchronous regular-file
163
+ disk I/O (`open().read()`, `os.read` on files), raw blocking `socket.recv` (use the
164
+ `interruptible_recv` helpers or `monkeypatch_socket=True`), `os.waitpid`, and
165
+ `Lock.acquire` on a builtin lock. The pending flag is honored at the next patched
166
+ primitive / checkpoint, but the in-progress call is not broken.
167
+ - **C extensions that release the GIL and never re-enter Python** (heavy NumPy kernels,
168
+ compiled crypto) cannot be preempted; only cooperative `check_interrupt()` helps.
169
+ - **Chunked-poll primitives** (`Event`/`Queue`/`Condition`) have up to `_POLL_INTERVAL`
170
+ (default 50 ms) latency, tunable via `set_poll_interval`.
171
+ - **Async injection lands at an arbitrary bytecode boundary** and is un-recallable, so
172
+ `critical_section()` is airtight only for the flag-driven paths; prefer
173
+ checkpoints/blocking primitives inside code that must not be interrupted mid-cleanup.
174
+ - **Catch-and-continue clears the flag explicitly.** The interrupt-pending flag is
175
+ durable (so an interrupt is never lost if the target parks in a blocking call before
176
+ async injection can fire). Consequently, if you *catch* `ThreadInterrupted` and want to
177
+ keep running, call `clear_interrupt()` — otherwise the next `check_interrupt()` /
178
+ blocking primitive re-raises. This mirrors Java's `Thread.interrupted()`.
179
+ - **`install_patches()` mutates global stdlib state.** Code that captured references
180
+ before install (e.g. `from time import sleep`) keeps the originals. Not for libraries
181
+ to call implicitly.
182
+ - **The main thread is intentionally not interruptible** by this mechanism, preserving
183
+ real Ctrl-C / `KeyboardInterrupt` semantics.
184
+
185
+ ## Development
186
+
187
+ ```sh
188
+ pip install -e .[test] # or: make devdeps
189
+ make check # blackcheck + ruff + mypy + pytest (with coverage)
190
+ pytest # just the tests
191
+ ```
@@ -0,0 +1,8 @@
1
+ interruptible_threading/__init__.py,sha256=xHYv5wBb1aq4cKP6FPdTsAvnltLZGLR9OHasbpPdBZs,31471
2
+ interruptible_threading/_version.py,sha256=-YncIGcUPILQBzd-acXtxeF0_hgPNMp39yPs5NlmFxI,24533
3
+ interruptible_threading/version.py,sha256=c8HwxLvTIqdRupa533VVTdBYkwZYWO68zdW1W9ZKhiU,558
4
+ interruptible_threading-0.0.1.dist-info/licenses/docs/LICENSE.txt,sha256=n1MAZY8xTWXTTcCY1xRTW-8dfjCJZCnXDBKGOe7WEQE,1521
5
+ interruptible_threading-0.0.1.dist-info/METADATA,sha256=kWd7T2i550ECvtPeeMf1Xbe3NooFo_Eo_E77jOs1UNo,8665
6
+ interruptible_threading-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ interruptible_threading-0.0.1.dist-info/top_level.txt,sha256=8UoMurxuXTgSJ5IUCfrh_e4Stx55qX_YlsPH6VKikcA,24
8
+ interruptible_threading-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Stephen Macke
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ interruptible_threading