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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ html
.qwen
.claude
CLAUDE.md
AGENTS.md
115 changes: 36 additions & 79 deletions cantok/tokens/abstract/abstract_token.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
from abc import ABC, abstractmethod
from threading import RLock
from typing import Any, Awaitable, Dict, List, Optional, Union
Expand All @@ -7,7 +6,6 @@
from cantok.tokens.abstract.cancel_cause import CancelCause
from cantok.tokens.abstract.coroutine_wrapper import WaitCoroutineWrapper
from cantok.tokens.abstract.report import CancellationReport
from cantok.types import IterableWithTokens


class AbstractToken(ABC):
Expand Down Expand Up @@ -43,9 +41,13 @@ class AbstractToken(ABC):
_rollback_if_nondirect_polling = False

def __init__(self, *tokens: 'AbstractToken', cancelled: bool = False) -> None:
from cantok import DefaultToken # noqa: PLC0415

self._cached_report: Optional[CancellationReport] = None
self._cancelled: bool = cancelled
self._tokens: List[AbstractToken] = self._filter_tokens(tokens)
self._tokens: List[AbstractToken] = [
token for token in tokens if not isinstance(token, DefaultToken)
]

self._lock: RLock = RLock()

Expand All @@ -65,75 +67,30 @@ def __repr__(self) -> str:
else:
extra_kwargs = {}
extra_kwargs.update(**(self._get_extra_kwargs()))
text_representation_of_extra_kwargs = self._text_representation_of_kwargs(**extra_kwargs)
text_representation_of_extra_kwargs = self._text_representation_of_kwargs(
**extra_kwargs,
)
if text_representation_of_extra_kwargs:
chunks.append(text_representation_of_extra_kwargs)

glued_chunks = ', '.join(chunks)
return f'{type(self).__name__}({glued_chunks})'

def __str__(self) -> str:
cancelled_flag = 'cancelled' if self.is_cancelled(direct=False) else 'not cancelled'
cancelled_flag = (
'cancelled' if self.is_cancelled(direct=False) else 'not cancelled'
)
return f'<{type(self).__name__} ({cancelled_flag})>'

def __add__(self, item: 'AbstractToken') -> 'AbstractToken': # noqa: PLR0911
def __add__(self, item: 'AbstractToken') -> 'AbstractToken':
if not isinstance(item, AbstractToken):
raise TypeError('Cancellation Token can only be combined with another Cancellation Token.')

from cantok import DefaultToken, SimpleToken, TimeoutToken # noqa: PLC0415

if self._cancelled or item._cancelled:
return SimpleToken(cancelled=True)

nested_tokens = []
container_token: Optional[AbstractToken] = None

# Inspect the caller's frame to determine if a token is "temporary"
# (not stored in any variable). This is robust across all Python versions,
# unlike refcount-based detection which varies with bytecode optimizations.
_frame = sys._getframe(1)
_caller_locals = list(_frame.f_locals.values())
_caller_globals = list(_frame.f_globals.values())

def is_temp(token: 'AbstractToken') -> bool:
for v in _caller_locals:
if v is token:
return False
return all(v is not token for v in _caller_globals)

_self_is_temp = is_temp(self)
_item_is_temp = is_temp(item)

if isinstance(self, TimeoutToken) and isinstance(item, TimeoutToken) and self._monotonic == item._monotonic:
if self._deadline >= item._deadline and _self_is_temp:
if _item_is_temp:
item._tokens.extend(self._tokens)
return item
if self._tokens:
return SimpleToken(*(self._tokens), item)
return item
if self._deadline < item._deadline and _item_is_temp:
if _self_is_temp:
self._tokens.extend(item._tokens)
return self
if item._tokens:
return SimpleToken(*(item._tokens), self)
return self

for token in self, item:
if isinstance(token, SimpleToken) and is_temp(token):
nested_tokens.extend(token._tokens)
elif isinstance(token, DefaultToken):
pass
elif not isinstance(token, SimpleToken) and is_temp(token) and container_token is None:
container_token = token
else:
nested_tokens.append(token)

if container_token is None:
return SimpleToken(*nested_tokens)
container_token._tokens.extend(container_token._filter_tokens(nested_tokens))
return container_token
raise TypeError(
'Cancellation Token can only be combined with another Cancellation Token.',
)

from cantok import SimpleToken # noqa: PLC0415

return SimpleToken(self, item)

def __bool__(self) -> bool:
return self.keep_on()
Expand Down Expand Up @@ -195,7 +152,11 @@ def is_cancelled(self, direct: bool = True) -> bool:
"""
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]
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.

Expand All @@ -213,17 +174,23 @@ def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, fl
>>> asyncio.run(TimeoutToken(5).wait()) # non-blocking, inside an asyncio event loop
"""
if step < 0:
raise ValueError('The token polling iteration time cannot be less than zero.')
raise ValueError(
'The token polling iteration time cannot be less than zero.',
)
if timeout is not None and timeout < 0:
raise ValueError('The total timeout of waiting cannot be less than zero.')
if timeout is not None and step > timeout:
raise ValueError('The total timeout of waiting cannot be less than the time of one iteration of the token polling.')
raise ValueError(
'The total timeout of waiting cannot be less than the time of one iteration of the token polling.',
)

if timeout is None:
from cantok import SimpleToken # noqa: PLC0415

token: AbstractToken = SimpleToken()
else:
from cantok import TimeoutToken # noqa: PLC0415

token = TimeoutToken(timeout)

return WaitCoroutineWrapper(step, self + token, token)
Expand Down Expand Up @@ -265,19 +232,6 @@ def check(self) -> None:
elif report.cause == CancelCause.SUPERPOWER:
report.from_token._raise_superpower_exception()

def _filter_tokens(self, tokens: IterableWithTokens) -> List['AbstractToken']:
from cantok import DefaultToken # noqa: PLC0415

result: List[AbstractToken] = []

for token in tokens:
if isinstance(token, DefaultToken):
pass
else:
result.append(token)

return result

def _get_report(self, direct: bool = True) -> CancellationReport:
if self._cancelled:
return CancellationReport(
Expand Down Expand Up @@ -307,7 +261,10 @@ def _get_report(self, direct: bool = True) -> CancellationReport:
def _superpower(self) -> bool: # pragma: no cover
pass

def _superpower_rollback(self, superpower_data: Dict[str, Any]) -> None: # pragma: no cover # noqa: B027
def _superpower_rollback( # noqa: B027
self,
superpower_data: Dict[str, Any],
) -> None: # pragma: no cover
pass

def _check_superpower(self, direct: bool) -> bool:
Expand Down
2 changes: 0 additions & 2 deletions cantok/tokens/timeout_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled
def function() -> bool:
return timer() >= deadline

self._deadline = deadline

super().__init__(function, *tokens, cancelled=cancelled)

def _text_representation_of_superpower(self) -> str:
Expand Down
14 changes: 0 additions & 14 deletions cantok/types.py

This file was deleted.

20 changes: 1 addition & 19 deletions docs/what_are_tokens/summation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,4 @@ def function(token: AbstractToken):
...
```

The token summation operation always generates a new token. If at least one of the operand tokens is cancelled, the sum will also be cancelled. It is also guaranteed that the cancellation of this token does not lead to the cancellation of the operands. That is, the sum of two tokens always behaves as if it were a [`SimpleToken`](../types_of_tokens/SimpleToken.md) in which both operands were [nested](embedding.md). However, it is difficult to say exactly which token will result from the summation, since the library strives to minimize the generated graph of tokens for performance reasons.

You may notice that some tokens disappear altogether during summation:

```python
print(repr(SimpleToken() + TimeoutToken(5)))
#> TimeoutToken(5)
print(repr(SimpleToken(cancelled=True) + TimeoutToken(5)))
#> SimpleToken(cancelled=True)
```

In addition, you can safely sum more than 2 tokens — this does not generate anything superfluous:

```python
print(repr(TimeoutToken(5) + ConditionToken(lambda: False) + CounterToken(23)))
#> TimeoutToken(5, ConditionToken(λ), CounterToken(23))
```

In fact, there are quite a few effective ways to optimize the token addition operation that are implemented in the library. This operation is well optimized, so it is recommended in all cases when you need to combine the constraints of different tokens into one.
The token summation operation always generates a new [`SimpleToken`](../types_of_tokens/SimpleToken.md). If at least one of the nested operand tokens is cancelled, the sum will also be cancelled. It is also guaranteed that the cancellation of this token does not lead to the cancellation of the operands. Direct [`DefaultToken`](../types_of_tokens/DefaultToken.md) operands are not nested into the sum.
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "cantok"
version = "0.0.36"
version = "0.0.37"
authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }]
description = 'Implementation of the "Cancellation Token" pattern'
readme = "README.md"
Expand Down Expand Up @@ -41,8 +41,7 @@ keywords = ['cancellation tokens', 'parallel programming', 'concurrency']
"cantok" = ["py.typed"]

[tool.mutmut]
paths_to_mutate = "cantok"
runner = "pytest"
paths_to_mutate = ["cantok"]

[tool.ruff]
lint.ignore = ['E501', 'E712', 'PTH123', 'PTH118', 'PLR2004', 'PTH107', 'SIM105', 'SIM102', 'RET503', 'PLR0912', 'C901', 'E731', 'F821']
Expand Down
Empty file added tests/skipped/__init__.py
Empty file.
Empty file.
Empty file.
Loading
Loading