calltimeout 0.2.0__tar.gz → 1.0.0__tar.gz
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.
- calltimeout-1.0.0/.gitignore +16 -0
- calltimeout-1.0.0/CHANGELOG.md +244 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/CONTRIBUTING.md +4 -2
- calltimeout-1.0.0/GUIDE.md +292 -0
- calltimeout-1.0.0/PKG-INFO +185 -0
- calltimeout-1.0.0/README.md +148 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/pyproject.toml +15 -7
- calltimeout-1.0.0/requirements-dev.txt +3 -0
- calltimeout-1.0.0/scripts/_tree.py +108 -0
- calltimeout-1.0.0/scripts/check.ps1 +17 -0
- calltimeout-1.0.0/scripts/fetch_upstream.py +40 -0
- calltimeout-1.0.0/scripts/mutants.json +422 -0
- calltimeout-1.0.0/scripts/mutate.py +67 -0
- calltimeout-1.0.0/scripts/release.ps1 +38 -0
- calltimeout-1.0.0/src/calltimeout/__init__.py +45 -0
- calltimeout-1.0.0/src/calltimeout/_api.py +125 -0
- calltimeout-1.0.0/src/calltimeout/_causes.py +42 -0
- calltimeout-1.0.0/src/calltimeout/_child.py +64 -0
- calltimeout-1.0.0/src/calltimeout/_decorator.py +155 -0
- calltimeout-1.0.0/src/calltimeout/_errors.py +88 -0
- calltimeout-1.0.0/src/calltimeout/_handoff.py +64 -0
- calltimeout-1.0.0/src/calltimeout/_parcel.py +186 -0
- calltimeout-1.0.0/src/calltimeout/_process.py +173 -0
- calltimeout-1.0.0/src/calltimeout/_signal.py +165 -0
- calltimeout-1.0.0/src/calltimeout/_text.py +53 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/src/calltimeout/_thread.py +14 -5
- calltimeout-1.0.0/src/calltimeout/_timers.py +91 -0
- calltimeout-1.0.0/src/calltimeout/_validate.py +192 -0
- calltimeout-1.0.0/src/calltimeout/_wire.py +119 -0
- calltimeout-1.0.0/tests/_child_targets.py +109 -0
- calltimeout-1.0.0/tests/_consumer.py +78 -0
- calltimeout-1.0.0/tests/_counting.py +25 -0
- calltimeout-1.0.0/tests/_hostile_errors.py +189 -0
- calltimeout-1.0.0/tests/_lazy_targets.py +20 -0
- calltimeout-1.0.0/tests/_method_targets.py +27 -0
- calltimeout-1.0.0/tests/_result_targets.py +22 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/_targets.py +38 -0
- calltimeout-1.0.0/tests/_upgrade_consumer.py +69 -0
- calltimeout-1.0.0/tests/_upstream.py +36 -0
- calltimeout-1.0.0/tests/_wire_targets.py +191 -0
- calltimeout-1.0.0/tests/test_aliased_keywords.py +65 -0
- calltimeout-1.0.0/tests/test_bounded_causes.py +133 -0
- calltimeout-1.0.0/tests/test_cause_details.py +85 -0
- calltimeout-1.0.0/tests/test_child_outcomes.py +102 -0
- calltimeout-1.0.0/tests/test_contract.py +72 -0
- calltimeout-1.0.0/tests/test_contract_api.py +119 -0
- calltimeout-1.0.0/tests/test_contract_edges.py +129 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_decorator.py +40 -0
- calltimeout-1.0.0/tests/test_descriptor_limit.py +98 -0
- calltimeout-1.0.0/tests/test_docs.py +124 -0
- calltimeout-1.0.0/tests/test_e2e_wheel.py +156 -0
- calltimeout-1.0.0/tests/test_error_paths.py +136 -0
- calltimeout-1.0.0/tests/test_errors.py +94 -0
- calltimeout-1.0.0/tests/test_fuzz_arguments.py +154 -0
- calltimeout-1.0.0/tests/test_fuzz_calls.py +97 -0
- calltimeout-1.0.0/tests/test_handle_release.py +87 -0
- calltimeout-1.0.0/tests/test_handles.py +179 -0
- calltimeout-1.0.0/tests/test_hardening.py +146 -0
- calltimeout-1.0.0/tests/test_hostile_causes.py +97 -0
- calltimeout-1.0.0/tests/test_hostile_names.py +153 -0
- calltimeout-1.0.0/tests/test_keyword_wrappers.py +147 -0
- calltimeout-1.0.0/tests/test_messages.py +61 -0
- calltimeout-1.0.0/tests/test_nested_process_contract.py +44 -0
- calltimeout-1.0.0/tests/test_oracles.py +104 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_process.py +31 -3
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_regressions.py +8 -3
- calltimeout-1.0.0/tests/test_result_bounds.py +87 -0
- calltimeout-1.0.0/tests/test_sending_calls.py +161 -0
- calltimeout-1.0.0/tests/test_signal_edges.py +193 -0
- calltimeout-1.0.0/tests/test_signal_floor.py +168 -0
- calltimeout-1.0.0/tests/test_signal_intervals.py +93 -0
- calltimeout-1.0.0/tests/test_signal_races.py +152 -0
- calltimeout-1.0.0/tests/test_signal_restore.py +61 -0
- calltimeout-1.0.0/tests/test_start_only_objects.py +160 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_thread.py +34 -1
- calltimeout-1.0.0/tests/test_timing_contracts.py +58 -0
- calltimeout-1.0.0/tests/test_transport_secrecy.py +62 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_typing.py +28 -0
- calltimeout-1.0.0/tests/test_unordered_timeouts.py +51 -0
- calltimeout-1.0.0/tests/test_upstream_processes.py +127 -0
- calltimeout-1.0.0/tests/test_upstream_threads.py +128 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_validation.py +82 -1
- calltimeout-1.0.0/tests/test_validation_contracts.py +180 -0
- calltimeout-1.0.0/tests/upstream/MANIFEST.json +74 -0
- calltimeout-0.2.0/.gitignore +0 -12
- calltimeout-0.2.0/CHANGELOG.md +0 -61
- calltimeout-0.2.0/IMPROVEMENTS-round.md +0 -160
- calltimeout-0.2.0/PKG-INFO +0 -233
- calltimeout-0.2.0/README.md +0 -198
- calltimeout-0.2.0/requirements-dev.txt +0 -3
- calltimeout-0.2.0/scripts/check.ps1 +0 -14
- calltimeout-0.2.0/scripts/release.ps1 +0 -28
- calltimeout-0.2.0/src/calltimeout/__init__.py +0 -11
- calltimeout-0.2.0/src/calltimeout/_api.py +0 -91
- calltimeout-0.2.0/src/calltimeout/_decorator.py +0 -77
- calltimeout-0.2.0/src/calltimeout/_errors.py +0 -30
- calltimeout-0.2.0/src/calltimeout/_process.py +0 -141
- calltimeout-0.2.0/src/calltimeout/_signal.py +0 -131
- calltimeout-0.2.0/src/calltimeout/_validate.py +0 -60
- calltimeout-0.2.0/src/calltimeout/_wire.py +0 -76
- calltimeout-0.2.0/tests/test_contract.py +0 -26
- calltimeout-0.2.0/tests/test_docs.py +0 -85
- calltimeout-0.2.0/tests/test_e2e_wheel.py +0 -104
- calltimeout-0.2.0/tests/test_errors.py +0 -39
- {calltimeout-0.2.0 → calltimeout-1.0.0}/.gitattributes +0 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/.github/FUNDING.yml +0 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/LICENSE +0 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/src/calltimeout/py.typed +0 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/conftest.py +0 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_signal_posix.py +0 -0
- {calltimeout-0.2.0 → calltimeout-1.0.0}/tests/test_signal_windows.py +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
# Third-party documents, fetched locally by scripts/fetch_upstream.py; only the manifest is tracked.
|
|
15
|
+
tests/upstream/*
|
|
16
|
+
!tests/upstream/MANIFEST.json
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.0.0] - 2026-11-02
|
|
10
|
+
|
|
11
|
+
The public API is now stable: `run`, `with_timeout`, `CallTimeout`, `CallCrashed`,
|
|
12
|
+
`RemoteTraceback` and `Mode`, with their parameter names, defaults and exception types,
|
|
13
|
+
follow Semantic Versioning from this release on. The changes below are against PyPI
|
|
14
|
+
0.2.0, published on 2026-10-31 from commit 9b6975c on `main`. Neither 0.1.0 nor 0.2.0
|
|
15
|
+
was tagged. Exceptions stored by 0.2.0 remain loadable after upgrading.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `RemoteTraceback`, the cause chained to an exception raised by a process-mode call,
|
|
20
|
+
whose text (`.text`) is the child's traceback, is public and shown and pickled as
|
|
21
|
+
`calltimeout.RemoteTraceback`. It was a private class, shown in every such traceback
|
|
22
|
+
as `calltimeout._wire.RemoteTraceback`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- A `mode` that is not a `str` raises `TypeError` (it raised `ValueError`); an unknown
|
|
27
|
+
mode name still raises `ValueError`, and the message lists the three valid names. A
|
|
28
|
+
`str` subclass is read as the plain name.
|
|
29
|
+
- `CallTimeout` and `CallCrashed` are shown and pickled as `calltimeout.CallTimeout` and
|
|
30
|
+
`calltimeout.CallCrashed`, so a pickle never names a private module, and they keep
|
|
31
|
+
extra attributes (such as notes) through pickle and copy. `run` and `with_timeout` are
|
|
32
|
+
shown and pickled as `calltimeout.run` and `calltimeout.with_timeout` too (a stored
|
|
33
|
+
`functools.partial(run, ...)` named `calltimeout._api`).
|
|
34
|
+
- `CallCrashed.exitcode` is typed `int`: it was `int | None`, though `run` raises
|
|
35
|
+
`CallCrashed` only once the child has exited, with its code. A `RemoteTraceback` built
|
|
36
|
+
with a text that is not a `str` shows it bounded; its `str()` raised `TypeError`.
|
|
37
|
+
- Generator functions and async generator functions raise `TypeError` in `run` and
|
|
38
|
+
`with_timeout` (an async one with the pointer to `asyncio`), and so does a call that
|
|
39
|
+
returns an async generator. They were accepted: the call returned the generator at
|
|
40
|
+
once, and its body then ran with no time limit as it was iterated.
|
|
41
|
+
- Thread mode: a call that ends after the budget ran out is a timeout, even when the
|
|
42
|
+
caller notices only later. A call holding the GIL could end after its deadline and
|
|
43
|
+
before the caller ran again, and its value was returned (a 1 ms budget gave values
|
|
44
|
+
from calls that ran 3 to 10 ms).
|
|
45
|
+
- A real-number timeout whose conversion to `float` fails raises `TypeError` (the
|
|
46
|
+
conversion's own error is not retained as a cause or context).
|
|
47
|
+
- A negative timeout too large for a `float` (`-10**400`) raises "timeout must not be
|
|
48
|
+
negative"; it was reported as above `threading.TIMEOUT_MAX`.
|
|
49
|
+
- Tiny negative real-number timeouts are rejected before float conversion can round
|
|
50
|
+
them to zero, including `Fraction(-1, 10**400)` in both public entry points.
|
|
51
|
+
- Invalid mode messages list valid names without quoting the input. Timeout conversion
|
|
52
|
+
and keyword mapping failures use fixed, value-free diagnostics and retain no original
|
|
53
|
+
cause or context. Ordinary mapping-access failures now raise `TypeError`.
|
|
54
|
+
- Validation checks actual types without consulting overridable `__class__` properties,
|
|
55
|
+
while accepting genuine subclasses and registered real-number and mapping types.
|
|
56
|
+
- `kwargs` is read once into a plain `dict` before the budget starts. Keys are validated
|
|
57
|
+
and string subclasses normalized before insertion, so overridden hashes and equality
|
|
58
|
+
cannot cause quadratic insertion work. Mapping views cannot bypass key validation.
|
|
59
|
+
- Existing built-in keyword dictionaries are copied from their stored key/value pairs,
|
|
60
|
+
avoiding quadratic source lookups for colliding string-subclass keys, even with a
|
|
61
|
+
zero timeout. Keys are still normalized and values preserved.
|
|
62
|
+
- Dictionary subclasses with inherited read behavior and dictionary-backed mapping
|
|
63
|
+
proxies also avoid repeated source lookups for colliding keyword keys. Custom key
|
|
64
|
+
selection and value lookup retain their semantics, including through proxies.
|
|
65
|
+
- An instance's `keys()` selection is preserved when its dictionary subclass aliases
|
|
66
|
+
a parent's `__dict__` descriptor, including through nested mapping proxies. Such
|
|
67
|
+
aliases previously caused excluded keywords to be passed to the callable.
|
|
68
|
+
- Aliased instance namespace descriptors no longer force repeated keyword lookups
|
|
69
|
+
when dictionary reads are unmodified. Colliding string-subclass keys use stored
|
|
70
|
+
pairs directly, while instance `keys()` selection remains respected.
|
|
71
|
+
- Validation and transport errors do not copy arbitrary exception text or notes.
|
|
72
|
+
Callable and type names in other diagnostics remain bounded to 60 characters; a
|
|
73
|
+
stand-in for an unpicklable callable exception keeps at most 300 characters of its
|
|
74
|
+
message. Ordinary callable exceptions and remote tracebacks retain their text.
|
|
75
|
+
- Process mode: the budget also covers pickling the call and starting the child, and a
|
|
76
|
+
call whose pickling used it up is never started. A call that pickles in the caller but
|
|
77
|
+
cannot be loaded in the child (a function defined under the main guard, in
|
|
78
|
+
`python -c` or a notebook, or in a module the child cannot import; an argument whose
|
|
79
|
+
rebuild raises) raises `pickle.UnpicklingError` ("the call cannot be loaded in the
|
|
80
|
+
child process", with a safe category and library traceback chained as the cause)
|
|
81
|
+
instead of a crash with exit code 1. A child that dies while
|
|
82
|
+
sending its result raises `CallCrashed`, and waiting for a crashed child's exit code
|
|
83
|
+
never runs past the budget. A result that cannot be pickled raises
|
|
84
|
+
`pickle.PicklingError` with the child's library traceback, and a result that cannot
|
|
85
|
+
be rebuilt raises `pickle.UnpicklingError`. Both use value-free transport causes.
|
|
86
|
+
- Process mode: the budget also covers receiving the result. On POSIX a long result is
|
|
87
|
+
written in two parts, and a child that stalled between or during them (stopped, or
|
|
88
|
+
blocked in a profile hook) blocked `run` for good; it is now stopped at the deadline
|
|
89
|
+
(terminated, and killed 1 second later) and the call raises `CallTimeout`.
|
|
90
|
+
- Process mode: a child error whose pickle rebuilds as something other than an exception
|
|
91
|
+
arrives as the `RuntimeError` stand-in naming its type; `run` raised an
|
|
92
|
+
`AttributeError` or `TypeError` from inside calltimeout, and the child's error was lost.
|
|
93
|
+
- Process mode: a call whose function or arguments cannot be pickled raises
|
|
94
|
+
`pickle.PicklingError` before any process starts, chained from a fresh standard
|
|
95
|
+
exception with fixed text. The cause preserves only a safe standard error category;
|
|
96
|
+
custom errors use `RuntimeError`. Original text, notes, frames and exception chains
|
|
97
|
+
are discarded without mutating the original error. This also applies to call loading,
|
|
98
|
+
result sending and result rebuilding failures. A call
|
|
99
|
+
holding objects that `multiprocessing` only pickles while a child starts (locks,
|
|
100
|
+
queues, shared values), any number of them, is first rehearsed as the start will
|
|
101
|
+
pickle it, so one whose contents cannot pickle (a `Value` guarded by a
|
|
102
|
+
`threading.Lock`) fails the same way, and one that still fails when the start pickles
|
|
103
|
+
it again (an object that pickles only once, a manager proxy whose pickling raises)
|
|
104
|
+
raises the same error, its child
|
|
105
|
+
stopped before it runs anything; a `KeyboardInterrupt` or `SystemExit` raised there is
|
|
106
|
+
raised as it is, once that child is stopped, instead of escaping half way through the
|
|
107
|
+
start, which on Windows could break later calls passing pipe ends. Under forkserver, a
|
|
108
|
+
call holding more than 248 descriptors (pipe ends, sockets, two per queue, one per block
|
|
109
|
+
of shared memory behind `Value` and `Array` objects, all named in the message), more than
|
|
110
|
+
one message to the fork server carries, raises `pickle.PicklingError` before any
|
|
111
|
+
process starts instead of an undocumented `OSError` or `ValueError` from the start. An
|
|
112
|
+
argument whose pickling raised `NotImplementedError` (a pool), `RecursionError` or a
|
|
113
|
+
`RuntimeError` used to fail after the child was created, and on Windows it then broke
|
|
114
|
+
the next process-mode call.
|
|
115
|
+
- Process mode: the call's function and arguments are pickled once, before the start;
|
|
116
|
+
only its pipe ends, sockets and manager proxies, or the whole call when it holds locks
|
|
117
|
+
or queues, are pickled by the start itself. Pipe ends and sockets are handed to the
|
|
118
|
+
child by that start (POSIX passes the descriptors with it, Windows duplicates them
|
|
119
|
+
straight into the child), so none is left open in the caller, even when the budget
|
|
120
|
+
runs out while pickling the call or before the child has loaded it. Such a call left a
|
|
121
|
+
duplicate of each one open in the caller (on Windows also one of the result pipe,
|
|
122
|
+
whatever the call held; under forkserver the caller also printed `ConnectionResetError`
|
|
123
|
+
tracebacks), so the reader of that pipe never saw its end and handles ran out. A
|
|
124
|
+
manager proxy goes with its manager's authkey, as `multiprocessing` sends it: one of a
|
|
125
|
+
manager with its own authkey failed in the child with `AuthenticationError` unless the
|
|
126
|
+
call also held a lock. An argument whose `__class__` raises is sent as it pickles.
|
|
127
|
+
- Process mode frees the child's handles as soon as the call ends. A call that raised
|
|
128
|
+
kept its child's process object, and its two descriptors or handles, alive in a
|
|
129
|
+
reference cycle until the next garbage collection, so a loop of failing calls could
|
|
130
|
+
run out of file descriptors (`OSError: [Errno 24] Too many open files`).
|
|
131
|
+
- A generic process-mode `run` preserves a supplied callable's process timeout
|
|
132
|
+
decorator, including its independent deadline, fallback and zero-budget no-call rule.
|
|
133
|
+
Wrapper bypass is authorized once only by a child call initiated by that decorator.
|
|
134
|
+
- `with_timeout` in process mode sends a callable that pickles by value (a
|
|
135
|
+
`functools.partial`, a callable object) as it is, as `run` does; it failed with
|
|
136
|
+
`AttributeError` under spawn and forkserver.
|
|
137
|
+
- `with_timeout` in process mode works under a `classmethod` under spawn and forkserver
|
|
138
|
+
(the wrapper is sent through its class); it raised `pickle.PicklingError`.
|
|
139
|
+
- Process mode: a child error whose type's own attributes raise (its traceback cannot be
|
|
140
|
+
formatted, its type cannot be named or pickled) arrives as a `RuntimeError` quoting its
|
|
141
|
+
text; the child crashed with exit code 1 and `run` raised `CallCrashed`.
|
|
142
|
+
- A callable whose `__getattr__` raises something other than `AttributeError` is run
|
|
143
|
+
like any other; that error escaped from the coroutine check in `run` and
|
|
144
|
+
`with_timeout`.
|
|
145
|
+
- `timeout` is annotated `float | numbers.Real` in `run` and `with_timeout`, so type
|
|
146
|
+
checkers accept a `Fraction`, which already worked.
|
|
147
|
+
- Signal mode: an exception raised with an explicit cause while handling the timeout
|
|
148
|
+
(`raise E from other`) counts as the timeout, like `raise E` and `raise E from None`;
|
|
149
|
+
it escaped as `E`.
|
|
150
|
+
- Signal mode: no timer is armed for less than 1 ms, so a timer tick less than 1 ms
|
|
151
|
+
before the deadline ends the call there, a timeout under 1 ms ends it at once without
|
|
152
|
+
starting it (like a zero timeout), and an enclosing timer due sooner than 1 ms has its
|
|
153
|
+
handler run up to 1 ms late. A timer that short could fire before the interrupted
|
|
154
|
+
system call blocked again, so a blocking call overran by its whole length.
|
|
155
|
+
- Signal mode: a timer tick that lands after the call has finished can no longer re-arm
|
|
156
|
+
the timer.
|
|
157
|
+
- Signal mode: an interval timer that was already running keeps its own schedule. Each
|
|
158
|
+
tick due during a call was counted from when it ran instead of when it was due, so up
|
|
159
|
+
to 1 ms of lateness per tick added up and ticks were lost (400 calls of 5 ms under a
|
|
160
|
+
10 ms timer: about 10 ticks lost, the last 120 to 130 ms late). On Linux a timer read
|
|
161
|
+
in the instant between a tick and its delivery shows no time left, and the call then
|
|
162
|
+
left it disarmed for good; it is now kept.
|
|
163
|
+
- Signal mode: a `SIGALRM` taken as a call starts, before its timer is set (a tick of an
|
|
164
|
+
enclosing timer already on its way, also from an enclosing signal-mode call), goes to
|
|
165
|
+
the handler that was there before. It reached the call's own handler half set up: that
|
|
166
|
+
raised `NameError` and stayed installed for good (later code, even a plain
|
|
167
|
+
`time.sleep`, raised `NameError`), or it took the tick as its own and dropped it, so an
|
|
168
|
+
enclosing signal-mode deadline could be lost and the call run forever. A `SIGALRM`
|
|
169
|
+
that is not the call's own timer's (sent with `kill`) also goes to that handler.
|
|
170
|
+
- Signal mode: an enclosing timer keeps its schedule over any number of calls. The call
|
|
171
|
+
reads and replaces it in one step, gives back the exact schedule it took, and waits
|
|
172
|
+
for a tick due within 0.2 ms before taking the timer (Linux 6.6 can drop a tick whose
|
|
173
|
+
timer is replaced in the instant it fires). Each call moved the timer by the time spent
|
|
174
|
+
reading and setting it, so over many calls a one-shot timer fired up to 18 ms early
|
|
175
|
+
(about 90,000 quick calls) and an interval timer drifted by milliseconds, lost ticks
|
|
176
|
+
(39 of 300 under calls that each timed out) and delivered some twice.
|
|
177
|
+
- Every mode starts its budget after argument validation and keyword copying. Those
|
|
178
|
+
caller-side hooks are not interrupted. Thread mode includes starting the thread.
|
|
179
|
+
- The coroutine error names `asyncio.wait_for()` as well as `asyncio.timeout()`, which
|
|
180
|
+
only exists from Python 3.11.
|
|
181
|
+
- Every public name has a docstring with an example that runs as a doctest, `Mode` in
|
|
182
|
+
the package docstring (typing caches `Literal` aliases, so a docstring of its own would
|
|
183
|
+
relabel the caller's identical aliases), and a new GUIDE covers each mode, every
|
|
184
|
+
error and use from async code. The process-mode examples guard their entry point and
|
|
185
|
+
are tested as scripts.
|
|
186
|
+
- `Development Status :: 5 - Production/Stable`.
|
|
187
|
+
|
|
188
|
+
## [0.2.0] - 2026-10-12
|
|
189
|
+
|
|
190
|
+
Published on PyPI on 2026-10-31; no git tag (commit 9b6975c on `main`).
|
|
191
|
+
|
|
192
|
+
### Added
|
|
193
|
+
|
|
194
|
+
- `mode="signal"`: a SIGALRM timer raises `CallTimeout` inside the call, which stops
|
|
195
|
+
pure Python code and blocking system calls in place. POSIX main thread only; other
|
|
196
|
+
threads get `RuntimeError` and Windows `NotImplementedError`. Timers already armed
|
|
197
|
+
keep their schedule (a raising outer handler runs once), so calls can be nested, and
|
|
198
|
+
a call that swallows the timeout still ends in `CallTimeout` when it returns, while
|
|
199
|
+
`KeyboardInterrupt`, `SystemExit` and unrelated errors propagate.
|
|
200
|
+
- `with_timeout(timeout, *, mode, default)`: a decorator that keeps the wrapped
|
|
201
|
+
function's signature for type checkers (`ParamSpec`), in every mode including process
|
|
202
|
+
mode for functions the child can import by name (decorated in place or assigned to a
|
|
203
|
+
new name).
|
|
204
|
+
- `CallCrashed`: raised in process mode when the child exits without a result, with its
|
|
205
|
+
exit code, as soon as it is gone.
|
|
206
|
+
|
|
207
|
+
### Changed
|
|
208
|
+
|
|
209
|
+
- Process mode waits on the result pipe and the process together, pickles results and
|
|
210
|
+
errors in the child (an unpicklable result raises `pickle.PicklingError` at once, an
|
|
211
|
+
unpicklable exception arrives as a `RuntimeError` stand-in), chains the child's
|
|
212
|
+
traceback as the cause, kills a child that ignores SIGTERM after 1 second, and stops
|
|
213
|
+
the child whenever the caller leaves early, including on `KeyboardInterrupt`.
|
|
214
|
+
- Process mode notices a crashed child even while a grandchild it forked keeps the
|
|
215
|
+
pipes open.
|
|
216
|
+
- Process mode pickles the call before starting the child, so a failed start cannot
|
|
217
|
+
leave the next call reading from a stale pipe on Windows; locks and queues, which
|
|
218
|
+
only pickle during start, are still passed through.
|
|
219
|
+
- A finished child gets up to 1 second to exit on its own before it is terminated; a
|
|
220
|
+
result the parent cannot unpickle raises `pickle.UnpicklingError`.
|
|
221
|
+
- `default` is returned only for the call's own timeout; a `CallTimeout` from a nested
|
|
222
|
+
call propagates.
|
|
223
|
+
- Thread mode runs the call in a copy of the caller's context variables and names the
|
|
224
|
+
thread `calltimeout:<function>`.
|
|
225
|
+
- `timeout` must be a real number (not a bool), not NaN and at most
|
|
226
|
+
`threading.TIMEOUT_MAX`; a zero timeout raises without starting the call. Coroutine
|
|
227
|
+
functions and coroutine results are rejected with a pointer to `asyncio.timeout`.
|
|
228
|
+
`kwargs` must be a mapping, and `run` and `with_timeout` reject modes the same way.
|
|
229
|
+
- `CallTimeout` keeps its `timeout` through pickle and copy, and reports the validated
|
|
230
|
+
number of seconds.
|
|
231
|
+
|
|
232
|
+
## [0.1.0] - 2026-10-11
|
|
233
|
+
|
|
234
|
+
Never published or tagged (commit a3b7c15).
|
|
235
|
+
|
|
236
|
+
- First version.
|
|
237
|
+
- `run(fn, timeout, *, args, kwargs, mode, default)` runs a callable under a
|
|
238
|
+
wall-clock timeout and returns its typed result.
|
|
239
|
+
- Raises `CallTimeout` (a subclass of `TimeoutError`) on overrun, or returns
|
|
240
|
+
`default` when one is given.
|
|
241
|
+
- `mode="thread"` (default) stops waiting but cannot kill a stuck,
|
|
242
|
+
non-cooperative call: the thread lingers until it returns or the process exits.
|
|
243
|
+
- `mode="process"` truly terminates the call on overrun, at the cost of needing
|
|
244
|
+
picklable arguments and result.
|
|
@@ -11,8 +11,10 @@ pip install -r requirements-dev.txt
|
|
|
11
11
|
## Before you open a pull request
|
|
12
12
|
|
|
13
13
|
- Keep public API in `src/calltimeout/__init__.py`; internals live in `_`-prefixed modules.
|
|
14
|
-
- Add a test for the behaviour you change.
|
|
15
|
-
|
|
14
|
+
- Add a test for the behaviour you change. The library's promise is about real time, so
|
|
15
|
+
the suite measures real budgets with real threads, child processes and timers. Tests
|
|
16
|
+
use no network (sockets only as local pairs handed to a child), and write files only
|
|
17
|
+
under pytest's `tmp_path`.
|
|
16
18
|
- Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
|
|
17
19
|
- One logical change per commit, imperative subject line.
|
|
18
20
|
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# calltimeout guide
|
|
2
|
+
|
|
3
|
+
The [README](https://github.com/JaviLianes8/calltimeout/blob/main/README.md) shows the
|
|
4
|
+
basics. This guide covers each mechanism in depth, the errors, nesting and async code.
|
|
5
|
+
Every example here is run as a script by the tests.
|
|
6
|
+
|
|
7
|
+
## The call
|
|
8
|
+
|
|
9
|
+
`run(fn, timeout, *, args=(), kwargs=None, mode="thread", default=...)` calls
|
|
10
|
+
`fn(*args, **kwargs)` and returns its result. `timeout` is a number of seconds: an `int`,
|
|
11
|
+
a `float` or another real number such as a `Fraction`, not a `bool`, not negative, not
|
|
12
|
+
NaN and at most `threading.TIMEOUT_MAX` (so not infinite). A zero timeout raises at once
|
|
13
|
+
without starting the call (in signal mode, so does any timeout under 1 ms). These
|
|
14
|
+
checks, and the ones on `fn`, `args`, `kwargs` and `mode`, happen before anything
|
|
15
|
+
starts, in every mode. `mode` is one of the three names of `Mode`,
|
|
16
|
+
`Literal["thread", "process", "signal"]`, which you can use to annotate your own
|
|
17
|
+
wrappers. `kwargs` is copied into a plain `dict` in the caller before the call starts, so
|
|
18
|
+
a mapping changed later does not affect the call. String-subclass keys are normalized
|
|
19
|
+
before insertion, so dictionary insertion uses base-string hashing and equality.
|
|
20
|
+
Ordinary failures while reading the mapping raise `TypeError`.
|
|
21
|
+
|
|
22
|
+
Validation and keyword copying precede the budget in every mode. User-defined numeric
|
|
23
|
+
conversion and mapping hooks run in the caller and are not interrupted. The budget
|
|
24
|
+
starts after those checks; thread startup is included.
|
|
25
|
+
|
|
26
|
+
The budget covers the call itself, so `fn` must do its work when called. A coroutine
|
|
27
|
+
function or an async generator function raises `TypeError` with a pointer to `asyncio`,
|
|
28
|
+
and so does a call that returns a coroutine or an async generator. A generator function
|
|
29
|
+
raises `TypeError` too: calling it only creates the generator, and its body would run as
|
|
30
|
+
it is iterated, after `run` returned, outside the budget. Run a function that consumes it
|
|
31
|
+
instead. A callable that returns some other lazy object (`map`, a file read later) is
|
|
32
|
+
timed only until it returns that object.
|
|
33
|
+
|
|
34
|
+
When the budget runs out, `run` raises `CallTimeout`, or returns `default` if one was
|
|
35
|
+
given (`None` is a valid default). An exception raised by the call reaches the caller. A
|
|
36
|
+
`CallTimeout` from a nested call inside `fn` is not this call's timeout: it propagates
|
|
37
|
+
even when this call has a default.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import time
|
|
41
|
+
|
|
42
|
+
from calltimeout import CallTimeout, run
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def inner() -> None:
|
|
46
|
+
return run(time.sleep, 0.05, args=(10,))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
run(inner, 5.0, default="outer default")
|
|
51
|
+
except CallTimeout as exc:
|
|
52
|
+
print(f"the inner {exc.timeout} second budget ran out") # the inner 0.05 second budget ran out
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Thread mode
|
|
56
|
+
|
|
57
|
+
The default. The call runs in a daemon thread named `calltimeout:<function>`, in a copy
|
|
58
|
+
of the caller's context variables (as `asyncio.to_thread` does), and `run` stops waiting
|
|
59
|
+
when the budget runs out. Python cannot stop a running thread, so a call that ignores the
|
|
60
|
+
clock keeps running after `run` has raised, until it returns or the process exits. Daemon
|
|
61
|
+
threads are stopped abruptly at shutdown, so do not leave one holding a file or a
|
|
62
|
+
transaction. Use thread mode for calls that end on their own, eventually.
|
|
63
|
+
|
|
64
|
+
Thread and process mode measure the budget with `time.monotonic()`, whose resolution
|
|
65
|
+
`time.get_clock_info("monotonic")` reports. On Windows it is `GetTickCount64()`, about
|
|
66
|
+
15.6 ms, up to Python 3.12, and `QueryPerformanceCounter()` from 3.13. In thread mode the
|
|
67
|
+
GIL limits precision more than the clock: a call running Python code holds it, and lets
|
|
68
|
+
the waiting caller run only every switch interval (`sys.getswitchinterval()`, 5 ms by
|
|
69
|
+
default). So `CallTimeout` can come up to about two intervals after the budget ran out. A
|
|
70
|
+
call that ends after the budget ran out, even before the caller noticed, is a timeout:
|
|
71
|
+
what it returned or raised then is dropped, as in process mode. Free-threaded builds have
|
|
72
|
+
no GIL and no such delay.
|
|
73
|
+
|
|
74
|
+
## Process mode
|
|
75
|
+
|
|
76
|
+
The call runs in a child process that is terminated when the budget runs out (SIGTERM on
|
|
77
|
+
POSIX, `TerminateProcess` on Windows), and killed if it is still alive 1 second later.
|
|
78
|
+
The budget covers pickling the call, starting the child and receiving its result.
|
|
79
|
+
Pickling runs in the caller and is not interrupted: a slow `__reduce__` overruns the
|
|
80
|
+
budget by as long as it takes (and one that never returns blocks `run`), but a call whose
|
|
81
|
+
pickling used up the budget is never started. A result still arriving when the budget
|
|
82
|
+
runs out (a huge one, or one whose child stalled half way through sending it) ends the
|
|
83
|
+
call like any overrun: the child is stopped and `CallTimeout` raised. Rebuilding the
|
|
84
|
+
result also runs in the caller, once it has arrived, and is neither counted against the
|
|
85
|
+
budget nor interrupted: a result whose rebuild is slow delays `run` by as long (and one
|
|
86
|
+
that never returns blocks it).
|
|
87
|
+
|
|
88
|
+
- `fn`, its arguments and its result cross the process boundary, so they must pickle
|
|
89
|
+
(under fork only the result is pickled, see below).
|
|
90
|
+
Targets must be importable: define them at module level, and guard the entry point of
|
|
91
|
+
a script with `if __name__ == "__main__":` as `multiprocessing` requires. Locks, queues,
|
|
92
|
+
pipe ends, sockets, manager proxies and other `multiprocessing` objects can be passed as
|
|
93
|
+
arguments, any number of them, except under forkserver (below). Pipe ends and sockets
|
|
94
|
+
are handed over by the start itself (on Windows, duplicated straight into the child), so
|
|
95
|
+
none is left open in the caller, even when the budget runs out before the child has
|
|
96
|
+
loaded the call.
|
|
97
|
+
- A forkserver start sends every descriptor of the call to the fork server in one
|
|
98
|
+
message, which Linux caps. So under forkserver a call carries at most 248 descriptors:
|
|
99
|
+
one per pipe end or socket, two per queue, and one per block of the shared memory that
|
|
100
|
+
`Value` and `Array` objects live in (many share one). A call holding more raises
|
|
101
|
+
`pickle.PicklingError` before any process starts.
|
|
102
|
+
- Under spawn and forkserver a call that cannot be pickled (a lambda, a thread lock, a
|
|
103
|
+
pool, an argument nested too deeply) raises `pickle.PicklingError` before any process
|
|
104
|
+
starts, chained from a fresh exception containing only a safe category and fixed text.
|
|
105
|
+
A call holding locks or queues is rehearsed as the start will pickle it, so one whose
|
|
106
|
+
contents cannot pickle (a `Value` guarded by a `threading.Lock`) fails the same way.
|
|
107
|
+
One that still fails when the start pickles it again (an object that pickles only once,
|
|
108
|
+
a manager proxy whose pickling raises) raises the same error, and the child created for
|
|
109
|
+
it is stopped before it runs anything. A `KeyboardInterrupt` or other exception that is
|
|
110
|
+
not an `Exception`, raised there, is raised as it is once that child is stopped. Under
|
|
111
|
+
fork nothing is pickled on the way in.
|
|
112
|
+
- Under spawn and forkserver a call that pickles in the caller but cannot be loaded in
|
|
113
|
+
the child (a function defined under `if __name__ == "__main__":`, in `python -c` or a
|
|
114
|
+
notebook, or in a module the child cannot import; an argument whose rebuild raises)
|
|
115
|
+
raises `pickle.UnpicklingError` with a safe error category and the child's
|
|
116
|
+
library traceback chained as its cause.
|
|
117
|
+
- The child process is named `calltimeout:<function>`, like the thread of thread mode.
|
|
118
|
+
- A child that exits without a result (a crash, `os._exit`, the out-of-memory killer)
|
|
119
|
+
raises `CallCrashed` with its exit code as soon as it is gone, not a timeout, and never
|
|
120
|
+
the default. The code is the one `multiprocessing` reports: the value given to
|
|
121
|
+
`os._exit`, or `-N` for a child killed by signal `N` on POSIX.
|
|
122
|
+
- An exception from the child arrives with the child's traceback chained as its cause:
|
|
123
|
+
a `RemoteTraceback`, whose text (also its `text` attribute) is that traceback. One that
|
|
124
|
+
cannot be pickled, or that would arrive as something other than an exception, arrives
|
|
125
|
+
as a `RuntimeError` naming its type and quoting its text.
|
|
126
|
+
`sys.exit()` in the call raises `SystemExit` in the caller like any other exception.
|
|
127
|
+
- A result that cannot be pickled raises `pickle.PicklingError` at once, and one the
|
|
128
|
+
parent cannot rebuild raises `pickle.UnpicklingError`.
|
|
129
|
+
- After the result arrives the child gets up to 1 second to shut down on its own before
|
|
130
|
+
it is terminated: its non-daemon threads finish, and its atexit handlers run where
|
|
131
|
+
`multiprocessing` runs them in a child (under spawn, and under fork and forkserver from
|
|
132
|
+
Python 3.13).
|
|
133
|
+
- Processes started by the call itself are not stopped with it.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import os
|
|
137
|
+
|
|
138
|
+
from calltimeout import CallCrashed, run
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main() -> None:
|
|
142
|
+
try:
|
|
143
|
+
run(os._exit, 30.0, args=(3,), mode="process")
|
|
144
|
+
except CallCrashed as exc:
|
|
145
|
+
print(f"exit code {exc.exitcode}") # exit code 3
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
run(int, 30.0, args=("ten",), mode="process")
|
|
149
|
+
except ValueError as exc:
|
|
150
|
+
print(type(exc.__cause__).__name__) # RemoteTraceback
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
main()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Signal mode
|
|
158
|
+
|
|
159
|
+
A `SIGALRM` timer raises `CallTimeout` inside the call, at the line it has reached. Since
|
|
160
|
+
PEP 475 a signal handler that raises also stops blocking system calls such as
|
|
161
|
+
`time.sleep` or a socket read. It needs no thread and no pickling, but only works in the
|
|
162
|
+
main thread on POSIX: in another thread it raises `RuntimeError`, and on Windows
|
|
163
|
+
`NotImplementedError`. Some C code checks for signals as it runs (a regular expression
|
|
164
|
+
match does), but a long call inside C that does not (sorting a huge list, `sum()` over a
|
|
165
|
+
long `range`) is only interrupted once it returns to Python.
|
|
166
|
+
|
|
167
|
+
- A call that catches the timeout and carries on still ends in `CallTimeout`, once it
|
|
168
|
+
returns. The timer fires once, so a loop that keeps catching `OSError` (which
|
|
169
|
+
`TimeoutError` is) runs on until it gives up.
|
|
170
|
+
- An exception raised while handling the timeout counts as the timeout, also one raised
|
|
171
|
+
`from` another cause; `KeyboardInterrupt`, `SystemExit` and unrelated errors propagate.
|
|
172
|
+
- No timer is armed for less than 1 ms: a tick due that soon could land before an
|
|
173
|
+
interrupted system call blocks again and be lost, letting a blocking call run to its
|
|
174
|
+
end. So a tick that arrives less than 1 ms before the deadline (an enclosing timer, a
|
|
175
|
+
stray `SIGALRM`) ends the call there, and a timeout under 1 ms ends it at once without
|
|
176
|
+
starting it.
|
|
177
|
+
- Timers already armed with `setitimer` or `alarm` keep their schedule and their handler
|
|
178
|
+
still runs when due (up to 1 ms late, by the rule above), so signal-mode calls can be
|
|
179
|
+
nested. An interval timer's ticks stay on its own schedule, over any number of calls:
|
|
180
|
+
none is lost or delivered twice, and the lateness of one tick does not carry over to
|
|
181
|
+
the next. A call that starts within 0.2 ms of such a tick lets it through first (Linux
|
|
182
|
+
can lose a tick whose timer is replaced in the instant it fires), which counts against
|
|
183
|
+
its budget. A `SIGALRM` that is not the call's own (a tick already on its way as the call
|
|
184
|
+
starts, or one sent with `kill`) goes to the handler that was there before. A call that
|
|
185
|
+
resets `ITIMER_REAL` itself, or installs
|
|
186
|
+
its own `SIGALRM` handler (`SIG_IGN` included, as an alarm-based helper may), cancels
|
|
187
|
+
the deadline: its result is then returned even if the budget ran out. A `SIGALRM`
|
|
188
|
+
handler installed outside Python is refused, because it could not be restored.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import sys
|
|
192
|
+
import time
|
|
193
|
+
|
|
194
|
+
from calltimeout import CallTimeout, run
|
|
195
|
+
|
|
196
|
+
if sys.platform != "win32":
|
|
197
|
+
try:
|
|
198
|
+
run(time.sleep, 0.1, args=(60,), mode="signal")
|
|
199
|
+
except CallTimeout as exc:
|
|
200
|
+
# The timeout was raised inside time.sleep and is chained as the cause.
|
|
201
|
+
print(f"interrupted: {type(exc.__cause__).__name__}") # interrupted: CallTimeout
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## The decorator
|
|
205
|
+
|
|
206
|
+
`with_timeout(timeout, *, mode="thread", default=...)` makes every call of the decorated
|
|
207
|
+
function go through `run`. Type checkers keep checking each call against the original
|
|
208
|
+
parameters, and the result type becomes `T | D` with a default. The settings are checked
|
|
209
|
+
when the decorator is made, and the function when it is applied. Signal mode's platform
|
|
210
|
+
and main-thread checks run at each call instead, since a decorated function can be called
|
|
211
|
+
from any thread: on Windows the decorator is made, and each call raises
|
|
212
|
+
`NotImplementedError`.
|
|
213
|
+
|
|
214
|
+
In process mode under spawn or forkserver the child imports the function by name, so
|
|
215
|
+
either the decorated function or the original must be reachable under its module and
|
|
216
|
+
name: a plain `@with_timeout` at module level, or `fast = with_timeout(...)(slow)`. A
|
|
217
|
+
method of a module-level class works too, as an instance method, a `staticmethod` or a
|
|
218
|
+
`classmethod` (with `with_timeout` applied first, under the method decorator). A
|
|
219
|
+
callable that pickles by value (a `functools.partial`, a callable object) is sent as it is.
|
|
220
|
+
Each call runs in its own child, also a call made from inside the function's own child
|
|
221
|
+
(a recursive function gets a new child, with its own budget, for every level).
|
|
222
|
+
Passing a decorated function to `run(..., mode="process")` preserves the decorator's
|
|
223
|
+
independent timeout, fallback and zero-budget guarantee. Only the child started by
|
|
224
|
+
that decorator itself may bypass its wrapper once to execute the body.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
import time
|
|
228
|
+
|
|
229
|
+
from calltimeout import with_timeout
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@with_timeout(0.05, default=None)
|
|
233
|
+
def lookup(key: str) -> str:
|
|
234
|
+
time.sleep(1.0)
|
|
235
|
+
return key.upper()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
print(lookup("id")) # None
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Errors
|
|
242
|
+
|
|
243
|
+
| Raised | When |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `CallTimeout` (a `TimeoutError`) | The budget ran out and no default was given |
|
|
246
|
+
| `CallCrashed` (a `RuntimeError`) | Process mode: the child exited without a result |
|
|
247
|
+
| `TypeError` | `timeout` is not a real number (or one that fails to convert to `float`), `fn` is not callable or is a coroutine, async generator or generator function, `args` is not a tuple, `kwargs` is not a readable mapping of `str` keys, `mode` is not a `str`, or the call returned a coroutine or an async generator |
|
|
248
|
+
| `ValueError` | `timeout` is negative, NaN or above `threading.TIMEOUT_MAX`, or `mode` is not one of the three names |
|
|
249
|
+
| `RuntimeError` | Signal mode outside the main thread, or with a `SIGALRM` handler set outside Python; process mode: the call raised an exception that cannot be pickled, or that would arrive as something other than an exception (raised in its place, naming its type) |
|
|
250
|
+
| `NotImplementedError` | Signal mode on Windows |
|
|
251
|
+
| `pickle.PicklingError` | Process mode: `fn` or an argument cannot be sent to the child (raised before the call runs: before any process starts, or once the child is stopped for a call that fails only when the start pickles it), a forkserver call holds more than 248 descriptors, or the result cannot be sent back |
|
|
252
|
+
| `pickle.UnpicklingError` | Process mode: the call cannot be loaded in the child (it pickles in the caller, but the child cannot import its function or rebuild an argument), or the result cannot be rebuilt in the caller |
|
|
253
|
+
|
|
254
|
+
Both exceptions keep their attribute through pickle and copy, so they can cross process
|
|
255
|
+
boundaries themselves. Invalid mode errors list the accepted names without echoing the
|
|
256
|
+
supplied value. Failed timeout conversions and mapping reads produce fixed `TypeError`
|
|
257
|
+
messages without retaining the original exception as a cause or context.
|
|
258
|
+
|
|
259
|
+
Transport diagnostics identify the operation and a safe standard exception category.
|
|
260
|
+
Their causes contain only fixed text: original messages, notes, traceback frames and
|
|
261
|
+
exception chains are discarded, including errors from pickling or rebuilding either
|
|
262
|
+
the call or its result. Custom exception types become `RuntimeError` causes. Original
|
|
263
|
+
exceptions are not modified. Truncation alone would still expose short secrets.
|
|
264
|
+
|
|
265
|
+
These protections apply to library-generated validation and transport diagnostics.
|
|
266
|
+
Exceptions deliberately raised by the callable retain their own messages and, in
|
|
267
|
+
process mode, their remote traceback; that text may contain caller data. Callable and
|
|
268
|
+
type names used in other diagnostics are bounded to 60 characters, and the stand-in
|
|
269
|
+
for an unpicklable callable exception keeps up to 300 characters of its message.
|
|
270
|
+
|
|
271
|
+
## In async code
|
|
272
|
+
|
|
273
|
+
Coroutines already have `asyncio.timeout()` (Python 3.11+) and `asyncio.wait_for()`, and
|
|
274
|
+
`run` refuses coroutine and async generator functions with a pointer to them. For a blocking call, run `run`
|
|
275
|
+
in a worker thread so the event loop stays free; process mode then really stops the call:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
import asyncio
|
|
279
|
+
import time
|
|
280
|
+
|
|
281
|
+
from calltimeout import run
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def main() -> str | None:
|
|
285
|
+
return await asyncio.to_thread(
|
|
286
|
+
run, time.sleep, 0.5, args=(60,), mode="process", default="stopped"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
print(asyncio.run(main())) # stopped
|
|
292
|
+
```
|