Skip to content

Commit

Permalink
Merge pull request #5 from pomponchik/develop
Browse files Browse the repository at this point in the history
0.0.5
  • Loading branch information
pomponchik authored Oct 2, 2023
2 parents ff5bde3 + fc7bade commit 86598f7
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 76 deletions.
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ counter = 0

def function(token):
global counter
while not token.cancelled:
while token:
counter += 1

token = ConditionToken(lambda: randint(1, 100_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1)
Expand Down Expand Up @@ -134,6 +134,19 @@ print(token.cancelled) # True
print(token.keep_on()) # False
```

You don't have to call the `keep_on()` method directly. Use the token itself as a boolean value, and the method call will occur "under the hood" automatically:

```python
from cantok import SimpleToken

token = SimpleToken()
print(bool(token)) # True
print(token.keep_on()) # True
token.cancel()
print(bool(token)) # False
print(token.keep_on()) # False
```

There is another method that is close in meaning to `is_cancelled()` - `check()`. It does nothing if the token is not canceled, or raises an exception if canceled. If the token was canceled by calling the `cancel()` method, a `CancellationError` exception will be raised:

```python
Expand Down Expand Up @@ -203,7 +216,7 @@ In addition, any tokens can be summed up among themselves. The summation operati
from cantok import SimpleToken, TimeoutToken

print(repr(SimpleToken() + TimeoutToken(5)))
# SimpleToken(SimpleToken(cancelled=False), TimeoutToken(5, cancelled=False, monotonic=False), cancelled=False)
# SimpleToken(SimpleToken(), TimeoutToken(5, monotonic=False))
```

This feature is convenient to use if your function has received a token with certain restrictions and wants to throw it into other called functions, imposing additional restrictions:
Expand Down Expand Up @@ -239,7 +252,7 @@ print(token.cancelled) # True
from cantok import CounterToken, TimeoutToken

print(repr(CounterToken(5) + TimeoutToken(5)))
# SimpleToken(CounterToken(5, cancelled=False, direct=True), TimeoutToken(5, cancelled=False, monotonic=False), cancelled=False)
# SimpleToken(CounterToken(5, direct=True), TimeoutToken(5, monotonic=False))
```

There is not much more to tell about it if you have read [the story](#tokens) about tokens in general.
Expand All @@ -257,7 +270,7 @@ from cantok import ConditionToken
counter = 0
token = ConditionToken(lambda: counter >= 5)

while not token.cancelled:
while token:
counter += 1

print(counter) # 5
Expand Down Expand Up @@ -327,7 +340,7 @@ from cantok import CounterToken
token = CounterToken(5)
counter = 0

while not token.cancelled:
while token:
counter += 1

print(counter) # 5
Expand Down
96 changes: 68 additions & 28 deletions cantok/tokens/abstract_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from abc import ABC, abstractmethod
from threading import RLock
from dataclasses import dataclass
from typing import List, Dict, Any

from cantok.errors import CancellationError

Expand All @@ -19,26 +20,39 @@ class CancellationReport:

class AbstractToken(ABC):
exception = CancellationError
rollback_if_nondirect_polling = False

def __init__(self, *tokens: 'AbstractToken', cancelled=False):
self.tokens = tokens
self._cancelled = cancelled
self.lock = RLock()

def __repr__(self):
other_tokens = ', '.join([repr(x) for x in self.tokens])
if other_tokens:
other_tokens += ', '
chunks = []
superpower = self.text_representation_of_superpower()
if superpower:
superpower += ', '
extra_kwargs = self.text_representation_of_extra_kwargs()
chunks.append(superpower)
other_tokens = ', '.join([repr(x) for x in self.tokens])
if other_tokens:
chunks.append(other_tokens)
report = self.get_report(direct=False)
if report.cause == CancelCause.NOT_CANCELLED:
extra_kwargs = {}
else:
if report.from_token is self and report.cause == CancelCause.CANCELLED:
extra_kwargs = {'cancelled': True}
else:
extra_kwargs = {}
extra_kwargs.update(**(self.get_extra_kwargs()))
extra_kwargs = self.text_representation_of_kwargs(**extra_kwargs)
if extra_kwargs:
extra_kwargs = ', ' + extra_kwargs
return f'{type(self).__name__}({superpower}{other_tokens}cancelled={self.cancelled}{extra_kwargs})'
chunks.append(extra_kwargs)

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

def __str__(self):
cancelled_flag = 'cancelled' if self.cancelled 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':
Expand All @@ -49,6 +63,9 @@ def __add__(self, item: 'AbstractToken') -> 'AbstractToken':

return SimpleToken(self, item)

def __bool__(self) -> bool:
return self.keep_on()

@property
def cancelled(self) -> bool:
return self.is_cancelled()
Expand All @@ -64,33 +81,32 @@ def cancelled(self, new_value) -> None:
def keep_on(self) -> bool:
return not self.is_cancelled()

def is_cancelled(self) -> bool:
return self.get_report().cause != CancelCause.NOT_CANCELLED
def is_cancelled(self, direct=True) -> bool:
return self.get_report(direct=direct).cause != CancelCause.NOT_CANCELLED

def get_report(self) -> CancellationReport:
def get_report(self, direct=True) -> CancellationReport:
if self._cancelled:
return CancellationReport(
cause=CancelCause.CANCELLED,
from_token=self,
)
elif self.superpower():
return CancellationReport(
cause=CancelCause.SUPERPOWER,
from_token=self,
)
else:
if self.check_superpower(direct):
return CancellationReport(
cause=CancelCause.SUPERPOWER,
from_token=self,
)

for token in self.tokens:
if token.is_cancelled_reflect():
return token.get_report()
report = token.get_report(direct=False)
if report.cause != CancelCause.NOT_CANCELLED:
return report

return CancellationReport(
cause=CancelCause.NOT_CANCELLED,
from_token=self,
)

def is_cancelled_reflect(self):
return self.is_cancelled()

def cancel(self) -> 'AbstractToken':
self._cancelled = True
return self
Expand All @@ -99,23 +115,47 @@ def cancel(self) -> 'AbstractToken':
def superpower(self) -> bool: # pragma: no cover
pass

def superpower_rollback(self, superpower_data: Dict[str, Any]) -> None:
pass

def check_superpower(self, direct: bool) -> bool:
if self.rollback_if_nondirect_polling and not direct:
return self.check_superpower_with_rollback()
return self.superpower()

def check_superpower_with_rollback(self) -> bool:
with self.lock:
superpower_data = self.get_superpower_data()
result = self.superpower()
self.superpower_rollback(superpower_data)
return result

def get_superpower_data(self) -> Dict[str, Any]:
return {}

@abstractmethod
def text_representation_of_superpower(self) -> str: # pragma: no cover
pass

def get_extra_kwargs(self) -> Dict[str, Any]:
return {}

def text_representation_of_extra_kwargs(self) -> str:
return ''
return self.text_representation_of_kwargs(**(self.get_extra_kwargs()))

def text_representation_of_kwargs(self, **kwargs: Any) -> str:
pairs: List[str] = [f'{key}={repr(value)}' for key, value in kwargs.items()]
return ', '.join(pairs)

def check(self) -> None:
with self.lock:
if self.is_cancelled_reflect():
report = self.get_report()
report = self.get_report()

if report.cause == CancelCause.CANCELLED:
report.from_token.raise_cancelled_exception()
if report.cause == CancelCause.CANCELLED:
report.from_token.raise_cancelled_exception()

elif report.cause == CancelCause.SUPERPOWER:
report.from_token.raise_superpower_exception()
elif report.cause == CancelCause.SUPERPOWER:
report.from_token.raise_superpower_exception()

def raise_cancelled_exception(self) -> None:
raise CancellationError('The token has been cancelled.', self)
Expand Down
7 changes: 3 additions & 4 deletions cantok/tokens/condition_token.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Callable
from typing import Callable, Dict, Any
from contextlib import suppress

from cantok import AbstractToken
Expand Down Expand Up @@ -38,12 +38,11 @@ def run_function(self) -> bool:
def text_representation_of_superpower(self) -> str:
return repr(self.function)

def text_representation_of_extra_kwargs(self) -> str:
extra_kwargs = {
def get_extra_kwargs(self) -> Dict[str, Any]:
return {
'suppress_exceptions': self.suppress_exceptions,
'default': self.default,
}
return ', '.join([f'{key}={value}' for key, value in extra_kwargs.items()])

def get_superpower_exception_message(self) -> str:
return 'The condition is not met.'
37 changes: 12 additions & 25 deletions cantok/tokens/counter_token.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Dict, Any

from cantok import AbstractToken
from cantok import ConditionToken
from cantok.errors import CounterCancellationError
Expand All @@ -13,6 +15,7 @@ def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False
self.counter = counter
self.initial_counter = counter
self.direct = direct
self.rollback_if_nondirect_polling = self.direct

def function() -> bool:
with self.lock:
Expand All @@ -23,35 +26,19 @@ def function() -> bool:

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

def __repr__(self):
other_tokens = ', '.join([repr(x) for x in self.tokens])
if other_tokens:
other_tokens += ', '
superpower = self.text_representation_of_superpower() + ', '
cancelled = self.get_cancelled_status_without_decrementing_counter()
return f'{type(self).__name__}({superpower}{other_tokens}cancelled={cancelled}, direct={self.direct})'

def __str__(self):
cancelled_flag = 'cancelled' if self.get_cancelled_status_without_decrementing_counter() else 'not cancelled'
return f'<{type(self).__name__} ({cancelled_flag})>'

def get_cancelled_status_without_decrementing_counter(self) -> bool:
with self.lock:
result = self.cancelled
if not result:
self.counter += 1
return result

def is_cancelled_reflect(self) -> bool:
if self.direct:
return self.get_cancelled_status_without_decrementing_counter()
return self.cancelled
def superpower_rollback(self, superpower_data: Dict[str, Any]) -> None:
self.counter = superpower_data['counter']

def text_representation_of_superpower(self) -> str:
return str(self.counter)

def text_representation_of_extra_kwargs(self) -> str:
return f'direct={self.direct}'
def get_extra_kwargs(self) -> Dict[str, Any]:
return {
'direct': self.direct,
}

def get_superpower_data(self) -> Dict[str, Any]:
return {'counter': self.counter}

def get_superpower_exception_message(self) -> str:
return f'After {self.initial_counter} attempts, the counter was reset to zero.'
8 changes: 5 additions & 3 deletions cantok/tokens/timeout_token.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from time import monotonic_ns, perf_counter

from typing import Union, Callable
from typing import Union, Callable, Dict, Any

from cantok import AbstractToken
from cantok import ConditionToken
Expand Down Expand Up @@ -33,8 +33,10 @@ def function() -> bool:
def text_representation_of_superpower(self) -> str:
return str(self.timeout)

def text_representation_of_extra_kwargs(self) -> str:
return f'monotonic={self.monotonic}'
def get_extra_kwargs(self) -> Dict[str, Any]:
return {
'monotonic': self.monotonic,
}

def get_superpower_exception_message(self) -> str:
return f'The timeout of {self.timeout} seconds has expired.'
17 changes: 17 additions & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Welcome to MkDocs

For full documentation visit [mkdocs.org](https://www.mkdocs.org).

## Commands

* `mkdocs new [dir-name]` - Create a new project.
* `mkdocs serve` - Start the live-reloading docs server.
* `mkdocs build` - Build the documentation site.
* `mkdocs -h` - Print help message and exit.

## Project layout

mkdocs.yml # The configuration file.
docs/
index.md # The documentation homepage.
... # Other markdown pages, images and other files.
6 changes: 6 additions & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
site_name: 'CANTOK: Implementation of the "Cancellation Token" pattern'
theme:
name: material
repo_url: https://github.com/pomponchik/cantok
docs_dir: docs
lang: en
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
long_description=readme,
long_description_content_type='text/markdown',
url='https://github.com/pomponchik/cantok',
packages=find_packages(exclude=['tests']),
packages=find_packages(exclude=['tests', 'docs']),
install_requires=requirements,
classifiers=[
'Operating System :: MacOS :: MacOS X',
Expand Down
Loading

0 comments on commit 86598f7

Please sign in to comment.