Skip to content

Commit f5fa3c4

Browse files
authored
Merge pull request #49 from pomponchik/develop
0.0.35
2 parents 80957c5 + 94216f0 commit f5fa3c4

25 files changed

Lines changed: 250 additions & 52 deletions

.github/ISSUE_TEMPLATE/documentation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ assignees: pomponchik
88

99
## It's cool that you're here!
1010

11-
Documentation is an important part of the project, we strive to make it high-quality and keep it up to date. Please adjust this template by outlining your proposal.
11+
Documentation is an important part of the project; we strive to make it high-quality and keep it up to date. Please adjust this template by outlining your proposal.
1212

1313

1414
## Type of action
@@ -18,7 +18,7 @@ What do you want to do: remove something, add it, or change it?
1818

1919
## Where?
2020

21-
Specify which part of the documentation you want to make a change to? For example, the name of an existing documentation section or the line number in a file `README.md`.
21+
Specify which part of the documentation you want to make a change to. For example, the name of an existing documentation section or the line number in a file `README.md`.
2222

2323

2424
## The essence

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ test.py
1616
html
1717
.qwen
1818
.claude
19+
CLAUDE.md

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
![logo](https://raw.githubusercontent.com/pomponchik/cantok/main/docs/assets/logo_5.png)
1919

2020

21-
Cancellation Token is a pattern that allows us to refuse to continue calculations that we no longer need. It is implemented out of the box in many programming languages, for example in [C#](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken) and in [Go](https://pkg.go.dev/context). However, there was still no sane implementation in Python, until the [cantok](https://github.com/pomponchik/cantok) library.
21+
Cancellation Token is a pattern that allows us to cancel calculations that we no longer need. It is implemented out of the box in many programming languages, for example in [C#](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken) and in [Go](https://pkg.go.dev/context). However, there was still no sane implementation in Python, until the [cantok](https://github.com/pomponchik/cantok) library appeared.
2222

2323

2424
## Quick start

cantok/tokens/abstract/abstract_token.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,34 @@
1111

1212

1313
class AbstractToken(ABC):
14+
"""
15+
Abstract base class for all cancellation tokens.
16+
17+
A cancellation token represents a signal that can be used to cooperatively
18+
cancel a long-running operation. Most subclasses add an automatic cancellation
19+
condition (superpower) evaluated on every check; SimpleToken and DefaultToken
20+
rely on manual cancellation only.
21+
22+
Tokens can be composed with the + operator: the resulting token is cancelled
23+
when any of the combined tokens is cancelled.
24+
25+
Use AbstractToken as a type hint when a function accepts any token type.
26+
Pass DefaultToken() as the default to make the token optional:
27+
28+
>>> def run(token: AbstractToken = DefaultToken()) -> bool:
29+
... return token.keep_on()
30+
>>> run() # DefaultToken never cancels
31+
True
32+
>>> run(SimpleToken().cancel()) # cancelled token passed explicitly
33+
False
34+
35+
The idiomatic loop pattern:
36+
37+
>>> token = SimpleToken()
38+
>>> while token:
39+
... ... # loop exits when token is cancelled
40+
"""
41+
1442
exception = CancellationError
1543
_rollback_if_nondirect_polling = False
1644

@@ -112,6 +140,20 @@ def __bool__(self) -> bool:
112140

113141
@property
114142
def cancelled(self) -> bool:
143+
"""
144+
Whether the token is currently cancelled.
145+
146+
Evaluated dynamically on each access, taking into account the token's own
147+
cancellation rules and all embedded tokens. Setting to True cancels the token;
148+
setting to False on an already cancelled token raises ValueError.
149+
150+
>>> token = SimpleToken()
151+
>>> token.cancelled
152+
False
153+
>>> token.cancel()
154+
>>> token.cancelled
155+
True
156+
"""
115157
return self.is_cancelled()
116158

117159
@cancelled.setter
@@ -123,12 +165,53 @@ def cancelled(self, new_value: bool) -> None:
123165
raise ValueError('You cannot restore a cancelled token.')
124166

125167
def keep_on(self) -> bool:
168+
"""
169+
Returns True if the token is not cancelled, False otherwise.
170+
The opposite of is_cancelled().
171+
172+
>>> token = SimpleToken()
173+
>>> token.keep_on()
174+
True
175+
>>> token.cancel()
176+
>>> token.keep_on()
177+
False
178+
"""
126179
return not self.is_cancelled()
127180

128181
def is_cancelled(self, direct: bool = True) -> bool:
182+
"""
183+
Returns True if the token is cancelled, False otherwise.
184+
185+
:param direct: When False, tokens with rollback behaviour (e.g. CounterToken
186+
with direct=True) do not apply their side effects while being
187+
polled indirectly through a parent token. Defaults to True.
188+
189+
>>> token = SimpleToken()
190+
>>> token.is_cancelled()
191+
False
192+
>>> token.cancel()
193+
>>> token.is_cancelled()
194+
True
195+
"""
129196
return self._get_report(direct=direct).cause != CancelCause.NOT_CANCELLED
130197

131198
def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, float]] = None) -> Awaitable: # type: ignore[type-arg]
199+
"""
200+
Waits until the token is cancelled.
201+
202+
When used with ``await``, runs non-blocking inside an asyncio event loop.
203+
When called without ``await``, blocks the current thread.
204+
205+
:param step: Interval between status checks, in seconds. Defaults to 0.0001.
206+
:param timeout: Maximum time to wait, in seconds. If exceeded,
207+
raises TimeoutCancellationError. Defaults to None (no limit).
208+
209+
>>> import asyncio
210+
>>>
211+
>>> token = TimeoutToken(5)
212+
>>> token.wait() # blocks for ~5 seconds, then returns
213+
>>> asyncio.run(token.wait()) # non-blocking, inside an asyncio event loop
214+
"""
132215
if step < 0:
133216
raise ValueError('The token polling iteration time cannot be less than zero.')
134217
if timeout is not None and timeout < 0:
@@ -146,10 +229,33 @@ def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, fl
146229
return WaitCoroutineWrapper(step, self + token, token)
147230

148231
def cancel(self) -> 'AbstractToken':
232+
"""
233+
Cancels the token. Returns the token itself to allow method chaining.
234+
235+
Cancellation is irreversible: once cancelled, the token cannot be restored.
236+
237+
>>> token = SimpleToken()
238+
>>> token.cancel()
239+
>>> token.cancelled
240+
True
241+
"""
149242
self._cancelled = True
150243
return self
151244

152245
def check(self) -> None:
246+
"""
247+
Raises an exception if the token is cancelled; does nothing otherwise.
248+
249+
The exception type depends on the cancellation cause:
250+
- Manual cancellation via cancel() raises CancellationError.
251+
- Automatic cancellation by a specific token type raises the corresponding
252+
subclass (e.g. TimeoutCancellationError for TimeoutToken).
253+
254+
>>> token = SimpleToken()
255+
>>> token.check() # nothing happens
256+
>>> token.cancel()
257+
>>> token.check() # raises CancellationError
258+
"""
153259
with self._lock:
154260
report = self._get_report()
155261

cantok/tokens/condition_token.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,31 @@
66

77

88
class ConditionToken(AbstractToken):
9+
"""
10+
A token that cancels automatically when a condition function returns True.
11+
12+
The condition function is evaluated on every cancellation check. Once it
13+
returns True, the result is cached by default and the token stays cancelled.
14+
15+
:param function: A callable returning bool. Called on each cancellation check.
16+
:param suppress_exceptions: If True (default), exceptions from the function
17+
are swallowed and treated as the default value.
18+
:param default: Value to use when the function raises and suppress_exceptions
19+
is True. Defaults to False.
20+
:param before: Callable invoked before the condition function on each check.
21+
:param after: Callable invoked after the condition function on each check.
22+
:param caching: If True (default), the token stays cancelled once the
23+
condition has returned True, without re-evaluating it.
24+
25+
>>> items = []
26+
>>> token = ConditionToken(lambda: len(items) >= 3)
27+
>>> token.cancelled
28+
False
29+
>>> items += [1, 2, 3]
30+
>>> token.cancelled
31+
True
32+
"""
33+
934
exception = ConditionCancellationError
1035

1136
def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, cancelled: bool = False, suppress_exceptions: bool = True, default: bool = False, before: Callable[[], Any] = lambda: None, after: Callable[[], Any] = lambda: None, caching: bool = True): # noqa: PLR0913
@@ -25,7 +50,7 @@ def _superpower(self) -> bool:
2550

2651
if not self._suppress_exceptions:
2752
self._before()
28-
result = self.run_function()
53+
result = self._run_function()
2954
self._after()
3055
return result
3156

@@ -34,13 +59,13 @@ def _superpower(self) -> bool:
3459
with suppress(Exception):
3560
self._before()
3661
with suppress(Exception):
37-
result = self.run_function()
62+
result = self._run_function()
3863
with suppress(Exception):
3964
self._after()
4065

4166
return result
4267

43-
def run_function(self) -> bool:
68+
def _run_function(self) -> bool:
4469
result = self._function()
4570

4671
if not isinstance(result, bool):

cantok/tokens/counter_token.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@
55

66

77
class CounterToken(ConditionToken):
8+
"""
9+
A token that cancels automatically after a fixed number of iterations.
10+
11+
The internal counter decrements on each direct cancellation check. When it
12+
reaches zero, the token is cancelled. Useful for limiting the number of
13+
iterations of a loop without tracking state externally.
14+
15+
:param counter: Number of iterations before cancellation. Must be >= 0.
16+
:param direct: If True (default), counter decrements even when polled
17+
indirectly through a parent token. If False, indirect polls
18+
are rolled back, so only direct checks consume the counter.
19+
20+
>>> token = CounterToken(3)
21+
>>> while token:
22+
... ... # loop body executes exactly 3 times
23+
"""
24+
825
exception = CounterCancellationError
926

1027
def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False, direct: bool = True):

cantok/tokens/default_token.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33

44

55
class DefaultToken(AbstractToken):
6+
"""
7+
An immutable token that never cancels.
8+
9+
Useful as a neutral default argument: a function that accepts a token can
10+
receive a DefaultToken when no real cancellation is needed, without
11+
requiring None checks. Calling cancel() raises ImpossibleCancelError.
12+
13+
>>> def run(token: AbstractToken = DefaultToken()) -> bool:
14+
... return token.keep_on()
15+
>>> run() # True — DefaultToken never cancels
16+
True
17+
>>> run(SimpleToken()) # True — SimpleToken not yet cancelled
18+
True
19+
"""
20+
621
exception = ImpossibleCancelError
722

823
def __init__(self) -> None:

cantok/tokens/simple_token.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33

44

55
class SimpleToken(AbstractToken):
6+
"""
7+
A basic cancellation token with no automatic cancellation condition.
8+
9+
Can only be cancelled explicitly by calling cancel() or setting
10+
cancelled = True. Useful as a manual stop signal passed between threads.
11+
12+
>>> token = SimpleToken()
13+
>>> token.cancel()
14+
>>> token.cancelled
15+
True
16+
"""
17+
618
exception = CancellationError
719

820
def _superpower(self) -> bool:

cantok/tokens/timeout_token.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@
66

77

88
class TimeoutToken(ConditionToken):
9+
"""
10+
A token that cancels automatically after a specified duration.
11+
12+
The timeout is measured from the moment the token is created. When the
13+
deadline is reached, any cancellation check will return True and subsequent
14+
check() calls will raise TimeoutCancellationError.
15+
16+
:param timeout: Duration in seconds before cancellation. Must be >= 0.
17+
:param monotonic: If True, uses time.monotonic_ns() instead of
18+
time.perf_counter(), which is unaffected by system
19+
clock adjustments. Defaults to False.
20+
21+
>>> import time
22+
>>>
23+
>>> token = TimeoutToken(0.1)
24+
>>> token.cancelled
25+
False
26+
>>> time.sleep(0.2)
27+
>>> token.cancelled
28+
True
29+
"""
30+
931
exception = TimeoutCancellationError
1032

1133
def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False, monotonic: bool = False):

docs/ecosystem/projects/regular_functions_calling.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ metronome = Metronome(0.2, lambda: None, token=TimeoutToken(1))
3838
metronome.start()
3939
print(metronome.stopped)
4040
#> False
41-
sleep(1.5) # Here I specify a little more time than in the constructor of the token itself, since a small margin is needed for operations related to the creation of the metronome object itself.
41+
sleep(1.5) # We specify a slightly longer sleep time than the token timeout to allow for the overhead of creating the metronome object.
4242
print(metronome.stopped)
4343
#> True
4444
```

0 commit comments

Comments
 (0)