Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ assignees: pomponchik

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

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.
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.


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

## Where?

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`.
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`.


## The essence
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ test.py
html
.qwen
.claude
CLAUDE.md
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
![logo](https://raw.githubusercontent.com/pomponchik/cantok/main/docs/assets/logo_5.png)


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.
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.


## Quick start
Expand Down
106 changes: 106 additions & 0 deletions cantok/tokens/abstract/abstract_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@


class AbstractToken(ABC):
"""
Abstract base class for all cancellation tokens.

A cancellation token represents a signal that can be used to cooperatively
cancel a long-running operation. Most subclasses add an automatic cancellation
condition (superpower) evaluated on every check; SimpleToken and DefaultToken
rely on manual cancellation only.

Tokens can be composed with the + operator: the resulting token is cancelled
when any of the combined tokens is cancelled.

Use AbstractToken as a type hint when a function accepts any token type.
Pass DefaultToken() as the default to make the token optional:

>>> def run(token: AbstractToken = DefaultToken()) -> bool:
... return token.keep_on()
>>> run() # DefaultToken never cancels
True
>>> run(SimpleToken().cancel()) # cancelled token passed explicitly
False

The idiomatic loop pattern:

>>> token = SimpleToken()
>>> while token:
... ... # loop exits when token is cancelled
"""

exception = CancellationError
_rollback_if_nondirect_polling = False

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

@property
def cancelled(self) -> bool:
"""
Whether the token is currently cancelled.

Evaluated dynamically on each access, taking into account the token's own
cancellation rules and all embedded tokens. Setting to True cancels the token;
setting to False on an already cancelled token raises ValueError.

>>> token = SimpleToken()
>>> token.cancelled
False
>>> token.cancel()
>>> token.cancelled
True
"""
return self.is_cancelled()

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

def keep_on(self) -> bool:
"""
Returns True if the token is not cancelled, False otherwise.
The opposite of is_cancelled().

>>> token = SimpleToken()
>>> token.keep_on()
True
>>> token.cancel()
>>> token.keep_on()
False
"""
return not self.is_cancelled()

def is_cancelled(self, direct: bool = True) -> bool:
"""
Returns True if the token is cancelled, False otherwise.

:param direct: When False, tokens with rollback behaviour (e.g. CounterToken
with direct=True) do not apply their side effects while being
polled indirectly through a parent token. Defaults to True.

>>> token = SimpleToken()
>>> token.is_cancelled()
False
>>> token.cancel()
>>> token.is_cancelled()
True
"""
return self._get_report(direct=direct).cause != CancelCause.NOT_CANCELLED

def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, float]] = None) -> Awaitable: # type: ignore[type-arg]
"""
Waits until the token is cancelled.

When used with ``await``, runs non-blocking inside an asyncio event loop.
When called without ``await``, blocks the current thread.

:param step: Interval between status checks, in seconds. Defaults to 0.0001.
:param timeout: Maximum time to wait, in seconds. If exceeded,
raises TimeoutCancellationError. Defaults to None (no limit).

>>> import asyncio
>>>
>>> token = TimeoutToken(5)
>>> token.wait() # blocks for ~5 seconds, then returns
>>> asyncio.run(token.wait()) # non-blocking, inside an asyncio event loop
"""
if step < 0:
raise ValueError('The token polling iteration time cannot be less than zero.')
if timeout is not None and timeout < 0:
Expand All @@ -146,10 +229,33 @@ def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, fl
return WaitCoroutineWrapper(step, self + token, token)

def cancel(self) -> 'AbstractToken':
"""
Cancels the token. Returns the token itself to allow method chaining.

Cancellation is irreversible: once cancelled, the token cannot be restored.

>>> token = SimpleToken()
>>> token.cancel()
>>> token.cancelled
True
"""
self._cancelled = True
return self

def check(self) -> None:
"""
Raises an exception if the token is cancelled; does nothing otherwise.

The exception type depends on the cancellation cause:
- Manual cancellation via cancel() raises CancellationError.
- Automatic cancellation by a specific token type raises the corresponding
subclass (e.g. TimeoutCancellationError for TimeoutToken).

>>> token = SimpleToken()
>>> token.check() # nothing happens
>>> token.cancel()
>>> token.check() # raises CancellationError
"""
with self._lock:
report = self._get_report()

Expand Down
31 changes: 28 additions & 3 deletions cantok/tokens/condition_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@


class ConditionToken(AbstractToken):
"""
A token that cancels automatically when a condition function returns True.

The condition function is evaluated on every cancellation check. Once it
returns True, the result is cached by default and the token stays cancelled.

:param function: A callable returning bool. Called on each cancellation check.
:param suppress_exceptions: If True (default), exceptions from the function
are swallowed and treated as the default value.
:param default: Value to use when the function raises and suppress_exceptions
is True. Defaults to False.
:param before: Callable invoked before the condition function on each check.
:param after: Callable invoked after the condition function on each check.
:param caching: If True (default), the token stays cancelled once the
condition has returned True, without re-evaluating it.

>>> items = []
>>> token = ConditionToken(lambda: len(items) >= 3)
>>> token.cancelled
False
>>> items += [1, 2, 3]
>>> token.cancelled
True
"""

exception = ConditionCancellationError

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
Expand All @@ -25,7 +50,7 @@ def _superpower(self) -> bool:

if not self._suppress_exceptions:
self._before()
result = self.run_function()
result = self._run_function()
self._after()
return result

Expand All @@ -34,13 +59,13 @@ def _superpower(self) -> bool:
with suppress(Exception):
self._before()
with suppress(Exception):
result = self.run_function()
result = self._run_function()
with suppress(Exception):
self._after()

return result

def run_function(self) -> bool:
def _run_function(self) -> bool:
result = self._function()

if not isinstance(result, bool):
Expand Down
17 changes: 17 additions & 0 deletions cantok/tokens/counter_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@


class CounterToken(ConditionToken):
"""
A token that cancels automatically after a fixed number of iterations.

The internal counter decrements on each direct cancellation check. When it
reaches zero, the token is cancelled. Useful for limiting the number of
iterations of a loop without tracking state externally.

:param counter: Number of iterations before cancellation. Must be >= 0.
:param direct: If True (default), counter decrements even when polled
indirectly through a parent token. If False, indirect polls
are rolled back, so only direct checks consume the counter.

>>> token = CounterToken(3)
>>> while token:
... ... # loop body executes exactly 3 times
"""

exception = CounterCancellationError

def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False, direct: bool = True):
Expand Down
15 changes: 15 additions & 0 deletions cantok/tokens/default_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@


class DefaultToken(AbstractToken):
"""
An immutable token that never cancels.

Useful as a neutral default argument: a function that accepts a token can
receive a DefaultToken when no real cancellation is needed, without
requiring None checks. Calling cancel() raises ImpossibleCancelError.

>>> def run(token: AbstractToken = DefaultToken()) -> bool:
... return token.keep_on()
>>> run() # True — DefaultToken never cancels
True
>>> run(SimpleToken()) # True — SimpleToken not yet cancelled
True
"""

exception = ImpossibleCancelError

def __init__(self) -> None:
Expand Down
12 changes: 12 additions & 0 deletions cantok/tokens/simple_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@


class SimpleToken(AbstractToken):
"""
A basic cancellation token with no automatic cancellation condition.

Can only be cancelled explicitly by calling cancel() or setting
cancelled = True. Useful as a manual stop signal passed between threads.

>>> token = SimpleToken()
>>> token.cancel()
>>> token.cancelled
True
"""

exception = CancellationError

def _superpower(self) -> bool:
Expand Down
22 changes: 22 additions & 0 deletions cantok/tokens/timeout_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@


class TimeoutToken(ConditionToken):
"""
A token that cancels automatically after a specified duration.

The timeout is measured from the moment the token is created. When the
deadline is reached, any cancellation check will return True and subsequent
check() calls will raise TimeoutCancellationError.

:param timeout: Duration in seconds before cancellation. Must be >= 0.
:param monotonic: If True, uses time.monotonic_ns() instead of
time.perf_counter(), which is unaffected by system
clock adjustments. Defaults to False.

>>> import time
>>>
>>> token = TimeoutToken(0.1)
>>> token.cancelled
False
>>> time.sleep(0.2)
>>> token.cancelled
True
"""

exception = TimeoutCancellationError

def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False, monotonic: bool = False):
Expand Down
2 changes: 1 addition & 1 deletion docs/ecosystem/projects/regular_functions_calling.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ metronome = Metronome(0.2, lambda: None, token=TimeoutToken(1))
metronome.start()
print(metronome.stopped)
#> False
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.
sleep(1.5) # We specify a slightly longer sleep time than the token timeout to allow for the overhead of creating the metronome object.
print(metronome.stopped)
#> True
```
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![logo](https://raw.githubusercontent.com/pomponchik/cantok/main/docs/assets/logo_5.png)


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.
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 appeared.
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Install it from [Pypi](https://pypi.org/project/cantok/):
Install it from [PyPI](https://pypi.org/project/cantok/):

```bash
pip install cantok
Expand Down
4 changes: 2 additions & 2 deletions docs/quick_start.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ while token:
print(counter)
```

In this code, we use a token that describes several restrictions: on the [number of iterations](types_of_tokens/CounterToken.md) of the cycle, on [time](types_of_tokens/TimeoutToken.md), as well as on the [occurrence](types_of_tokens/ConditionToken.md) of a random unlikely event. When any of the indicated events occur, the cycle stops.
In this code, we use a token that describes several restrictions: on the [number of iterations](types_of_tokens/CounterToken.md) of the loop, on [time](types_of_tokens/TimeoutToken.md), as well as on the [occurrence](types_of_tokens/ConditionToken.md) of a random unlikely event. When any of the indicated events occur, the loop stops.

In fact, the library's capabilities are much broader, read the documentation below.
In fact, the library's capabilities are much broader. Read the documentation below.
10 changes: 5 additions & 5 deletions docs/the_pattern.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# The pattern

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 appeared.
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.

The essence of the pattern is that we pass special objects to functions and constructors, by which the executed code can understand whether it should continue its execution or not. When deciding whether to allow code execution to continue, this object can take into account both the restrictions specified to it, such as the maximum code execution time, and receive signals about the need to stop from the outside, for example from another thread or a coroutine. Thus, we do not nail down the logic associated with stopping code execution, for example, by directly tracking cycle counters, but implement [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) of this restriction.
The essence of the pattern is that we pass special objects to functions and constructors, by which the executed code can understand whether it should continue its execution or not. When deciding whether to allow code execution to continue, this object can both take into account the restrictions imposed on it, such as the maximum code execution time, and receive signals about the need to stop from the outside, for example from another thread or a coroutine. Thus, we do not nail down the logic associated with stopping code execution, for example, by directly tracking loop counters, but implement [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) of this restriction.

In addition, the pattern assumes that various restrictions can be combined indefinitely with each other: if at least one of the restrictions is not met, code execution will be interrupted. It is assumed that each function in the call stack will call other functions, throwing its token directly to them, or wrapping it in another token, with a stricter restriction imposed on it.
In addition, the pattern assumes that various restrictions can be combined in unlimited combinations with each other: if at least one of the restrictions is not met, code execution will be interrupted. It is assumed that each function in the call stack will call other functions, passing its token directly to them, or wrapping it in another token with stricter restrictions.

Unlike other ways of interrupting code execution, tokens do not force the execution thread to be interrupted forcibly. The interruption occurs "gently", allowing the code to terminate correctly, return all occupied resources and restore consistency.
Unlike other ways of stopping code execution, tokens do not force the execution thread to be interrupted. The interruption occurs "gently", allowing the code to terminate correctly, release all held resources and restore consistency.

It is highly desirable for library developers to use this pattern for any long-term composite operations. Your function can accept a token as an optional argument, with a default value that imposes minimal restrictions or none at all. If the user wishes, he can transfer his token there, imposing stricter restrictions on the library code. In addition to a more convenient and extensible API, this will give the library an advantage in the form of better testability, because the restrictions are no longer sewn directly into the function, which means they can be made whatever you want for the test. In addition, the library developer no longer needs to think about all the numerous restrictions that can be imposed on his code - the user can take care of it himself if he needs to.
It is highly desirable for library developers to use this pattern for any long-running operations. Your function can accept a token as an optional argument, with a default value that imposes minimal restrictions or none at all. If the user wishes, they can pass their token to it, imposing stricter restrictions on the library code. In addition to a more convenient and extensible API, this will give the library an advantage in the form of better testability, because the restrictions are no longer hardcoded into the function, which means they can be made whatever you want for the test. In addition, the library developer no longer needs to think about all the numerous restrictions that can be imposed on their code - the user can take care of it themselves if they need to.
Loading
Loading