From ed5093dd715612d1d2494f78f60abf6176701680 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 16:36:41 +0300 Subject: [PATCH 001/110] base structure --- .github/workflows/tests_and_coverage.yml | 45 ++++++++++++++++++++++++ .gitignore | 8 +++++ ctok/__init__.py | 0 ctok/abstract_token.py | 33 +++++++++++++++++ ctok/simple_token.py | 6 ++++ requirements_dev.txt | 5 +++ setup.py | 35 ++++++++++++++++++ tests/__init__.py | 0 tests/test_simple_token.py | 16 +++++++++ 9 files changed, 148 insertions(+) create mode 100644 .github/workflows/tests_and_coverage.yml create mode 100644 .gitignore create mode 100644 ctok/__init__.py create mode 100644 ctok/abstract_token.py create mode 100644 ctok/simple_token.py create mode 100644 requirements_dev.txt create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_simple_token.py diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml new file mode 100644 index 0000000..2830db0 --- /dev/null +++ b/.github/workflows/tests_and_coverage.yml @@ -0,0 +1,45 @@ +name: Tests and coverage + +on: + push + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install the library + shell: bash + run: python setup.py install + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Print all libs + shell: bash + run: pip list + + - name: Run tests and show coverage on the command line + run: coverage run --source=ctok --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m + + - name: Upload reports to codecov + env: + CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} + if: runner.os == 'Linux' + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + find . -iregex "codecov.*" + chmod +x codecov + ./codecov -t ${CODECOV_TOKEN} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08b156a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +venv +.pytest_cache +*.egg-info +build +dist +__pycache__ +.idea +test.py diff --git a/ctok/__init__.py b/ctok/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ctok/abstract_token.py b/ctok/abstract_token.py new file mode 100644 index 0000000..fd7557b --- /dev/null +++ b/ctok/abstract_token.py @@ -0,0 +1,33 @@ +from abc import ABC, abstractmethod + + +class AbstractToken(ABC): + def __init__(self, *tokens: 'AbstractToken'): + self.tokens = tokens + self._cancelled = False + + @property + def cancelled(self) -> bool: + return self.is_cancelled() + + def keep_on(self) -> bool: + return not self.is_cancelled() + + def is_cancelled(self) -> bool: + if self._cancelled: + return True + + elif any(x.is_cancelled() for x in self.tokens): + return True + + elif self.superpower(): + return True + + return False + + def cancel(self) -> None: + self._cancelled = True + + @abstractmethod + def superpower(self) -> bool: + pass diff --git a/ctok/simple_token.py b/ctok/simple_token.py new file mode 100644 index 0000000..4b9d30b --- /dev/null +++ b/ctok/simple_token.py @@ -0,0 +1,6 @@ +from ctok.abstract_token import AbstractToken + + +class SimpleToken(AbstractToken): + def superpower(self) ->: + return False diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..47b4792 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,5 @@ +pytest==7.4.2 +coverage==7.2.7 +twine==4.0.2 +wheel==0.40.0 +setuptools==68.2.2 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..770d4d8 --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +from setuptools import setup, find_packages + + +with open('README.md', 'r', encoding='utf8') as readme_file: + readme = readme_file.read() + +requirements = [] + +setup( + name='ctok', + version='0.0.1', + author='Evgeniy Blinov', + author_email='zheni-b@yandex.ru', + description='Implementation of the "Cancellation Token" pattern', + long_description=readme, + long_description_content_type='text/markdown', + url='https://github.com/pomponchik/ctok', + packages=find_packages(exclude=['tests']), + install_requires=requirements, + classifiers=[ + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries', + ], +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_simple_token.py b/tests/test_simple_token.py new file mode 100644 index 0000000..43e819b --- /dev/null +++ b/tests/test_simple_token.py @@ -0,0 +1,16 @@ +from ctok.simple_token import SimpleToken + + +def test_created_token_is_going_on(): + assert SimpleToken().cancelled == False + assert SimpleToken().is_cancelled() == False + assert SimpleToken().keep_on() == True + + +def test_stopped_token_is_not_going_on(): + token = SimpleToken() + token.cancel() + + assert token.cancelled == True + assert token.is_cancelled() == True + assert token.keep_on() == False From 8cfbfe9d43ada33a0f6871b45f6fb005b7224e2c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 16:42:51 +0300 Subject: [PATCH 002/110] no extra requirement --- requirements_dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 47b4792..f3c0107 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,4 +2,3 @@ pytest==7.4.2 coverage==7.2.7 twine==4.0.2 wheel==0.40.0 -setuptools==68.2.2 From 9e41d8f097aab7e11844bfcdbcaf0603b52005a5 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 16:43:54 +0300 Subject: [PATCH 003/110] type hint --- ctok/simple_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctok/simple_token.py b/ctok/simple_token.py index 4b9d30b..b6983ff 100644 --- a/ctok/simple_token.py +++ b/ctok/simple_token.py @@ -2,5 +2,5 @@ class SimpleToken(AbstractToken): - def superpower(self) ->: + def superpower(self) -> bool: return False From a46e689fcad2595f9648717384e5798a5d646454 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 16:47:13 +0300 Subject: [PATCH 004/110] pragma --- ctok/abstract_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctok/abstract_token.py b/ctok/abstract_token.py index fd7557b..3008702 100644 --- a/ctok/abstract_token.py +++ b/ctok/abstract_token.py @@ -29,5 +29,5 @@ def cancel(self) -> None: self._cancelled = True @abstractmethod - def superpower(self) -> bool: + def superpower(self) -> bool: # pragma: no cover pass From 0d51c8316dd193c9c49fd07606165b44eadfe357 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 18:56:57 +0300 Subject: [PATCH 005/110] __repr__, __str__ --- ctok/abstract_token.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ctok/abstract_token.py b/ctok/abstract_token.py index 3008702..fcffb36 100644 --- a/ctok/abstract_token.py +++ b/ctok/abstract_token.py @@ -2,9 +2,19 @@ class AbstractToken(ABC): - def __init__(self, *tokens: 'AbstractToken'): + def __init__(self, *tokens: 'AbstractToken', cancelled=False): self.tokens = tokens - self._cancelled = False + self._cancelled = cancelled + + def __repr__(self): + other_tokens = ', '.join([repr(x) for x in self.tokens]) + if other_tokens: + other_tokens += ', ' + return f'{type(self).__name__}({other_tokens}cancelled={self.cancelled})' + + def __str__(self): + cancelled_flag = 'cancelled' if self.cancelled else 'not cancelled' + return f'<{type(self).__name__} ({cancelled_flag})>' @property def cancelled(self) -> bool: @@ -25,8 +35,9 @@ def is_cancelled(self) -> bool: return False - def cancel(self) -> None: + def cancel(self) -> 'AbstractToken': self._cancelled = True + return self @abstractmethod def superpower(self) -> bool: # pragma: no cover From 83ed1ce929f0955a4a753b4dfd1496ffdbd74f3d Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 18:57:16 +0300 Subject: [PATCH 006/110] tests --- tests/test_simple_token.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_simple_token.py b/tests/test_simple_token.py index 43e819b..ec66a87 100644 --- a/tests/test_simple_token.py +++ b/tests/test_simple_token.py @@ -1,12 +1,28 @@ +import pytest + from ctok.simple_token import SimpleToken -def test_created_token_is_going_on(): +def test_just_created_token_without_arguments(): assert SimpleToken().cancelled == False assert SimpleToken().is_cancelled() == False assert SimpleToken().keep_on() == True +@pytest.mark.parametrize('arguments,expected_cancelled_status', [ + ([SimpleToken(), SimpleToken().cancel()], True), + ([SimpleToken()], False), + ([SimpleToken().cancel()], True), + ([SimpleToken(), SimpleToken()], False), + ([SimpleToken(), SimpleToken(), SimpleToken()], False), + ([SimpleToken(), SimpleToken(), SimpleToken(), SimpleToken()], False), +]) +def test_just_created_token_with_arguments(arguments, expected_cancelled_status): + assert SimpleToken(*arguments).cancelled == expected_cancelled_status + assert SimpleToken(*arguments).is_cancelled() == expected_cancelled_status + assert SimpleToken(*arguments).keep_on() == (not expected_cancelled_status) + + def test_stopped_token_is_not_going_on(): token = SimpleToken() token.cancel() From 04b6949463240c3e7ab92fb6d2af49268bd83969 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 18:57:32 +0300 Subject: [PATCH 007/110] condition token --- ctok/condition_token.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 ctok/condition_token.py diff --git a/ctok/condition_token.py b/ctok/condition_token.py new file mode 100644 index 0000000..df4ef92 --- /dev/null +++ b/ctok/condition_token.py @@ -0,0 +1,21 @@ +from typing import Callable +from contextlib import suppress + +from ctok.abstract_token import AbstractToken + + +class ConditionToken(AbstractToken): + def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True): + super().__init__(*tokens) + self.function = function + self.suppress_exceptions = suppress_exceptions + + def superpower(self) -> bool: + if not self.suppress_exceptions: + return self.function() + + else: + with suppress(Exception): + return self.function() + + return False From e429f670d308814cc3000653f784aa5caa128a09 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 18:59:40 +0300 Subject: [PATCH 008/110] dedicated directory for tokens --- ctok/tokens/__init__.py | 0 ctok/{ => tokens}/abstract_token.py | 0 ctok/{ => tokens}/condition_token.py | 0 ctok/{ => tokens}/simple_token.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 ctok/tokens/__init__.py rename ctok/{ => tokens}/abstract_token.py (100%) rename ctok/{ => tokens}/condition_token.py (100%) rename ctok/{ => tokens}/simple_token.py (100%) diff --git a/ctok/tokens/__init__.py b/ctok/tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ctok/abstract_token.py b/ctok/tokens/abstract_token.py similarity index 100% rename from ctok/abstract_token.py rename to ctok/tokens/abstract_token.py diff --git a/ctok/condition_token.py b/ctok/tokens/condition_token.py similarity index 100% rename from ctok/condition_token.py rename to ctok/tokens/condition_token.py diff --git a/ctok/simple_token.py b/ctok/tokens/simple_token.py similarity index 100% rename from ctok/simple_token.py rename to ctok/tokens/simple_token.py From 5c18d8e246118f859f7f78a94b46a52ed253ca0a Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 19:00:49 +0300 Subject: [PATCH 009/110] dedicated directory for tokens --- ctok/tokens/simple_token.py | 2 +- tests/test_simple_token.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ctok/tokens/simple_token.py b/ctok/tokens/simple_token.py index b6983ff..6143b08 100644 --- a/ctok/tokens/simple_token.py +++ b/ctok/tokens/simple_token.py @@ -1,4 +1,4 @@ -from ctok.abstract_token import AbstractToken +from ctok.tokens.abstract_token import AbstractToken class SimpleToken(AbstractToken): diff --git a/tests/test_simple_token.py b/tests/test_simple_token.py index ec66a87..fba0ee2 100644 --- a/tests/test_simple_token.py +++ b/tests/test_simple_token.py @@ -1,6 +1,6 @@ import pytest -from ctok.simple_token import SimpleToken +from ctok.tokens.simple_token import SimpleToken def test_just_created_token_without_arguments(): From 258891a064b1a7ede8116bd7ba4d1d9fe13f4e35 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 19 Sep 2023 19:03:04 +0300 Subject: [PATCH 010/110] dedicated directory for tokens --- ctok/tokens/condition_token.py | 2 +- tests/tokens/__init__.py | 0 tests/tokens/test_condition_token.py | 1 + tests/{ => tokens}/test_simple_token.py | 0 4 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 tests/tokens/__init__.py create mode 100644 tests/tokens/test_condition_token.py rename tests/{ => tokens}/test_simple_token.py (100%) diff --git a/ctok/tokens/condition_token.py b/ctok/tokens/condition_token.py index df4ef92..fb6df40 100644 --- a/ctok/tokens/condition_token.py +++ b/ctok/tokens/condition_token.py @@ -1,7 +1,7 @@ from typing import Callable from contextlib import suppress -from ctok.abstract_token import AbstractToken +from ctok.tokens.abstract_token import AbstractToken class ConditionToken(AbstractToken): diff --git a/tests/tokens/__init__.py b/tests/tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py new file mode 100644 index 0000000..664acba --- /dev/null +++ b/tests/tokens/test_condition_token.py @@ -0,0 +1 @@ +from ctok.tokens.condition_token import ConditionToken diff --git a/tests/test_simple_token.py b/tests/tokens/test_simple_token.py similarity index 100% rename from tests/test_simple_token.py rename to tests/tokens/test_simple_token.py From 5da9292af9b53b5abede7ddda8a99ae3675fb63c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 17:11:16 +0300 Subject: [PATCH 011/110] tests --- tests/tokens/test_condition_token.py | 54 ++++++++++++++++++++++++++++ tests/tokens/test_simple_token.py | 7 ++++ 2 files changed, 61 insertions(+) diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index 664acba..ea6a6bc 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -1 +1,55 @@ +from functools import partial + +import pytest + from ctok.tokens.condition_token import ConditionToken +from ctok.tokens.simple_token import SimpleToken + +def test_condition_counter(): + loop_size = 5 + def condition(): + for number in range(loop_size): + yield False + while True: + yield True + + token = ConditionToken(partial(next, iter(condition()))) + + counter = 0 + while not token.cancelled: + counter += 1 + + assert counter == loop_size + + +def test_condition_false(): + assert ConditionToken(lambda: False).cancelled == False + assert ConditionToken(lambda: False).is_cancelled() == False + assert ConditionToken(lambda: False).keep_on() == True + + +def test_condition_true(): + assert ConditionToken(lambda: True).cancelled == True + assert ConditionToken(lambda: True).is_cancelled() == True + assert ConditionToken(lambda: True).keep_on() == False + + +@pytest.mark.parametrize('arguments,expected_cancelled_status', [ + ([SimpleToken(), SimpleToken().cancel()], True), + ([SimpleToken(), ConditionToken(lambda: True)], True), + ([ConditionToken(lambda: False), ConditionToken(lambda: True)], True), + ([SimpleToken()], False), + ([ConditionToken(lambda: False)], False), + ([SimpleToken().cancel()], True), + ([ConditionToken(lambda: False).cancel()], True), + ([ConditionToken(lambda: True).cancel()], True), + ([SimpleToken(), SimpleToken()], False), + ([SimpleToken(), SimpleToken(), SimpleToken()], False), + ([SimpleToken(), SimpleToken(), SimpleToken(), SimpleToken()], False), + ([ConditionToken(lambda: False), ConditionToken(lambda: False)], False), + ([ConditionToken(lambda: False), ConditionToken(lambda: False), ConditionToken(lambda: False)], False), +]) +def test_just_created_condition_token_with_arguments(arguments, expected_cancelled_status): + assert ConditionToken(lambda: False, *arguments).cancelled == expected_cancelled_status + assert ConditionToken(lambda: False, *arguments).is_cancelled() == expected_cancelled_status + assert ConditionToken(lambda: False, *arguments).keep_on() == (not expected_cancelled_status) diff --git a/tests/tokens/test_simple_token.py b/tests/tokens/test_simple_token.py index fba0ee2..d7ee9cb 100644 --- a/tests/tokens/test_simple_token.py +++ b/tests/tokens/test_simple_token.py @@ -13,6 +13,7 @@ def test_just_created_token_without_arguments(): ([SimpleToken(), SimpleToken().cancel()], True), ([SimpleToken()], False), ([SimpleToken().cancel()], True), + ([SimpleToken(SimpleToken().cancel())], True), ([SimpleToken(), SimpleToken()], False), ([SimpleToken(), SimpleToken(), SimpleToken()], False), ([SimpleToken(), SimpleToken(), SimpleToken(), SimpleToken()], False), @@ -30,3 +31,9 @@ def test_stopped_token_is_not_going_on(): assert token.cancelled == True assert token.is_cancelled() == True assert token.keep_on() == False + + +def test_chain_with_simple_tokens(): + assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken(cancelled=True))))).cancelled == True + assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken().cancel())))).cancelled == True + assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken())))).cancelled == False From 62f7ebd2f3b4e989b2ec2de7044ac47c5de965a9 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 17:29:40 +0300 Subject: [PATCH 012/110] setter for the cancelled attribute --- ctok/tokens/abstract_token.py | 8 ++++++++ tests/tokens/test_simple_token.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/ctok/tokens/abstract_token.py b/ctok/tokens/abstract_token.py index fcffb36..5828d98 100644 --- a/ctok/tokens/abstract_token.py +++ b/ctok/tokens/abstract_token.py @@ -20,6 +20,14 @@ def __str__(self): def cancelled(self) -> bool: return self.is_cancelled() + @cancelled.setter + def cancelled(self, new_value) -> None: + if new_value == True: + self._cancelled = True + else: + if self._cancelled == True: + raise ValueError('You cannot restore a cancelled token.') + def keep_on(self) -> bool: return not self.is_cancelled() diff --git a/tests/tokens/test_simple_token.py b/tests/tokens/test_simple_token.py index d7ee9cb..31aac4a 100644 --- a/tests/tokens/test_simple_token.py +++ b/tests/tokens/test_simple_token.py @@ -9,6 +9,12 @@ def test_just_created_token_without_arguments(): assert SimpleToken().keep_on() == True +def test_just_created_token_with_argument_cancelled(): + assert SimpleToken(cancelled=True).cancelled == True + assert SimpleToken(cancelled=True).is_cancelled() == True + assert SimpleToken(cancelled=True).keep_on() == False + + @pytest.mark.parametrize('arguments,expected_cancelled_status', [ ([SimpleToken(), SimpleToken().cancel()], True), ([SimpleToken()], False), @@ -37,3 +43,28 @@ def test_chain_with_simple_tokens(): assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken(cancelled=True))))).cancelled == True assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken().cancel())))).cancelled == True assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken())))).cancelled == False + + +def test_change_attribute_cancelled_from_false_to_true(): + token = SimpleToken() + token.cancelled = True + assert token.cancelled == True + + +def test_change_attribute_cancelled_from_true_to_true(): + token = SimpleToken(cancelled=True) + token.cancelled = True + assert token.cancelled == True + + +def test_change_attribute_cancelled_from_true_to_false(): + token = SimpleToken(cancelled=True) + + with pytest.raises(ValueError): + token.cancelled = False + + +def test_change_attribute_cancelled_from_false_to_false(): + token = SimpleToken() + token.cancelled = False + assert token.cancelled == False From 0132a0819444fe99144a7e0ab607c95044c06aaf Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 17:40:54 +0300 Subject: [PATCH 013/110] test --- tests/tokens/test_condition_token.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index ea6a6bc..f6edb77 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -53,3 +53,8 @@ def test_just_created_condition_token_with_arguments(arguments, expected_cancell assert ConditionToken(lambda: False, *arguments).cancelled == expected_cancelled_status assert ConditionToken(lambda: False, *arguments).is_cancelled() == expected_cancelled_status assert ConditionToken(lambda: False, *arguments).keep_on() == (not expected_cancelled_status) + + +def test_raise_without_first_argument(): + with pytest.raises(TypeError): + ConditionToken() From 7884bdca6f480ddcfb17bbfa88238f26b4c9cd5d Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 18:33:17 +0300 Subject: [PATCH 014/110] tests --- ctok/__init__.py | 2 ++ ctok/tokens/condition_token.py | 4 +-- tests/tokens/test_abstract_token.py | 56 +++++++++++++++++++++++++++++ tests/tokens/test_simple_token.py | 27 +------------- 4 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 tests/tokens/test_abstract_token.py diff --git a/ctok/__init__.py b/ctok/__init__.py index e69de29..24fe507 100644 --- a/ctok/__init__.py +++ b/ctok/__init__.py @@ -0,0 +1,2 @@ +from ctok.tokens.simple_token import SimpleToken +from ctok.tokens.condition_token import ConditionToken diff --git a/ctok/tokens/condition_token.py b/ctok/tokens/condition_token.py index fb6df40..923910d 100644 --- a/ctok/tokens/condition_token.py +++ b/ctok/tokens/condition_token.py @@ -5,8 +5,8 @@ class ConditionToken(AbstractToken): - def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True): - super().__init__(*tokens) + def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True, cancelled=False): + super().__init__(*tokens, cancelled=cancelled) self.function = function self.suppress_exceptions = suppress_exceptions diff --git a/tests/tokens/test_abstract_token.py b/tests/tokens/test_abstract_token.py new file mode 100644 index 0000000..ae58ade --- /dev/null +++ b/tests/tokens/test_abstract_token.py @@ -0,0 +1,56 @@ +from functools import partial + +import pytest + +from ctok.tokens.abstract_token import AbstractToken +from ctok import SimpleToken, ConditionToken + + +ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken] +ALL_ARGUMENTS_FOR_TOKEN_CLASSES = [tuple(), (lambda: False, )] +ALL_TOKENS_FABRICS = [partial(token_class, *arguments) for token_class, arguments in zip(ALL_TOKEN_CLASSES, ALL_ARGUMENTS_FOR_TOKEN_CLASSES)] + + + +def test_cant_instantiate_abstract_token(): + with pytest.raises(TypeError): + AbstractToken() + + +@pytest.mark.parametrize( + 'cancelled_flag', + [True, False], +) +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +def test_cancelled_true_as_parameter(token_fabric, cancelled_flag): + token = token_fabric(cancelled=cancelled_flag) + assert token.cancelled == cancelled_flag + + +@pytest.mark.parametrize( + 'first_cancelled_flag,second_cancelled_flag,expected_value', + [ + (True, True, True), + (False, False, False), + (False, True, True), + (True, False, None), + ], +) +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +def test_change_attribute_cancelled(token_fabric, first_cancelled_flag, second_cancelled_flag, expected_value): + token = token_fabric(cancelled=first_cancelled_flag) + + + if expected_value is None: + with pytest.raises(ValueError): + token.cancelled = second_cancelled_flag + + else: + token.cancelled = second_cancelled_flag + assert token.cancelled == expected_value diff --git a/tests/tokens/test_simple_token.py b/tests/tokens/test_simple_token.py index 31aac4a..39ead72 100644 --- a/tests/tokens/test_simple_token.py +++ b/tests/tokens/test_simple_token.py @@ -1,6 +1,6 @@ import pytest -from ctok.tokens.simple_token import SimpleToken +from ctok import SimpleToken def test_just_created_token_without_arguments(): @@ -43,28 +43,3 @@ def test_chain_with_simple_tokens(): assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken(cancelled=True))))).cancelled == True assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken().cancel())))).cancelled == True assert SimpleToken(SimpleToken(SimpleToken(SimpleToken(SimpleToken())))).cancelled == False - - -def test_change_attribute_cancelled_from_false_to_true(): - token = SimpleToken() - token.cancelled = True - assert token.cancelled == True - - -def test_change_attribute_cancelled_from_true_to_true(): - token = SimpleToken(cancelled=True) - token.cancelled = True - assert token.cancelled == True - - -def test_change_attribute_cancelled_from_true_to_false(): - token = SimpleToken(cancelled=True) - - with pytest.raises(ValueError): - token.cancelled = False - - -def test_change_attribute_cancelled_from_false_to_false(): - token = SimpleToken() - token.cancelled = False - assert token.cancelled == False From ad520c0f7cc47fabd3486672d839d8856ef0335f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 19:09:38 +0300 Subject: [PATCH 015/110] timeout token --- ctok/__init__.py | 4 ++++ ctok/tokens/condition_token.py | 2 +- ctok/tokens/timeout_token.py | 17 +++++++++++++++++ tests/tokens/test_condition_token.py | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 ctok/tokens/timeout_token.py diff --git a/ctok/__init__.py b/ctok/__init__.py index 24fe507..a717335 100644 --- a/ctok/__init__.py +++ b/ctok/__init__.py @@ -1,2 +1,6 @@ from ctok.tokens.simple_token import SimpleToken from ctok.tokens.condition_token import ConditionToken +from ctok.tokens.timeout_token import TimeoutToken + + +TimeOutToken = TimeoutToken diff --git a/ctok/tokens/condition_token.py b/ctok/tokens/condition_token.py index 923910d..93ee1f6 100644 --- a/ctok/tokens/condition_token.py +++ b/ctok/tokens/condition_token.py @@ -5,7 +5,7 @@ class ConditionToken(AbstractToken): - def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True, cancelled=False): + def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True, cancelled: bool = False): super().__init__(*tokens, cancelled=cancelled) self.function = function self.suppress_exceptions = suppress_exceptions diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py new file mode 100644 index 0000000..d76be3a --- /dev/null +++ b/ctok/tokens/timeout_token.py @@ -0,0 +1,17 @@ +from time import monotonic as current_time +from typing import Union + +from ctok.tokens.abstract_token import AbstractToken +from ctok import ConditionToken + + +class TimeoutToken(ConditionToken): + def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False): + if timeout < 0: + raise ValueError + + start_time = current_time() + def function() -> bool: + return (start_time + timeout) > current_time() + + super().__init__(function, *tokens, cancelled=cancelled) diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index f6edb77..5f53f1b 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -5,6 +5,7 @@ from ctok.tokens.condition_token import ConditionToken from ctok.tokens.simple_token import SimpleToken + def test_condition_counter(): loop_size = 5 def condition(): From a42004017b2ab84a73897778317df98c966a3600 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 19:16:48 +0300 Subject: [PATCH 016/110] timeout token fix + test --- ctok/tokens/timeout_token.py | 2 +- tests/tokens/test_timeout_token.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/tokens/test_timeout_token.py diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index d76be3a..d65d92f 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -12,6 +12,6 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled start_time = current_time() def function() -> bool: - return (start_time + timeout) > current_time() + return (start_time + timeout) < current_time() super().__init__(function, *tokens, cancelled=cancelled) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py new file mode 100644 index 0000000..b7b4f62 --- /dev/null +++ b/tests/tokens/test_timeout_token.py @@ -0,0 +1,10 @@ +from ctok import TimeoutToken + + +def test_zero_timeout(): + assert TimeoutToken(0).cancelled == True + assert TimeoutToken(0.0).cancelled == True + assert TimeoutToken(0).is_cancelled() == True + assert TimeoutToken(0.0).is_cancelled() == True + assert TimeoutToken(0).keep_on() == False + assert TimeoutToken(0.0).keep_on() == False From f4c26e023da259b9fabf92b82a02f73df092fe1e Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 19:19:12 +0300 Subject: [PATCH 017/110] tokens imports --- tests/tokens/test_abstract_token.py | 6 +++--- tests/tokens/test_condition_token.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/tokens/test_abstract_token.py b/tests/tokens/test_abstract_token.py index ae58ade..4513703 100644 --- a/tests/tokens/test_abstract_token.py +++ b/tests/tokens/test_abstract_token.py @@ -3,11 +3,11 @@ import pytest from ctok.tokens.abstract_token import AbstractToken -from ctok import SimpleToken, ConditionToken +from ctok import SimpleToken, ConditionToken, TimeoutToken -ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken] -ALL_ARGUMENTS_FOR_TOKEN_CLASSES = [tuple(), (lambda: False, )] +ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken, TimeoutToken] +ALL_ARGUMENTS_FOR_TOKEN_CLASSES = [tuple(), (lambda: False, ), (15, )] ALL_TOKENS_FABRICS = [partial(token_class, *arguments) for token_class, arguments in zip(ALL_TOKEN_CLASSES, ALL_ARGUMENTS_FOR_TOKEN_CLASSES)] diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index 5f53f1b..23d4916 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -2,8 +2,7 @@ import pytest -from ctok.tokens.condition_token import ConditionToken -from ctok.tokens.simple_token import SimpleToken +from ctok import SimpleToken, ConditionToken def test_condition_counter(): From 80d0c26c39ef9bda7c107a807ce56d159937a7b6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 19:28:42 +0300 Subject: [PATCH 018/110] current time fix --- ctok/tokens/timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index d65d92f..5a25af0 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -12,6 +12,6 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled start_time = current_time() def function() -> bool: - return (start_time + timeout) < current_time() + return current_time() > (start_time + timeout) super().__init__(function, *tokens, cancelled=cancelled) From 41df17000d5872169dd1828293b6152a51dc12b6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 19:49:25 +0300 Subject: [PATCH 019/110] time import --- ctok/tokens/timeout_token.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index 5a25af0..93b79fb 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -1,4 +1,8 @@ -from time import monotonic as current_time +try: + from time import monotonic_ns as current_time +except ImportError: + from time import monotonic as current_time + from typing import Union from ctok.tokens.abstract_token import AbstractToken From fb3bc1f4809257242023a34c06f94137850a6278 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 19:53:28 +0300 Subject: [PATCH 020/110] nanosec timeout --- ctok/tokens/timeout_token.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index 93b79fb..40b1770 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -1,7 +1,4 @@ -try: - from time import monotonic_ns as current_time -except ImportError: - from time import monotonic as current_time +from time import monotonic_ns as current_time from typing import Union @@ -14,6 +11,8 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled if timeout < 0: raise ValueError + timeout *= 1_000_000_000 + start_time = current_time() def function() -> bool: return current_time() > (start_time + timeout) From beab7f1d2a6702e14301ac21b46cbdab640ca578 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 20:02:10 +0300 Subject: [PATCH 021/110] refactoring of a test --- tests/tokens/test_timeout_token.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index b7b4f62..5157b61 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -1,10 +1,18 @@ +import pytest + from ctok import TimeoutToken -def test_zero_timeout(): - assert TimeoutToken(0).cancelled == True - assert TimeoutToken(0.0).cancelled == True - assert TimeoutToken(0).is_cancelled() == True - assert TimeoutToken(0.0).is_cancelled() == True - assert TimeoutToken(0).keep_on() == False - assert TimeoutToken(0.0).keep_on() == False +@pytest.mark.parametrize( + 'zero_timeout', + [0, 0.0], +) +def test_zero_timeout(zero_timeout): + token = TimeoutToken(zero_timeout) + + assert token.cancelled == True + assert token.cancelled == True + assert token.is_cancelled() == True + assert token.is_cancelled() == True + assert token.keep_on() == False + assert token.keep_on() == False From 7e6c8ddc077cd9caba9f36903ccda7eda89262cd Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 20:02:51 +0300 Subject: [PATCH 022/110] sleep in a test --- tests/tokens/test_timeout_token.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 5157b61..2dd73eb 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -1,3 +1,5 @@ +from time import sleep + import pytest from ctok import TimeoutToken @@ -10,6 +12,8 @@ def test_zero_timeout(zero_timeout): token = TimeoutToken(zero_timeout) + sleep(0.0001) + assert token.cancelled == True assert token.cancelled == True assert token.is_cancelled() == True From e399206850ced2798ded532fb6f3f9b5dfeaf3a1 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 20:04:47 +0300 Subject: [PATCH 023/110] sleep in a test --- tests/tokens/test_timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 2dd73eb..338b851 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -12,7 +12,7 @@ def test_zero_timeout(zero_timeout): token = TimeoutToken(zero_timeout) - sleep(0.0001) + sleep(0.001) assert token.cancelled == True assert token.cancelled == True From cfa150d68b8093bb94f7e098a5627afbc4f3608b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 20:11:22 +0300 Subject: [PATCH 024/110] sleep in a test --- tests/tokens/test_timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 338b851..21f1350 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -12,7 +12,7 @@ def test_zero_timeout(zero_timeout): token = TimeoutToken(zero_timeout) - sleep(0.001) + sleep(1) assert token.cancelled == True assert token.cancelled == True From bfcde8c7c558e87da6495a4b355b90e359f03dc4 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 20:13:45 +0300 Subject: [PATCH 025/110] sleep in a test --- tests/tokens/test_timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 21f1350..9228b22 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -12,7 +12,7 @@ def test_zero_timeout(zero_timeout): token = TimeoutToken(zero_timeout) - sleep(1) + sleep(0.1) assert token.cancelled == True assert token.cancelled == True From ff2372b7402e8667afaaa175f33013cd9aa71592 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:13:54 +0300 Subject: [PATCH 026/110] fix of the timeout token + removing of a sleep from a test --- ctok/tokens/timeout_token.py | 2 +- tests/tokens/test_timeout_token.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index 40b1770..d7ebcb4 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -15,6 +15,6 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled start_time = current_time() def function() -> bool: - return current_time() > (start_time + timeout) + return current_time() >= (start_time + timeout) super().__init__(function, *tokens, cancelled=cancelled) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 9228b22..6fa3a0d 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -12,8 +12,6 @@ def test_zero_timeout(zero_timeout): token = TimeoutToken(zero_timeout) - sleep(0.1) - assert token.cancelled == True assert token.cancelled == True assert token.is_cancelled() == True From 8e0e0d2333d17d5cd41b2d6a4021efc7002b6584 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:23:49 +0300 Subject: [PATCH 027/110] monotonic as an option --- ctok/tokens/timeout_token.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index d7ebcb4..72fa0cb 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -1,4 +1,4 @@ -from time import monotonic_ns as current_time +from time import monotonic_ns, perf_counter from typing import Union @@ -7,14 +7,20 @@ class TimeoutToken(ConditionToken): - def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False): + def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False, monotonic: bool = True): if timeout < 0: raise ValueError - timeout *= 1_000_000_000 + if monotonic: + timeout *= 1_000_000_000 - start_time = current_time() - def function() -> bool: - return current_time() >= (start_time + timeout) + start_time = monotonic_ns() + def function() -> bool: + return monotonic_ns() >= (start_time + timeout) + + else: + start_time = perf_counter() + def function() -> bool: + return perf_counter() >= (start_time + timeout) super().__init__(function, *tokens, cancelled=cancelled) From a106b7a93736f88086986f2d999d30f4cbf7491f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:25:49 +0300 Subject: [PATCH 028/110] test --- tests/tokens/test_timeout_token.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 6fa3a0d..f562636 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -5,12 +5,23 @@ from ctok import TimeoutToken +@pytest.mark.parametrize( + 'options', + [ + {}, + {'monotonic': True}, + {'monotonic': False}, + ], +) @pytest.mark.parametrize( 'zero_timeout', - [0, 0.0], + [ + 0, + 0.0, + ], ) -def test_zero_timeout(zero_timeout): - token = TimeoutToken(zero_timeout) +def test_zero_timeout(zero_timeout, options): + token = TimeoutToken(zero_timeout, **options) assert token.cancelled == True assert token.cancelled == True From 738f279b653ccb9258f5ec79c6ade83e7f132014 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:31:22 +0300 Subject: [PATCH 029/110] tests --- tests/tokens/test_timeout_token.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index f562636..004c71f 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -1,5 +1,3 @@ -from time import sleep - import pytest from ctok import TimeoutToken @@ -29,3 +27,28 @@ def test_zero_timeout(zero_timeout, options): assert token.is_cancelled() == True assert token.keep_on() == False assert token.keep_on() == False + + +@pytest.mark.parametrize( + 'options', + [ + {}, + {'monotonic': True}, + {'monotonic': False}, + ], +) +@pytest.mark.parametrize( + 'timeout', + [ + -1, + -0.5, + ], +) +def test_less_than_zero_timeout(options, timeout): + with pytest.raises(ValueError): + TimeoutToken(timeout, **options) + + +def test_raise_without_first_argument(): + with pytest.raises(TypeError): + TimeoutToken() From 2754e571f34b731d85cd49e67c8b97e76d601464 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:33:57 +0300 Subject: [PATCH 030/110] cant remember what --- ctok/tokens/timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index 72fa0cb..748a000 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -9,7 +9,7 @@ class TimeoutToken(ConditionToken): def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False, monotonic: bool = True): if timeout < 0: - raise ValueError + raise ValueError('') if monotonic: timeout *= 1_000_000_000 From 6f517e738ea6c394d969191b909377372543e50f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:35:04 +0300 Subject: [PATCH 031/110] exception message --- ctok/tokens/timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index 748a000..acedac4 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -9,7 +9,7 @@ class TimeoutToken(ConditionToken): def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False, monotonic: bool = True): if timeout < 0: - raise ValueError('') + raise ValueError('You cannot specify a timeout less than zero.') if monotonic: timeout *= 1_000_000_000 From a830a90fb3efe268d498a00d6888283508898845 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:39:03 +0300 Subject: [PATCH 032/110] test --- tests/tokens/test_timeout_token.py | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 004c71f..e1d495a 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -1,3 +1,5 @@ +from time import sleep + import pytest from ctok import TimeoutToken @@ -52,3 +54,32 @@ def test_less_than_zero_timeout(options, timeout): def test_raise_without_first_argument(): with pytest.raises(TypeError): TimeoutToken() + + +@pytest.mark.parametrize( + 'options', + [ + {}, + {'monotonic': True}, + {'monotonic': False}, + ], +) +def test_timeout_expired(options): + timeout = 0.1 + token = TimeoutToken(timeout, **options) + + assert token.cancelled == False + assert token.cancelled == False + assert token.is_cancelled() == False + assert token.is_cancelled() == False + assert token.keep_on() == True + assert token.keep_on() == True + + sleep(timeout) + + assert token.cancelled == True + assert token.cancelled == True + assert token.is_cancelled() == True + assert token.is_cancelled() == True + assert token.keep_on() == False + assert token.keep_on() == False From a0fb14247418965b3fd12ce9130373ebbb7265fa Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:46:32 +0300 Subject: [PATCH 033/110] new tests --- tests/tokens/test_condition_token.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index 23d4916..97e4ef9 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -58,3 +58,31 @@ def test_just_created_condition_token_with_arguments(arguments, expected_cancell def test_raise_without_first_argument(): with pytest.raises(TypeError): ConditionToken() + + +def test_suppress_exception_false(): + def condition(): + raise ValueError + + token = ConditionToken(condition, suppress_exceptions=False) + + with pytest.raises(ValueError): + token.cancelled + + +def test_suppress_exception_true(): + def condition(): + raise ValueError + + token = ConditionToken(condition, suppress_exceptions=True) + + assert token.cancelled == False + + +def test_suppress_exception_default_true(): + def condition(): + raise ValueError + + token = ConditionToken(condition) + + assert token.cancelled == False From 01460f67808e80570f3aa3fd110c64ad342819f6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:48:19 +0300 Subject: [PATCH 034/110] timeout in a test --- tests/tokens/test_timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index e1d495a..16146d7 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -75,7 +75,7 @@ def test_timeout_expired(options): assert token.keep_on() == True assert token.keep_on() == True - sleep(timeout) + sleep(timeout * 2) assert token.cancelled == True assert token.cancelled == True From 9255566a12492a8fb7b774935ae54a38445ddddc Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 22:56:03 +0300 Subject: [PATCH 035/110] text representation of superpower --- ctok/tokens/abstract_token.py | 4 ++++ ctok/tokens/condition_token.py | 3 +++ ctok/tokens/simple_token.py | 3 +++ ctok/tokens/timeout_token.py | 5 +++++ 4 files changed, 15 insertions(+) diff --git a/ctok/tokens/abstract_token.py b/ctok/tokens/abstract_token.py index 5828d98..1eead91 100644 --- a/ctok/tokens/abstract_token.py +++ b/ctok/tokens/abstract_token.py @@ -50,3 +50,7 @@ def cancel(self) -> 'AbstractToken': @abstractmethod def superpower(self) -> bool: # pragma: no cover pass + + @abstractmethod + def text_representation_of_superpower(self) -> str: # pragma: no cover + pass diff --git a/ctok/tokens/condition_token.py b/ctok/tokens/condition_token.py index 93ee1f6..b113e6c 100644 --- a/ctok/tokens/condition_token.py +++ b/ctok/tokens/condition_token.py @@ -19,3 +19,6 @@ def superpower(self) -> bool: return self.function() return False + + def text_representation_of_superpower(self) -> str: + return repr(self.function) diff --git a/ctok/tokens/simple_token.py b/ctok/tokens/simple_token.py index 6143b08..34bbf90 100644 --- a/ctok/tokens/simple_token.py +++ b/ctok/tokens/simple_token.py @@ -4,3 +4,6 @@ class SimpleToken(AbstractToken): def superpower(self) -> bool: return False + + def text_representation_of_superpower(self) -> str: + return '' diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index acedac4..d62a295 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -11,6 +11,8 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled if timeout < 0: raise ValueError('You cannot specify a timeout less than zero.') + self.timeout = timeout + if monotonic: timeout *= 1_000_000_000 @@ -24,3 +26,6 @@ def function() -> bool: return perf_counter() >= (start_time + timeout) super().__init__(function, *tokens, cancelled=cancelled) + + def text_representation_of_superpower(self) -> str: + return str(self.timeout) From 5c18fc7fd24eee96040cbfe6490a30d35c114c6c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 23:07:04 +0300 Subject: [PATCH 036/110] superpower printing --- ctok/tokens/abstract_token.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ctok/tokens/abstract_token.py b/ctok/tokens/abstract_token.py index 1eead91..b4748e1 100644 --- a/ctok/tokens/abstract_token.py +++ b/ctok/tokens/abstract_token.py @@ -10,7 +10,10 @@ def __repr__(self): other_tokens = ', '.join([repr(x) for x in self.tokens]) if other_tokens: other_tokens += ', ' - return f'{type(self).__name__}({other_tokens}cancelled={self.cancelled})' + superpower = self.text_representation_of_superpower() + if superpower: + superpower += ', ' + return f'{type(self).__name__}({superpower}{other_tokens}cancelled={self.cancelled})' def __str__(self): cancelled_flag = 'cancelled' if self.cancelled else 'not cancelled' From 47a99a99c59b07092fd1e4e10eccb75e8de1df58 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 23:09:06 +0300 Subject: [PATCH 037/110] tests --- tests/tokens/test_abstract_token.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/tokens/test_abstract_token.py b/tests/tokens/test_abstract_token.py index 4513703..89bdf76 100644 --- a/tests/tokens/test_abstract_token.py +++ b/tests/tokens/test_abstract_token.py @@ -54,3 +54,28 @@ def test_change_attribute_cancelled(token_fabric, first_cancelled_flag, second_c else: token.cancelled = second_cancelled_flag assert token.cancelled == expected_value + + +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +def test_repr(token_fabric): + token = token_fabric() + superpower_text = token.text_representation_of_superpower() + + assert repr(token) == type(token).__name__ + '(' + ('' if not superpower_text else f'{superpower_text}, ') + 'cancelled=False' + ')' + + +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +def test_str(token_fabric): + token = token_fabric() + + assert str(token) == '<' + type(token).__name__ + ' (not cancelled)>' + + token.cancel() + + assert str(token) == '<' + type(token).__name__ + ' (cancelled)>' From b6962f1efe3d5fa5b8ad11812c974c4fec138c23 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 23:16:51 +0300 Subject: [PATCH 038/110] tests --- tests/tokens/test_abstract_token.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/tokens/test_abstract_token.py b/tests/tokens/test_abstract_token.py index 89bdf76..cdecc2a 100644 --- a/tests/tokens/test_abstract_token.py +++ b/tests/tokens/test_abstract_token.py @@ -67,6 +67,19 @@ def test_repr(token_fabric): assert repr(token) == type(token).__name__ + '(' + ('' if not superpower_text else f'{superpower_text}, ') + 'cancelled=False' + ')' +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +def test_repr_with_another_token(token_fabric): + another_token = token_fabric() + token = token_fabric(another_token) + + superpower_text = token.text_representation_of_superpower() + + assert repr(token) == type(token).__name__ + '(' + ('' if not superpower_text else f'{superpower_text}, ') + repr(another_token) + ', ' + 'cancelled=False' + ')' + + @pytest.mark.parametrize( 'token_fabric', ALL_TOKENS_FABRICS, From 4fd82db2fe8c738d6767bd8107098bbfdfc4be46 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 23:31:19 +0300 Subject: [PATCH 039/110] case when the condition function is returning a not bool value --- ctok/tokens/condition_token.py | 15 +++++++++++++-- tests/tokens/test_condition_token.py | 8 ++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ctok/tokens/condition_token.py b/ctok/tokens/condition_token.py index b113e6c..6de27bd 100644 --- a/ctok/tokens/condition_token.py +++ b/ctok/tokens/condition_token.py @@ -12,13 +12,24 @@ def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppres def superpower(self) -> bool: if not self.suppress_exceptions: - return self.function() + return self.run_function() else: with suppress(Exception): - return self.function() + return self.run_function() return False + def run_function(self) -> bool: + result = self.function() + + if not isinstance(result, bool): + if not self.suppress_exceptions: + raise TypeError(f'The condition function can only return a bool value. The passed function returned "{result}" ({type(result).__name__}).') + else: + return False + + return result + def text_representation_of_superpower(self) -> str: return repr(self.function) diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index 97e4ef9..11830cf 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -86,3 +86,11 @@ def condition(): token = ConditionToken(condition) assert token.cancelled == False + + +def test_condition_function_returning_not_bool_value(): + assert ConditionToken(lambda: 'kek', suppress_exceptions=True).cancelled == False + assert ConditionToken(lambda: 'kek').cancelled == False + + with pytest.raises(TypeError): + ConditionToken(lambda: 'kek', suppress_exceptions=False).cancelled From e4c48a5b83da07fda41f7fd7e0a34a9020252ac2 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 23:35:54 +0300 Subject: [PATCH 040/110] default value if an exception raised --- ctok/tokens/condition_token.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ctok/tokens/condition_token.py b/ctok/tokens/condition_token.py index 6de27bd..f934c5e 100644 --- a/ctok/tokens/condition_token.py +++ b/ctok/tokens/condition_token.py @@ -5,10 +5,11 @@ class ConditionToken(AbstractToken): - def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True, cancelled: bool = False): + def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True, cancelled: bool = False, default: bool = False): super().__init__(*tokens, cancelled=cancelled) self.function = function self.suppress_exceptions = suppress_exceptions + self.default = default def superpower(self) -> bool: if not self.suppress_exceptions: @@ -18,7 +19,7 @@ def superpower(self) -> bool: with suppress(Exception): return self.run_function() - return False + return self.default def run_function(self) -> bool: result = self.function() @@ -27,7 +28,7 @@ def run_function(self) -> bool: if not self.suppress_exceptions: raise TypeError(f'The condition function can only return a bool value. The passed function returned "{result}" ({type(result).__name__}).') else: - return False + return self.default return result From e8ef0c6625c2bd1ec660addabb454e358801bb17 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 23:53:03 +0300 Subject: [PATCH 041/110] text representation of extra kwargs --- ctok/tokens/abstract_token.py | 8 +++++++- ctok/tokens/condition_token.py | 9 ++++++++- ctok/tokens/timeout_token.py | 4 ++++ tests/tokens/test_abstract_token.py | 6 ++++-- tests/tokens/test_condition_token.py | 16 ++++++++++++++++ tests/tokens/test_timeout_token.py | 6 ++++++ 6 files changed, 45 insertions(+), 4 deletions(-) diff --git a/ctok/tokens/abstract_token.py b/ctok/tokens/abstract_token.py index b4748e1..62f4a03 100644 --- a/ctok/tokens/abstract_token.py +++ b/ctok/tokens/abstract_token.py @@ -13,7 +13,10 @@ def __repr__(self): superpower = self.text_representation_of_superpower() if superpower: superpower += ', ' - return f'{type(self).__name__}({superpower}{other_tokens}cancelled={self.cancelled})' + extra_kwargs = self.text_representation_of_extra_kwargs() + if extra_kwargs: + extra_kwargs = ', ' + extra_kwargs + return f'{type(self).__name__}({superpower}{other_tokens}cancelled={self.cancelled}{extra_kwargs})' def __str__(self): cancelled_flag = 'cancelled' if self.cancelled else 'not cancelled' @@ -57,3 +60,6 @@ def superpower(self) -> bool: # pragma: no cover @abstractmethod def text_representation_of_superpower(self) -> str: # pragma: no cover pass + + def text_representation_of_extra_kwargs(self) -> str: + return '' diff --git a/ctok/tokens/condition_token.py b/ctok/tokens/condition_token.py index f934c5e..c340184 100644 --- a/ctok/tokens/condition_token.py +++ b/ctok/tokens/condition_token.py @@ -5,7 +5,7 @@ class ConditionToken(AbstractToken): - def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, suppress_exceptions: bool = True, cancelled: bool = False, default: bool = False): + def __init__(self, function: Callable[[], bool], *tokens: AbstractToken, cancelled: bool = False, suppress_exceptions: bool = True, default: bool = False): super().__init__(*tokens, cancelled=cancelled) self.function = function self.suppress_exceptions = suppress_exceptions @@ -34,3 +34,10 @@ 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 = { + 'suppress_exceptions': self.suppress_exceptions, + 'default': self.default, + } + return ', '.join([f'{key}={value}' for key, value in extra_kwargs.items()]) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index d62a295..f7104a3 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -12,6 +12,7 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled raise ValueError('You cannot specify a timeout less than zero.') self.timeout = timeout + self.monotonic = monotonic if monotonic: timeout *= 1_000_000_000 @@ -29,3 +30,6 @@ 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}' diff --git a/tests/tokens/test_abstract_token.py b/tests/tokens/test_abstract_token.py index cdecc2a..73b83cf 100644 --- a/tests/tokens/test_abstract_token.py +++ b/tests/tokens/test_abstract_token.py @@ -63,8 +63,9 @@ def test_change_attribute_cancelled(token_fabric, first_cancelled_flag, second_c def test_repr(token_fabric): token = token_fabric() superpower_text = token.text_representation_of_superpower() + extra_kwargs_text = token.text_representation_of_extra_kwargs() - assert repr(token) == type(token).__name__ + '(' + ('' if not superpower_text else f'{superpower_text}, ') + 'cancelled=False' + ')' + assert repr(token) == type(token).__name__ + '(' + ('' if not superpower_text else f'{superpower_text}, ') + 'cancelled=False' + (', ' + extra_kwargs_text if extra_kwargs_text else '') + ')' @pytest.mark.parametrize( @@ -76,8 +77,9 @@ def test_repr_with_another_token(token_fabric): token = token_fabric(another_token) superpower_text = token.text_representation_of_superpower() + extra_kwargs_text = token.text_representation_of_extra_kwargs() - assert repr(token) == type(token).__name__ + '(' + ('' if not superpower_text else f'{superpower_text}, ') + repr(another_token) + ', ' + 'cancelled=False' + ')' + assert repr(token) == type(token).__name__ + '(' + ('' if not superpower_text else f'{superpower_text}, ') + repr(another_token) + ', ' + 'cancelled=False' + (', ' + extra_kwargs_text if extra_kwargs_text else '') + ')' @pytest.mark.parametrize( diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index 11830cf..f7a4a6d 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -94,3 +94,19 @@ def test_condition_function_returning_not_bool_value(): with pytest.raises(TypeError): ConditionToken(lambda: 'kek', suppress_exceptions=False).cancelled + + +@pytest.mark.parametrize( + 'suppress_exceptions_flag', + [True, False], +) +@pytest.mark.parametrize( + 'default_flag', + [True, False], +) +def test_test_representaion_of_extra_kwargs(suppress_exceptions_flag, default_flag): + assert ConditionToken( + lambda: False, + suppress_exceptions=suppress_exceptions_flag, + default=default_flag, + ).text_representation_of_extra_kwargs() == f'suppress_exceptions={suppress_exceptions_flag}, default={default_flag}' diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 16146d7..4a1e818 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -83,3 +83,9 @@ def test_timeout_expired(options): assert token.is_cancelled() == True assert token.keep_on() == False assert token.keep_on() == False + + +def test_test_representaion_of_extra_kwargs(): + assert TimeoutToken(5, monotonic=False).text_representation_of_extra_kwargs() == 'monotonic=False' + assert TimeoutToken(5, monotonic=True).text_representation_of_extra_kwargs() == 'monotonic=True' + assert TimeoutToken(5).text_representation_of_extra_kwargs() == 'monotonic=True' From a29316544f9aebfd6283eb256a7fad55260b0a54 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 20 Sep 2023 23:56:57 +0300 Subject: [PATCH 042/110] tests --- tests/tokens/test_condition_token.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index f7a4a6d..4f1dbdd 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -110,3 +110,29 @@ def test_test_representaion_of_extra_kwargs(suppress_exceptions_flag, default_fl suppress_exceptions=suppress_exceptions_flag, default=default_flag, ).text_representation_of_extra_kwargs() == f'suppress_exceptions={suppress_exceptions_flag}, default={default_flag}' + + +@pytest.mark.parametrize( + 'default', + [True, False], +) +def test_default_if_exception(default): + def condition(): + raise ValueError + + token = ConditionToken(condition, suppress_exceptions=True, default=default) + + assert token.cancelled == default + + +@pytest.mark.parametrize( + 'default', + [True, False], +) +def test_default_if_not_bool(default): + def condition(): + return 'kek' + + token = ConditionToken(condition, suppress_exceptions=True, default=default) + + assert token.cancelled == default From 77b3b1f535b3c896e6951bfcd8270eacaae15870 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:04:08 +0300 Subject: [PATCH 043/110] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 08b156a..0d4036a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist __pycache__ .idea test.py +.mypy_cache From 607b04ec404e14beb8b3c1ff86ac87c4dccba258 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:19:58 +0300 Subject: [PATCH 044/110] type hints --- ctok/tokens/timeout_token.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index f7104a3..992c53c 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -1,6 +1,6 @@ from time import monotonic_ns, perf_counter -from typing import Union +from typing import Union, Callable from ctok.tokens.abstract_token import AbstractToken from ctok import ConditionToken @@ -14,17 +14,16 @@ def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled self.timeout = timeout self.monotonic = monotonic + timer: Callable[[], Union[int, float]] if monotonic: timeout *= 1_000_000_000 - - start_time = monotonic_ns() - def function() -> bool: - return monotonic_ns() >= (start_time + timeout) - + timer = monotonic_ns else: - start_time = perf_counter() - def function() -> bool: - return perf_counter() >= (start_time + timeout) + timer = perf_counter + + start_time: Union[int, float] = timer() + def function() -> bool: + return timer() >= (start_time + timeout) super().__init__(function, *tokens, cancelled=cancelled) From 790b67bbba3a5ebd5b7a0bfcf5abba6a02bfe91d Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:21:41 +0300 Subject: [PATCH 045/110] monotonic is not a default for timeout token --- ctok/tokens/timeout_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctok/tokens/timeout_token.py b/ctok/tokens/timeout_token.py index 992c53c..d5c1710 100644 --- a/ctok/tokens/timeout_token.py +++ b/ctok/tokens/timeout_token.py @@ -7,7 +7,7 @@ class TimeoutToken(ConditionToken): - def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False, monotonic: bool = True): + def __init__(self, timeout: Union[int, float], *tokens: AbstractToken, cancelled: bool = False, monotonic: bool = False): if timeout < 0: raise ValueError('You cannot specify a timeout less than zero.') From e0760ad3bc0b7fe3433e383629f59798976f97d0 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:22:19 +0300 Subject: [PATCH 046/110] monotonic is not a default for timeout token --- tests/tokens/test_timeout_token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 4a1e818..9145dfe 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -85,7 +85,7 @@ def test_timeout_expired(options): assert token.keep_on() == False -def test_test_representaion_of_extra_kwargs(): +def test_text_representaion_of_extra_kwargs(): assert TimeoutToken(5, monotonic=False).text_representation_of_extra_kwargs() == 'monotonic=False' assert TimeoutToken(5, monotonic=True).text_representation_of_extra_kwargs() == 'monotonic=True' - assert TimeoutToken(5).text_representation_of_extra_kwargs() == 'monotonic=True' + assert TimeoutToken(5).text_representation_of_extra_kwargs() == 'monotonic=False' From b15b86c7c4acdd1fc7d34fd6386bc7326f918a19 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:31:54 +0300 Subject: [PATCH 047/110] mypy in the requirements file --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index f3c0107..80a7718 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,3 +2,4 @@ pytest==7.4.2 coverage==7.2.7 twine==4.0.2 wheel==0.40.0 +mypy==1.5.1 From 5537e77b1be0c831c28baa6e8bfb62e5ad989b2e Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:37:10 +0300 Subject: [PATCH 048/110] ruff --- ctok/__init__.py | 4 ++-- requirements_dev.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ctok/__init__.py b/ctok/__init__.py index a717335..f9832fd 100644 --- a/ctok/__init__.py +++ b/ctok/__init__.py @@ -1,5 +1,5 @@ -from ctok.tokens.simple_token import SimpleToken -from ctok.tokens.condition_token import ConditionToken +from ctok.tokens.simple_token import SimpleToken # noqa: F401 +from ctok.tokens.condition_token import ConditionToken # noqa: F401 from ctok.tokens.timeout_token import TimeoutToken diff --git a/requirements_dev.txt b/requirements_dev.txt index 80a7718..5d6f9b0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,3 +3,4 @@ coverage==7.2.7 twine==4.0.2 wheel==0.40.0 mypy==1.5.1 +ruff==0.0.290 From 48a23c04bb5eb2be64a8bdfd41e3a7046ce0e245 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:37:59 +0300 Subject: [PATCH 049/110] lint in CI --- .github/workflows/lint.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..52c379c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint + +on: + push: + branches: + - main + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Run mypy + shell: bash + run: mypy ctok + + - name: Run ruff + shell: bash + run: ruff ctok From 45f7d5c229165bc5a4b7afc132859bc5eb47d4d6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:39:33 +0300 Subject: [PATCH 050/110] lint on all branches --- .github/workflows/lint.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 52c379c..9f000e8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,9 +1,7 @@ name: Lint on: - push: - branches: - - main + push jobs: build: From b1e4aff3588c499bb7d5309f4f4ca8ae75268e49 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:40:27 +0300 Subject: [PATCH 051/110] ruff --- .ruff.toml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ruff.toml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..ec5b9a4 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1 @@ +ignore = ['E501', 'E712'] From 6033aa4f571733d31aee03557e9c9f7693057b0d Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:41:02 +0300 Subject: [PATCH 052/110] version of mypy --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 5d6f9b0..cf6ad2b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,5 +2,5 @@ pytest==7.4.2 coverage==7.2.7 twine==4.0.2 wheel==0.40.0 -mypy==1.5.1 +mypy==1.4.1 ruff==0.0.290 From 5f0f5713dd379fa73cc2e992b74835141228810c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:50:17 +0300 Subject: [PATCH 053/110] release CI --- .github/workflows/release.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..079daeb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.11'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Upload to pypi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} From 69f9ec5412f59584436d39ce4e92cc129bc36f79 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:50:48 +0300 Subject: [PATCH 054/110] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0d4036a..e6bd197 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ .idea test.py .mypy_cache +.ruff_cache From 4d4684d421beebd97441591c352788023840dc4b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:56:04 +0300 Subject: [PATCH 055/110] version in own file --- .github/workflows/release.yml | 7 +++++++ setup.py | 5 ++++- version.txt | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 version.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 079daeb..e555b69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,13 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Create release + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.set-tag.outputs.tag_name }} + bodyFile: './release_notes.txt' + token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload to pypi uses: pypa/gh-action-pypi-publish@release/v1 with: diff --git a/setup.py b/setup.py index 770d4d8..702529c 100644 --- a/setup.py +++ b/setup.py @@ -4,11 +4,14 @@ with open('README.md', 'r', encoding='utf8') as readme_file: readme = readme_file.read() +with open('version.txt', 'r', encoding='utf8') as version_file: + version = version_file.read().strip() + requirements = [] setup( name='ctok', - version='0.0.1', + version=version, author='Evgeniy Blinov', author_email='zheni-b@yandex.ru', description='Implementation of the "Cancellation Token" pattern', diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..8acdd82 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.0.1 From 2163fde79a96bd76144eaf55b5ce534ea064d665 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 00:59:02 +0300 Subject: [PATCH 056/110] without a release block in CI --- .github/workflows/release.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e555b69..079daeb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,13 +21,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Create release - uses: ncipollo/release-action@v1 - with: - tag: ${{ steps.set-tag.outputs.tag_name }} - bodyFile: './release_notes.txt' - token: ${{ secrets.GITHUB_TOKEN }} - - name: Upload to pypi uses: pypa/gh-action-pypi-publish@release/v1 with: From 58d2f726998d6c11756b6a145a0ddf9f34d9e1ae Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 01:03:42 +0300 Subject: [PATCH 057/110] badges in the readme --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c3355d1..0dcb793 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ -# ctok \ No newline at end of file +[![Downloads](https://static.pepy.tech/badge/ctok/month)](https://pepy.tech/project/ctok) +[![Downloads](https://static.pepy.tech/badge/ctok)](https://pepy.tech/project/ctok) +[![codecov](https://codecov.io/gh/pomponchik/ctok/graph/badge.svg?token=8iyMsUaLvN)](https://codecov.io/gh/pomponchik/ctok) +[![Test-Package](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml) +[![Python versions](https://img.shields.io/pypi/pyversions/ctok.svg)](https://pypi.python.org/pypi/ctok) +[![PyPI version](https://badge.fury.io/py/ctok.svg)](https://badge.fury.io/py/ctok) + +# ctok From 03e6969aa76aecf669110d557f2dd97f9dd604ea Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 01:04:34 +0300 Subject: [PATCH 058/110] badges in the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0dcb793..b13c45a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Downloads](https://static.pepy.tech/badge/ctok/month)](https://pepy.tech/project/ctok) [![Downloads](https://static.pepy.tech/badge/ctok)](https://pepy.tech/project/ctok) -[![codecov](https://codecov.io/gh/pomponchik/ctok/graph/badge.svg?token=8iyMsUaLvN)](https://codecov.io/gh/pomponchik/ctok) +[![codecov](https://codecov.io/gh/pomponchik/ctok/graph/badge.svg?token=eZ4eK6fkmx)](https://codecov.io/gh/pomponchik/ctok) [![Test-Package](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml) [![Python versions](https://img.shields.io/pypi/pyversions/ctok.svg)](https://pypi.python.org/pypi/ctok) [![PyPI version](https://badge.fury.io/py/ctok.svg)](https://badge.fury.io/py/ctok) From f2b85f8eab58bd0ad83bc266734d532d2cd5b69c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 01:07:37 +0300 Subject: [PATCH 059/110] mypy badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b13c45a..f968421 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,6 @@ [![Test-Package](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml) [![Python versions](https://img.shields.io/pypi/pyversions/ctok.svg)](https://pypi.python.org/pypi/ctok) [![PyPI version](https://badge.fury.io/py/ctok.svg)](https://badge.fury.io/py/ctok) +[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) # ctok From 0802618619e1080ca3793e080f6cd4933fa616f4 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 01:09:20 +0300 Subject: [PATCH 060/110] ruff badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f968421..bd37f9e 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,6 @@ [![Python versions](https://img.shields.io/pypi/pyversions/ctok.svg)](https://pypi.python.org/pypi/ctok) [![PyPI version](https://badge.fury.io/py/ctok.svg)](https://badge.fury.io/py/ctok) [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) # ctok From 24dddc2924fe8d274ac9d4b4e40410459675ef07 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 14:37:45 +0300 Subject: [PATCH 061/110] counter token --- ctok/__init__.py | 1 + ctok/tokens/counter_token.py | 21 +++++++++++++++++++++ tests/tokens/test_counter_token.py | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 ctok/tokens/counter_token.py create mode 100644 tests/tokens/test_counter_token.py diff --git a/ctok/__init__.py b/ctok/__init__.py index f9832fd..87b546c 100644 --- a/ctok/__init__.py +++ b/ctok/__init__.py @@ -1,5 +1,6 @@ from ctok.tokens.simple_token import SimpleToken # noqa: F401 from ctok.tokens.condition_token import ConditionToken # noqa: F401 +from ctok.tokens.counter_token import CounterToken # noqa: F401 from ctok.tokens.timeout_token import TimeoutToken diff --git a/ctok/tokens/counter_token.py b/ctok/tokens/counter_token.py new file mode 100644 index 0000000..bfc3219 --- /dev/null +++ b/ctok/tokens/counter_token.py @@ -0,0 +1,21 @@ +from ctok.tokens.abstract_token import AbstractToken +from ctok import ConditionToken + + +class CounterToken(ConditionToken): + def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False): + if counter < 0: + raise ValueError('') + + self.counter = counter + + def function() -> bool: + if not self.counter: + return True + self.counter -= 1 + return False + + super().__init__(function, *tokens, cancelled=cancelled) + + def text_representation_of_superpower(self) -> str: + return str(self.counter) diff --git a/tests/tokens/test_counter_token.py b/tests/tokens/test_counter_token.py new file mode 100644 index 0000000..50c84db --- /dev/null +++ b/tests/tokens/test_counter_token.py @@ -0,0 +1,22 @@ +import pytest + +from ctok.tokens.counter_token import CounterToken + + +@pytest.mark.parametrize( + 'iterations', + [ + 0, + 1, + 5, + 15, + ], +) +def test_counter(iterations): + token = CounterToken(iterations) + counter = 0 + + while not token.cancelled: + counter += 1 + + assert counter == iterations From bdbed205ef319ae8917928b84db6d19d8283341c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 15:45:23 +0300 Subject: [PATCH 062/110] test --- tests/tokens/test_counter_token.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/tokens/test_counter_token.py b/tests/tokens/test_counter_token.py index 50c84db..d9aa67a 100644 --- a/tests/tokens/test_counter_token.py +++ b/tests/tokens/test_counter_token.py @@ -20,3 +20,8 @@ def test_counter(iterations): counter += 1 assert counter == iterations + + +def test_counter_less_than_zero(): + with pytest.raises(ValueError): + CounterToken(-1) From cf6ff3554d3af7f6095eea6024beef5acebeff38 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 15:47:06 +0300 Subject: [PATCH 063/110] tests (not passed) --- tests/tokens/test_abstract_token.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/tokens/test_abstract_token.py b/tests/tokens/test_abstract_token.py index 73b83cf..ac14a3d 100644 --- a/tests/tokens/test_abstract_token.py +++ b/tests/tokens/test_abstract_token.py @@ -3,11 +3,11 @@ import pytest from ctok.tokens.abstract_token import AbstractToken -from ctok import SimpleToken, ConditionToken, TimeoutToken +from ctok import SimpleToken, ConditionToken, TimeoutToken, CounterToken -ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken, TimeoutToken] -ALL_ARGUMENTS_FOR_TOKEN_CLASSES = [tuple(), (lambda: False, ), (15, )] +ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken, TimeoutToken, CounterToken] +ALL_ARGUMENTS_FOR_TOKEN_CLASSES = [tuple(), (lambda: False, ), (15, ), (15, )] ALL_TOKENS_FABRICS = [partial(token_class, *arguments) for token_class, arguments in zip(ALL_TOKEN_CLASSES, ALL_ARGUMENTS_FOR_TOKEN_CLASSES)] From 67a4e2e7787cd21b633a8af71c2e46c235018908 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 17:38:15 +0300 Subject: [PATCH 064/110] printing of the counter token without decrementing the counter --- ctok/tokens/abstract_token.py | 5 ++++- ctok/tokens/counter_token.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/ctok/tokens/abstract_token.py b/ctok/tokens/abstract_token.py index 62f4a03..78c74ea 100644 --- a/ctok/tokens/abstract_token.py +++ b/ctok/tokens/abstract_token.py @@ -41,7 +41,7 @@ def is_cancelled(self) -> bool: if self._cancelled: return True - elif any(x.is_cancelled() for x in self.tokens): + elif any(x.is_cancelled_reflect() for x in self.tokens): return True elif self.superpower(): @@ -49,6 +49,9 @@ def is_cancelled(self) -> bool: return False + def is_cancelled_reflect(self): + return self.is_cancelled() + def cancel(self) -> 'AbstractToken': self._cancelled = True return self diff --git a/ctok/tokens/counter_token.py b/ctok/tokens/counter_token.py index bfc3219..0249e4b 100644 --- a/ctok/tokens/counter_token.py +++ b/ctok/tokens/counter_token.py @@ -5,7 +5,7 @@ class CounterToken(ConditionToken): def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False): if counter < 0: - raise ValueError('') + raise ValueError('The counter must be greater than or equal to zero.') self.counter = counter @@ -17,5 +17,29 @@ 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})' + + 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: + result = self.cancelled + if not result: + self.counter += 1 + return result + + def is_cancelled_reflect(self): + return self.get_cancelled_status_without_decrementing_counter() + def text_representation_of_superpower(self) -> str: return str(self.counter) + + def text_representation_of_extra_kwargs(self) -> str: + return '' From 0a422833f97d5269b2ddaab25b2b7dc98ec3ab2f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 17:39:57 +0300 Subject: [PATCH 065/110] try to race condition --- tests/tokens/test_counter_token.py | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/tokens/test_counter_token.py b/tests/tokens/test_counter_token.py index d9aa67a..8333deb 100644 --- a/tests/tokens/test_counter_token.py +++ b/tests/tokens/test_counter_token.py @@ -1,3 +1,5 @@ +from threading import Thread + import pytest from ctok.tokens.counter_token import CounterToken @@ -25,3 +27,40 @@ def test_counter(iterations): def test_counter_less_than_zero(): with pytest.raises(ValueError): CounterToken(-1) + + +@pytest.mark.parametrize( + 'iterations', + [ + 10_000, + 50_000, + 1_000, + ], +) +@pytest.mark.parametrize( + 'number_of_threads', + [ + 1, + 2, + 5, + ], +) +def test_race_condition_for_counter(iterations, number_of_threads): + results = [] + token = CounterToken(iterations) + + def decrementer(number): + counter = 0 + while not token.cancelled: + counter += 1 + results.append(counter) + + threads = [Thread(target=decrementer, args=(iterations / number_of_threads, )) for _ in range(number_of_threads)] + + for thread in threads: + thread.start() + + for thread in threads: + thread.join() + + assert sum(results) == iterations From c76c0a2ee3c387bd0c7ebee10c5b573029eaa3d9 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 17:47:12 +0300 Subject: [PATCH 066/110] try to race condition --- tests/tokens/test_counter_token.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/tokens/test_counter_token.py b/tests/tokens/test_counter_token.py index 8333deb..e49ee9f 100644 --- a/tests/tokens/test_counter_token.py +++ b/tests/tokens/test_counter_token.py @@ -63,4 +63,5 @@ def decrementer(number): for thread in threads: thread.join() - assert sum(results) == iterations + result = sum(results) + assert result == iterations From f492bbca08068f6f2bbfd0b43dd003906d4f4993 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 17:53:52 +0300 Subject: [PATCH 067/110] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e6bd197..7d8251d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__ test.py .mypy_cache .ruff_cache +venv2 From 0dd8453d774dfdc5f0d913cf4899f9d500291f43 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 18:20:08 +0300 Subject: [PATCH 068/110] print in a test --- tests/tokens/test_counter_token.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tokens/test_counter_token.py b/tests/tokens/test_counter_token.py index e49ee9f..1a023b9 100644 --- a/tests/tokens/test_counter_token.py +++ b/tests/tokens/test_counter_token.py @@ -64,4 +64,5 @@ def decrementer(number): thread.join() result = sum(results) + print(result) assert result == iterations From 18354cfc3f54c3b95c154414fa9385a8503b8e72 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 18:46:47 +0300 Subject: [PATCH 069/110] lock added --- ctok/tokens/counter_token.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ctok/tokens/counter_token.py b/ctok/tokens/counter_token.py index 0249e4b..6995e4e 100644 --- a/ctok/tokens/counter_token.py +++ b/ctok/tokens/counter_token.py @@ -1,3 +1,5 @@ +from threading import Lock + from ctok.tokens.abstract_token import AbstractToken from ctok import ConditionToken @@ -8,12 +10,14 @@ def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False raise ValueError('The counter must be greater than or equal to zero.') self.counter = counter + self.lock = Lock() def function() -> bool: - if not self.counter: - return True - self.counter -= 1 - return False + with self.lock: + if not self.counter: + return True + self.counter -= 1 + return False super().__init__(function, *tokens, cancelled=cancelled) @@ -30,10 +34,11 @@ def __str__(self): return f'<{type(self).__name__} ({cancelled_flag})>' def get_cancelled_status_without_decrementing_counter(self) -> bool: - result = self.cancelled - if not result: - self.counter += 1 - return result + with self.lock: + result = self.cancelled + if not result: + self.counter += 1 + return result def is_cancelled_reflect(self): return self.get_cancelled_status_without_decrementing_counter() From d0506caded93fc0028c8981e6be8f9f44df6140a Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 18:52:05 +0300 Subject: [PATCH 070/110] RLock --- ctok/tokens/counter_token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ctok/tokens/counter_token.py b/ctok/tokens/counter_token.py index 6995e4e..7380c94 100644 --- a/ctok/tokens/counter_token.py +++ b/ctok/tokens/counter_token.py @@ -1,4 +1,4 @@ -from threading import Lock +from threading import RLock from ctok.tokens.abstract_token import AbstractToken from ctok import ConditionToken @@ -10,7 +10,7 @@ def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False raise ValueError('The counter must be greater than or equal to zero.') self.counter = counter - self.lock = Lock() + self.lock = RLock() def function() -> bool: with self.lock: From 6cfe3d67cb3aa3b0ab0e2e002d7dd5fc0992ab2d Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 21:46:58 +0300 Subject: [PATCH 071/110] logo --- README.md | 2 ++ docs/assets/logo_1.png | Bin 0 -> 15109 bytes docs/assets/logo_2.png | Bin 0 -> 56359 bytes 3 files changed, 2 insertions(+) create mode 100644 docs/assets/logo_1.png create mode 100644 docs/assets/logo_2.png diff --git a/README.md b/README.md index bd37f9e..9b4789c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![logo](https://raw.githubusercontent.com/pomponchik/ctok/main/docs/assets/logo_2.png) + [![Downloads](https://static.pepy.tech/badge/ctok/month)](https://pepy.tech/project/ctok) [![Downloads](https://static.pepy.tech/badge/ctok)](https://pepy.tech/project/ctok) [![codecov](https://codecov.io/gh/pomponchik/ctok/graph/badge.svg?token=eZ4eK6fkmx)](https://codecov.io/gh/pomponchik/ctok) diff --git a/docs/assets/logo_1.png b/docs/assets/logo_1.png new file mode 100644 index 0000000000000000000000000000000000000000..0c9f91194dadc716202edad6fedf03e3f9a2e374 GIT binary patch literal 15109 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 literal 0 HcmV?d00001 diff --git a/docs/assets/logo_2.png b/docs/assets/logo_2.png new file mode 100644 index 0000000000000000000000000000000000000000..2ca0d9bb1085b6beceb2c79a5bbe0759d413b19e GIT binary patch literal 56359 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�r&nk~;$zzq7zGd!m80JC09WEJF^0E2( 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 P8e3pDjILMdIw1ZJsd-v@ literal 0 HcmV?d00001 From 7f9c677717a4cba492f0524545e51175c5691b84 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 21:51:13 +0300 Subject: [PATCH 072/110] logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b4789c..d2c34fe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![logo](https://raw.githubusercontent.com/pomponchik/ctok/main/docs/assets/logo_2.png) +![logo](https://raw.githubusercontent.com/pomponchik/ctok/develop/docs/assets/logo_2.png) [![Downloads](https://static.pepy.tech/badge/ctok/month)](https://pepy.tech/project/ctok) [![Downloads](https://static.pepy.tech/badge/ctok)](https://pepy.tech/project/ctok) From 5ea4df4e78ab0ae6c56f57a9e2f93f562fa3990e Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 21:57:38 +0300 Subject: [PATCH 073/110] the project has renamed --- .github/workflows/lint.yml | 4 ++-- .github/workflows/tests_and_coverage.yml | 2 +- README.md | 14 +++++++------- cantok/__init__.py | 7 +++++++ {ctok => cantok}/tokens/__init__.py | 0 {ctok => cantok}/tokens/abstract_token.py | 0 {ctok => cantok}/tokens/condition_token.py | 2 +- {ctok => cantok}/tokens/counter_token.py | 4 ++-- {ctok => cantok}/tokens/simple_token.py | 2 +- {ctok => cantok}/tokens/timeout_token.py | 4 ++-- ctok/__init__.py | 7 ------- setup.py | 4 ++-- tests/tokens/test_abstract_token.py | 4 ++-- tests/tokens/test_condition_token.py | 2 +- tests/tokens/test_counter_token.py | 2 +- tests/tokens/test_simple_token.py | 2 +- tests/tokens/test_timeout_token.py | 2 +- 17 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 cantok/__init__.py rename {ctok => cantok}/tokens/__init__.py (100%) rename {ctok => cantok}/tokens/abstract_token.py (100%) rename {ctok => cantok}/tokens/condition_token.py (96%) rename {ctok => cantok}/tokens/counter_token.py (94%) rename {ctok => cantok}/tokens/simple_token.py (75%) rename {ctok => cantok}/tokens/timeout_token.py (91%) delete mode 100644 ctok/__init__.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9f000e8..f080e85 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,8 +25,8 @@ jobs: - name: Run mypy shell: bash - run: mypy ctok + run: mypy cantok - name: Run ruff shell: bash - run: ruff ctok + run: ruff cantok diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index 2830db0..8b1fe8e 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -32,7 +32,7 @@ jobs: run: pip list - name: Run tests and show coverage on the command line - run: coverage run --source=ctok --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m + run: coverage run --source=cantok --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m - name: Upload reports to codecov env: diff --git a/README.md b/README.md index d2c34fe..115f250 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -![logo](https://raw.githubusercontent.com/pomponchik/ctok/develop/docs/assets/logo_2.png) +![logo](https://raw.githubusercontent.com/pomponchik/cantok/develop/docs/assets/logo_2.png) -[![Downloads](https://static.pepy.tech/badge/ctok/month)](https://pepy.tech/project/ctok) -[![Downloads](https://static.pepy.tech/badge/ctok)](https://pepy.tech/project/ctok) -[![codecov](https://codecov.io/gh/pomponchik/ctok/graph/badge.svg?token=eZ4eK6fkmx)](https://codecov.io/gh/pomponchik/ctok) -[![Test-Package](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/ctok/actions/workflows/tests_and_coverage.yml) -[![Python versions](https://img.shields.io/pypi/pyversions/ctok.svg)](https://pypi.python.org/pypi/ctok) -[![PyPI version](https://badge.fury.io/py/ctok.svg)](https://badge.fury.io/py/ctok) +[![Downloads](https://static.pepy.tech/badge/cantok/month)](https://pepy.tech/project/cantok) +[![Downloads](https://static.pepy.tech/badge/cantok)](https://pepy.tech/project/cantok) +[![codecov](https://codecov.io/gh/pomponchik/cantok/graph/badge.svg?token=eZ4eK6fkmx)](https://codecov.io/gh/pomponchik/cantok) +[![Test-Package](https://github.com/pomponchik/cantok/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/cantok/actions/workflows/tests_and_coverage.yml) +[![Python versions](https://img.shields.io/pypi/pyversions/cantok.svg)](https://pypi.python.org/pypi/cantok) +[![PyPI version](https://badge.fury.io/py/cantok.svg)](https://badge.fury.io/py/cantok) [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) diff --git a/cantok/__init__.py b/cantok/__init__.py new file mode 100644 index 0000000..0cab75f --- /dev/null +++ b/cantok/__init__.py @@ -0,0 +1,7 @@ +from cantok.tokens.simple_token import SimpleToken # noqa: F401 +from cantok.tokens.condition_token import ConditionToken # noqa: F401 +from cantok.tokens.counter_token import CounterToken # noqa: F401 +from cantok.tokens.timeout_token import TimeoutToken + + +TimeOutToken = TimeoutToken diff --git a/ctok/tokens/__init__.py b/cantok/tokens/__init__.py similarity index 100% rename from ctok/tokens/__init__.py rename to cantok/tokens/__init__.py diff --git a/ctok/tokens/abstract_token.py b/cantok/tokens/abstract_token.py similarity index 100% rename from ctok/tokens/abstract_token.py rename to cantok/tokens/abstract_token.py diff --git a/ctok/tokens/condition_token.py b/cantok/tokens/condition_token.py similarity index 96% rename from ctok/tokens/condition_token.py rename to cantok/tokens/condition_token.py index c340184..4116068 100644 --- a/ctok/tokens/condition_token.py +++ b/cantok/tokens/condition_token.py @@ -1,7 +1,7 @@ from typing import Callable from contextlib import suppress -from ctok.tokens.abstract_token import AbstractToken +from cantok.tokens.abstract_token import AbstractToken class ConditionToken(AbstractToken): diff --git a/ctok/tokens/counter_token.py b/cantok/tokens/counter_token.py similarity index 94% rename from ctok/tokens/counter_token.py rename to cantok/tokens/counter_token.py index 7380c94..78fbd6a 100644 --- a/ctok/tokens/counter_token.py +++ b/cantok/tokens/counter_token.py @@ -1,7 +1,7 @@ from threading import RLock -from ctok.tokens.abstract_token import AbstractToken -from ctok import ConditionToken +from cantok.tokens.abstract_token import AbstractToken +from cantok import ConditionToken class CounterToken(ConditionToken): diff --git a/ctok/tokens/simple_token.py b/cantok/tokens/simple_token.py similarity index 75% rename from ctok/tokens/simple_token.py rename to cantok/tokens/simple_token.py index 34bbf90..dfbe269 100644 --- a/ctok/tokens/simple_token.py +++ b/cantok/tokens/simple_token.py @@ -1,4 +1,4 @@ -from ctok.tokens.abstract_token import AbstractToken +from cantok.tokens.abstract_token import AbstractToken class SimpleToken(AbstractToken): diff --git a/ctok/tokens/timeout_token.py b/cantok/tokens/timeout_token.py similarity index 91% rename from ctok/tokens/timeout_token.py rename to cantok/tokens/timeout_token.py index d5c1710..9944c89 100644 --- a/ctok/tokens/timeout_token.py +++ b/cantok/tokens/timeout_token.py @@ -2,8 +2,8 @@ from typing import Union, Callable -from ctok.tokens.abstract_token import AbstractToken -from ctok import ConditionToken +from cantok.tokens.abstract_token import AbstractToken +from cantok import ConditionToken class TimeoutToken(ConditionToken): diff --git a/ctok/__init__.py b/ctok/__init__.py deleted file mode 100644 index 87b546c..0000000 --- a/ctok/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from ctok.tokens.simple_token import SimpleToken # noqa: F401 -from ctok.tokens.condition_token import ConditionToken # noqa: F401 -from ctok.tokens.counter_token import CounterToken # noqa: F401 -from ctok.tokens.timeout_token import TimeoutToken - - -TimeOutToken = TimeoutToken diff --git a/setup.py b/setup.py index 702529c..9fc4e24 100644 --- a/setup.py +++ b/setup.py @@ -10,14 +10,14 @@ requirements = [] setup( - name='ctok', + name='cantok', version=version, author='Evgeniy Blinov', author_email='zheni-b@yandex.ru', description='Implementation of the "Cancellation Token" pattern', long_description=readme, long_description_content_type='text/markdown', - url='https://github.com/pomponchik/ctok', + url='https://github.com/pomponchik/cantok', packages=find_packages(exclude=['tests']), install_requires=requirements, classifiers=[ diff --git a/tests/tokens/test_abstract_token.py b/tests/tokens/test_abstract_token.py index ac14a3d..4a05e0c 100644 --- a/tests/tokens/test_abstract_token.py +++ b/tests/tokens/test_abstract_token.py @@ -2,8 +2,8 @@ import pytest -from ctok.tokens.abstract_token import AbstractToken -from ctok import SimpleToken, ConditionToken, TimeoutToken, CounterToken +from cantok.tokens.abstract_token import AbstractToken +from cantok import SimpleToken, ConditionToken, TimeoutToken, CounterToken ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken, TimeoutToken, CounterToken] diff --git a/tests/tokens/test_condition_token.py b/tests/tokens/test_condition_token.py index 4f1dbdd..e1151ee 100644 --- a/tests/tokens/test_condition_token.py +++ b/tests/tokens/test_condition_token.py @@ -2,7 +2,7 @@ import pytest -from ctok import SimpleToken, ConditionToken +from cantok import SimpleToken, ConditionToken def test_condition_counter(): diff --git a/tests/tokens/test_counter_token.py b/tests/tokens/test_counter_token.py index 1a023b9..abd4598 100644 --- a/tests/tokens/test_counter_token.py +++ b/tests/tokens/test_counter_token.py @@ -2,7 +2,7 @@ import pytest -from ctok.tokens.counter_token import CounterToken +from cantok.tokens.counter_token import CounterToken @pytest.mark.parametrize( diff --git a/tests/tokens/test_simple_token.py b/tests/tokens/test_simple_token.py index 39ead72..44a2676 100644 --- a/tests/tokens/test_simple_token.py +++ b/tests/tokens/test_simple_token.py @@ -1,6 +1,6 @@ import pytest -from ctok import SimpleToken +from cantok import SimpleToken def test_just_created_token_without_arguments(): diff --git a/tests/tokens/test_timeout_token.py b/tests/tokens/test_timeout_token.py index 9145dfe..9edd492 100644 --- a/tests/tokens/test_timeout_token.py +++ b/tests/tokens/test_timeout_token.py @@ -2,7 +2,7 @@ import pytest -from ctok import TimeoutToken +from cantok import TimeoutToken @pytest.mark.parametrize( From e1c0ef431705451a7aab348d98b9b9d1457f93c8 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 22:16:59 +0300 Subject: [PATCH 074/110] readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 115f250..dcf6d97 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,7 @@ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -# ctok + + + +## The pattern From 89382a9b33077fbd0aa3b31a22a2e8bf4a16e1fb Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 22:59:18 +0300 Subject: [PATCH 075/110] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcf6d97..d8e334e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,6 @@ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) - +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# and in Go. However, there was still no sane implementation in Python, until the cantok library appeared. ## The pattern From e12b6ca38c959f03a4368b365cd3e9e6294df4eb Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 23:02:22 +0300 Subject: [PATCH 076/110] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d8e334e..d7e6948 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -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# and in Go. However, there was still no sane implementation in Python, until the cantok library appeared. +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# 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 pattern From 8f94b386006499bf47972bcfba5d0fd937b0fbdc Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 21 Sep 2023 23:10:36 +0300 Subject: [PATCH 077/110] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7e6948..ec23caf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -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# 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 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. ## The pattern From a3ea00715f41684b504a1599a6c06fb53ba40617 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 16:39:55 +0300 Subject: [PATCH 078/110] readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index ec23caf..0461dc1 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,12 @@ 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. +## Table of contents + +- [**Quick start**](#quick-start) +- [**The pattern**](#the-pattern) + + +## Quick start + ## The pattern From 8f03cbcb00885d4c84baf8a754c10aa139798c4c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 16:44:09 +0300 Subject: [PATCH 079/110] readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 0461dc1..0e7aee5 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,24 @@ Cancellation Token is a pattern that allows us to refuse to continue calculation - [**Quick start**](#quick-start) - [**The pattern**](#the-pattern) +- [**Tokens**](#tokens) + - [**Simple token**](#simple-token) + - [**Condition token**](#simple-token) + - [**Timeout token**](#timeout-token) + - [**Counter token**](#counter-token) ## Quick start + + ## The pattern + + +## Tokens + + +### Simple token +### Condition token +### Timeout token +### Counter token From 08b5cf75b8fea7dac19fa4e9964d20b4fc9c08d6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 17:33:28 +0300 Subject: [PATCH 080/110] test of a readme example --- tests/{tokens => readme_examples}/__init__.py | 0 tests/readme_examples/test_examples.py | 25 +++++++++++++++++++ tests/units/__init__.py | 0 tests/units/tokens/__init__.py | 0 .../{ => units}/tokens/test_abstract_token.py | 0 .../tokens/test_condition_token.py | 0 .../{ => units}/tokens/test_counter_token.py | 1 - tests/{ => units}/tokens/test_simple_token.py | 0 .../{ => units}/tokens/test_timeout_token.py | 0 9 files changed, 25 insertions(+), 1 deletion(-) rename tests/{tokens => readme_examples}/__init__.py (100%) create mode 100644 tests/readme_examples/test_examples.py create mode 100644 tests/units/__init__.py create mode 100644 tests/units/tokens/__init__.py rename tests/{ => units}/tokens/test_abstract_token.py (100%) rename tests/{ => units}/tokens/test_condition_token.py (100%) rename tests/{ => units}/tokens/test_counter_token.py (98%) rename tests/{ => units}/tokens/test_simple_token.py (100%) rename tests/{ => units}/tokens/test_timeout_token.py (100%) diff --git a/tests/tokens/__init__.py b/tests/readme_examples/__init__.py similarity index 100% rename from tests/tokens/__init__.py rename to tests/readme_examples/__init__.py diff --git a/tests/readme_examples/test_examples.py b/tests/readme_examples/test_examples.py new file mode 100644 index 0000000..ca2d6ac --- /dev/null +++ b/tests/readme_examples/test_examples.py @@ -0,0 +1,25 @@ +from time import sleep +from queue import Queue +from threading import Thread + +from cantok import SimpleToken + + +def test_cancel_simple_token(): + counter = 0 + + def function(token): + nonlocal counter + while not token.cancelled: + counter += 1 + + token = SimpleToken() + thread = Thread(target=function, args=(token, )) + thread.start() + + sleep(1) + + token.cancel() + thread.join() + + assert counter diff --git a/tests/units/__init__.py b/tests/units/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/units/tokens/__init__.py b/tests/units/tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tokens/test_abstract_token.py b/tests/units/tokens/test_abstract_token.py similarity index 100% rename from tests/tokens/test_abstract_token.py rename to tests/units/tokens/test_abstract_token.py diff --git a/tests/tokens/test_condition_token.py b/tests/units/tokens/test_condition_token.py similarity index 100% rename from tests/tokens/test_condition_token.py rename to tests/units/tokens/test_condition_token.py diff --git a/tests/tokens/test_counter_token.py b/tests/units/tokens/test_counter_token.py similarity index 98% rename from tests/tokens/test_counter_token.py rename to tests/units/tokens/test_counter_token.py index abd4598..0f78e56 100644 --- a/tests/tokens/test_counter_token.py +++ b/tests/units/tokens/test_counter_token.py @@ -64,5 +64,4 @@ def decrementer(number): thread.join() result = sum(results) - print(result) assert result == iterations diff --git a/tests/tokens/test_simple_token.py b/tests/units/tokens/test_simple_token.py similarity index 100% rename from tests/tokens/test_simple_token.py rename to tests/units/tokens/test_simple_token.py diff --git a/tests/tokens/test_timeout_token.py b/tests/units/tokens/test_timeout_token.py similarity index 100% rename from tests/tokens/test_timeout_token.py rename to tests/units/tokens/test_timeout_token.py From 480784157dcfcf5981cd5f8f89a2426bc8649eba Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 17:35:19 +0300 Subject: [PATCH 081/110] readme example --- README.md | 31 ++++++++++++++++++++++++++ tests/readme_examples/test_examples.py | 1 - 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e7aee5..a477933 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,38 @@ Cancellation Token is a pattern that allows us to refuse to continue calculation ## Quick start +Install [it](https://pypi.org/project/cantok/): +```bash +pip install cantok +``` + +And use: + +```python +from time import sleep +from threading import Thread +from cantok import SimpleToken + + +counter = 0 + +def function(token): + nonlocal counter + while not token.cancelled: + counter += 1 + +token = SimpleToken() +thread = Thread(target=function, args=(token, )) +thread.start() + +sleep(1) + +token.cancel() +thread.join() + +assert counter +``` ## The pattern diff --git a/tests/readme_examples/test_examples.py b/tests/readme_examples/test_examples.py index ca2d6ac..b3432e0 100644 --- a/tests/readme_examples/test_examples.py +++ b/tests/readme_examples/test_examples.py @@ -1,5 +1,4 @@ from time import sleep -from queue import Queue from threading import Thread from cantok import SimpleToken From a83c6d8a53be6cd6c57bb6c3819affd692e9b3eb Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 18:13:23 +0300 Subject: [PATCH 082/110] adding of tokens --- cantok/tokens/abstract_token.py | 8 +++++ tests/units/tokens/test_abstract_token.py | 41 +++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/cantok/tokens/abstract_token.py b/cantok/tokens/abstract_token.py index 78c74ea..55f7d94 100644 --- a/cantok/tokens/abstract_token.py +++ b/cantok/tokens/abstract_token.py @@ -22,6 +22,14 @@ def __str__(self): cancelled_flag = 'cancelled' if self.cancelled else 'not cancelled' return f'<{type(self).__name__} ({cancelled_flag})>' + def __add__(self, item: 'AbstractToken') -> 'SimpleToken': + if not isinstance(item, AbstractToken): + raise TypeError('Cancellation Token can only be combined with another Cancellation Token.') + + from cantok import SimpleToken + + return SimpleToken(self, item) + @property def cancelled(self) -> bool: return self.is_cancelled() diff --git a/tests/units/tokens/test_abstract_token.py b/tests/units/tokens/test_abstract_token.py index 4a05e0c..eb378ef 100644 --- a/tests/units/tokens/test_abstract_token.py +++ b/tests/units/tokens/test_abstract_token.py @@ -94,3 +94,44 @@ def test_str(token_fabric): token.cancel() assert str(token) == '<' + type(token).__name__ + ' (cancelled)>' + + +@pytest.mark.parametrize( + 'first_token_fabric', + ALL_TOKENS_FABRICS, +) +@pytest.mark.parametrize( + 'second_token_fabric', + ALL_TOKENS_FABRICS, +) +def test_add_tokens(first_token_fabric, second_token_fabric): + first_token = first_token_fabric() + second_token = second_token_fabric() + + tokens_sum = first_token + second_token + + assert isinstance(tokens_sum, SimpleToken) + assert len(tokens_sum.tokens) == 2 + assert tokens_sum.tokens[0] is first_token + assert tokens_sum.tokens[1] is second_token + + +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +@pytest.mark.parametrize( + 'another_object', + [ + 1, + 'kek', + '', + None, + ], +) +def test_add_token_and_not_token(token_fabric, another_object): + with pytest.raises(TypeError): + token_fabric() + another_object + + with pytest.raises(TypeError): + another_object + token_fabric() From 7e5dfb54513460f60d083613efa3ac6f695b3f55 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:02:21 +0300 Subject: [PATCH 083/110] an example of code --- README.md | 17 ++++++++--------- tests/readme_examples/test_examples.py | 9 +++------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a477933..50885c2 100644 --- a/README.md +++ b/README.md @@ -35,30 +35,29 @@ pip install cantok And use: ```python -from time import sleep +from random import randint from threading import Thread -from cantok import SimpleToken + +from cantok import SimpleToken, ConditionToken, CounterToken, TimeoutToken counter = 0 def function(token): - nonlocal counter + global counter while not token.cancelled: counter += 1 -token = SimpleToken() +token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(1_000) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() - -sleep(1) - -token.cancel() thread.join() -assert counter +print(counter) ``` +In this example, we pass a token to the function that describes several restrictions: on the number of iterations of the cycle, on time, as well as on the occurrence of a random unlikely event. When any of the indicated events occur, the cycle stops. + ## The pattern diff --git a/tests/readme_examples/test_examples.py b/tests/readme_examples/test_examples.py index b3432e0..12fd23f 100644 --- a/tests/readme_examples/test_examples.py +++ b/tests/readme_examples/test_examples.py @@ -1,7 +1,7 @@ -from time import sleep from threading import Thread +from random import randint -from cantok import SimpleToken +from cantok import SimpleToken, ConditionToken, CounterToken, TimeoutToken def test_cancel_simple_token(): @@ -12,13 +12,10 @@ def function(token): while not token.cancelled: counter += 1 - token = SimpleToken() + token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000)) + CounterToken(100_000) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() - sleep(1) - - token.cancel() thread.join() assert counter From a539de0a371044a71c861e4839d040c2b44724d2 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:03:07 +0300 Subject: [PATCH 084/110] an example of code --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50885c2..e1e42bc 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ def function(token): while not token.cancelled: counter += 1 -token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(1_000) + TimeoutToken(1) +token = ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(1_000) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() thread.join() From 5ca9857271ced7bf80833a845b3839c4f81d6b14 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:05:55 +0300 Subject: [PATCH 085/110] readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e1e42bc..8479cc8 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ print(counter) In this example, we pass a token to the function that describes several restrictions: on the number of iterations of the cycle, on time, as well as on the occurrence of a random unlikely event. When any of the indicated events occur, the cycle stops. +Read more about the [possibilities of tokens](#tokens), as well as about the [pattern in general](#the-pattern). + ## The pattern From 6746a041a3c111ed30a6f23c39e67d476c9c0f8c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:07:56 +0300 Subject: [PATCH 086/110] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8479cc8..eee4721 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,11 @@ thread.join() print(counter) ``` -In this example, we pass a token to the function that describes several restrictions: on the number of iterations of the cycle, on time, as well as on the occurrence of a random unlikely event. When any of the indicated events occur, the cycle stops. +In this example, we pass a token to the function that describes several restrictions: on the [number of iterations](#counter-token) of the cycle, on [time](#timeout-token), as well as on the [occurrence](#condition-token) of a random unlikely event. When any of the indicated events occur, the cycle stops. Read more about the [possibilities of tokens](#tokens), as well as about the [pattern in general](#the-pattern). + ## The pattern From 06f655ad433ddff3f245dfd364f268f36f2c9fca Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:35:26 +0300 Subject: [PATCH 087/110] direct flag for counter token --- cantok/tokens/counter_token.py | 13 ++++++++----- tests/units/tokens/test_counter_token.py | 21 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/cantok/tokens/counter_token.py b/cantok/tokens/counter_token.py index 78fbd6a..803d6fb 100644 --- a/cantok/tokens/counter_token.py +++ b/cantok/tokens/counter_token.py @@ -5,11 +5,12 @@ class CounterToken(ConditionToken): - def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False): + def __init__(self, counter: int, *tokens: AbstractToken, cancelled: bool = False, direct: bool = True): if counter < 0: raise ValueError('The counter must be greater than or equal to zero.') self.counter = counter + self.direct = direct self.lock = RLock() def function() -> bool: @@ -27,7 +28,7 @@ def __repr__(self): 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})' + 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' @@ -40,11 +41,13 @@ def get_cancelled_status_without_decrementing_counter(self) -> bool: self.counter += 1 return result - def is_cancelled_reflect(self): - return self.get_cancelled_status_without_decrementing_counter() + def is_cancelled_reflect(self) -> bool: + if self.direct: + return self.get_cancelled_status_without_decrementing_counter() + return self.cancelled def text_representation_of_superpower(self) -> str: return str(self.counter) def text_representation_of_extra_kwargs(self) -> str: - return '' + return f'direct={self.direct}' diff --git a/tests/units/tokens/test_counter_token.py b/tests/units/tokens/test_counter_token.py index 0f78e56..459d57a 100644 --- a/tests/units/tokens/test_counter_token.py +++ b/tests/units/tokens/test_counter_token.py @@ -2,7 +2,7 @@ import pytest -from cantok.tokens.counter_token import CounterToken +from cantok import CounterToken, SimpleToken @pytest.mark.parametrize( @@ -65,3 +65,22 @@ def decrementer(number): result = sum(results) assert result == iterations + + +@pytest.mark.parametrize( + 'kwargs,expected_result', + [ + ({}, 5), + ({'direct': True}, 5), + ({'direct': False}, 4), + ], +) +def test_direct_default_counter(kwargs, expected_result): + first_token = CounterToken(5, **kwargs) + second_token = SimpleToken(first_token) + + assert not second_token.cancelled + assert first_token.counter == expected_result + + assert not first_token.cancelled + assert first_token.counter == expected_result - 1 From c1096fded9801e104578e882032569c84f949d05 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:39:25 +0300 Subject: [PATCH 088/110] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eee4721..490f00c 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ def function(token): while not token.cancelled: counter += 1 -token = ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(1_000) + TimeoutToken(1) +token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() thread.join() From 0d4dd1df1e22476917337693e925727c2b97c9d2 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:50:42 +0300 Subject: [PATCH 089/110] typevar --- cantok/tokens/abstract_token.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cantok/tokens/abstract_token.py b/cantok/tokens/abstract_token.py index 55f7d94..d345ca8 100644 --- a/cantok/tokens/abstract_token.py +++ b/cantok/tokens/abstract_token.py @@ -1,3 +1,4 @@ +from typing import TypeVar from abc import ABC, abstractmethod @@ -22,7 +23,7 @@ def __str__(self): cancelled_flag = 'cancelled' if self.cancelled else 'not cancelled' return f'<{type(self).__name__} ({cancelled_flag})>' - def __add__(self, item: 'AbstractToken') -> 'SimpleToken': + def __add__(self, item: 'AbstractToken') -> TypeVar('SimpleToken'): if not isinstance(item, AbstractToken): raise TypeError('Cancellation Token can only be combined with another Cancellation Token.') From 296fe55f950294e26ab1f7720338623619f1602f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:52:34 +0300 Subject: [PATCH 090/110] typevar --- cantok/tokens/abstract_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cantok/tokens/abstract_token.py b/cantok/tokens/abstract_token.py index d345ca8..669e13a 100644 --- a/cantok/tokens/abstract_token.py +++ b/cantok/tokens/abstract_token.py @@ -23,7 +23,7 @@ def __str__(self): cancelled_flag = 'cancelled' if self.cancelled else 'not cancelled' return f'<{type(self).__name__} ({cancelled_flag})>' - def __add__(self, item: 'AbstractToken') -> TypeVar('SimpleToken'): + def __add__(self, item: 'AbstractToken') -> TypeVar['SimpleToken']: if not isinstance(item, AbstractToken): raise TypeError('Cancellation Token can only be combined with another Cancellation Token.') From d20860f8e2a01e58c4e029d8dc859e0996fe161b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 22:58:41 +0300 Subject: [PATCH 091/110] without typevar --- cantok/tokens/abstract_token.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cantok/tokens/abstract_token.py b/cantok/tokens/abstract_token.py index 669e13a..cc7d5c2 100644 --- a/cantok/tokens/abstract_token.py +++ b/cantok/tokens/abstract_token.py @@ -1,4 +1,3 @@ -from typing import TypeVar from abc import ABC, abstractmethod @@ -23,7 +22,7 @@ def __str__(self): cancelled_flag = 'cancelled' if self.cancelled else 'not cancelled' return f'<{type(self).__name__} ({cancelled_flag})>' - def __add__(self, item: 'AbstractToken') -> TypeVar['SimpleToken']: + def __add__(self, item: 'AbstractToken') -> 'AbstractToken': if not isinstance(item, AbstractToken): raise TypeError('Cancellation Token can only be combined with another Cancellation Token.') From 8f8a370d549c6a3433cd8d60d8ad8f75156e15ce Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:01:18 +0300 Subject: [PATCH 092/110] test of an example --- tests/readme_examples/test_examples.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/readme_examples/test_examples.py b/tests/readme_examples/test_examples.py index 12fd23f..1ec34bf 100644 --- a/tests/readme_examples/test_examples.py +++ b/tests/readme_examples/test_examples.py @@ -4,18 +4,17 @@ from cantok import SimpleToken, ConditionToken, CounterToken, TimeoutToken -def test_cancel_simple_token(): - counter = 0 +counter = 0 +def test_cancel_simple_token(): def function(token): - nonlocal counter + global counter while not token.cancelled: counter += 1 - token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000)) + CounterToken(100_000) + TimeoutToken(1) + token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() - thread.join() assert counter From 0a313d1c8fbbd38ed2b78d760521b5c6fb66a4dc Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:05:22 +0300 Subject: [PATCH 093/110] test of an example --- README.md | 2 +- tests/readme_examples/test_examples.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 490f00c..ac407a1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ def function(token): while not token.cancelled: counter += 1 -token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) +token = ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() thread.join() diff --git a/tests/readme_examples/test_examples.py b/tests/readme_examples/test_examples.py index 1ec34bf..18494d8 100644 --- a/tests/readme_examples/test_examples.py +++ b/tests/readme_examples/test_examples.py @@ -1,7 +1,7 @@ from threading import Thread from random import randint -from cantok import SimpleToken, ConditionToken, CounterToken, TimeoutToken +from cantok import ConditionToken, CounterToken, TimeoutToken counter = 0 @@ -12,7 +12,7 @@ def function(token): while not token.cancelled: counter += 1 - token = SimpleToken() + ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) + token = ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() thread.join() From 2858476bbacf8e0f0a10eba0b526b8415d99e341 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:09:24 +0300 Subject: [PATCH 094/110] test of an example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac407a1..4f2d4f3 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ def function(token): while not token.cancelled: counter += 1 -token = ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) +token = ConditionToken(lambda: randint(1, 1_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() thread.join() From 15e39e30e0f41180aae78d20f4912b8a3598f757 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:10:47 +0300 Subject: [PATCH 095/110] test of an example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f2d4f3..ffa9ef9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ def function(token): while not token.cancelled: counter += 1 -token = ConditionToken(lambda: randint(1, 1_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) +token = ConditionToken(lambda: randint(1, 100_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() thread.join() From ae4221970c5e10ab23a9430edc4b7c3be584dd09 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:11:10 +0300 Subject: [PATCH 096/110] test of an example --- tests/readme_examples/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/readme_examples/test_examples.py b/tests/readme_examples/test_examples.py index 18494d8..d0c34b4 100644 --- a/tests/readme_examples/test_examples.py +++ b/tests/readme_examples/test_examples.py @@ -12,7 +12,7 @@ def function(token): while not token.cancelled: counter += 1 - token = ConditionToken(lambda: randint(1, 1_000_000_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) + token = ConditionToken(lambda: randint(1, 100_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) thread = Thread(target=function, args=(token, )) thread.start() thread.join() From fb8c6226a29d6c4ee93322e01aef39b2d6a623f5 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:23:30 +0300 Subject: [PATCH 097/110] readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ffa9ef9..bdd0b73 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ Read more about the [possibilities of tokens](#tokens), as well as about the [pa ## The pattern +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 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. ## Tokens From 1bba6ef2e783ba9d302baa0bda3c19f378f4c228 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:32:47 +0300 Subject: [PATCH 098/110] readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index bdd0b73..499dfd5 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,11 @@ The essence of the pattern is that we pass special objects to functions and cons 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. +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. + +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. + + ## Tokens From 265c4af8520997b3ac35b495c34f4822ef2284cc Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 22 Sep 2023 23:43:25 +0300 Subject: [PATCH 099/110] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 499dfd5..c1c3e34 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Read more about the [possibilities of tokens](#tokens), as well as about the [pa ## The pattern -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 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 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. 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. From 34898d51ecefa593ee6a090026fe36886598dac8 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 01:40:02 +0300 Subject: [PATCH 100/110] readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c1c3e34..95eb4b1 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ It is highly desirable for library developers to use this pattern for any long-t ## Tokens +All token classes presented in this library have a uniform interface. And they are all inherited from one class: `AbstractToken`. ### Simple token ### Condition token From 66aa40dcd2aeeeb62c255afe665fc7dccf8fc852 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 01:41:57 +0300 Subject: [PATCH 101/110] directly imported AbstractToken --- cantok/__init__.py | 1 + cantok/tokens/condition_token.py | 2 +- cantok/tokens/counter_token.py | 2 +- cantok/tokens/simple_token.py | 2 +- cantok/tokens/timeout_token.py | 2 +- tests/units/tokens/test_abstract_token.py | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cantok/__init__.py b/cantok/__init__.py index 0cab75f..2522534 100644 --- a/cantok/__init__.py +++ b/cantok/__init__.py @@ -1,3 +1,4 @@ +from cantok.tokens.abstract_token import AbstractToken # noqa: F401 from cantok.tokens.simple_token import SimpleToken # noqa: F401 from cantok.tokens.condition_token import ConditionToken # noqa: F401 from cantok.tokens.counter_token import CounterToken # noqa: F401 diff --git a/cantok/tokens/condition_token.py b/cantok/tokens/condition_token.py index 4116068..56c0c45 100644 --- a/cantok/tokens/condition_token.py +++ b/cantok/tokens/condition_token.py @@ -1,7 +1,7 @@ from typing import Callable from contextlib import suppress -from cantok.tokens.abstract_token import AbstractToken +from cantok import AbstractToken class ConditionToken(AbstractToken): diff --git a/cantok/tokens/counter_token.py b/cantok/tokens/counter_token.py index 803d6fb..9ab370b 100644 --- a/cantok/tokens/counter_token.py +++ b/cantok/tokens/counter_token.py @@ -1,6 +1,6 @@ from threading import RLock -from cantok.tokens.abstract_token import AbstractToken +from cantok import AbstractToken from cantok import ConditionToken diff --git a/cantok/tokens/simple_token.py b/cantok/tokens/simple_token.py index dfbe269..55c7033 100644 --- a/cantok/tokens/simple_token.py +++ b/cantok/tokens/simple_token.py @@ -1,4 +1,4 @@ -from cantok.tokens.abstract_token import AbstractToken +from cantok import AbstractToken class SimpleToken(AbstractToken): diff --git a/cantok/tokens/timeout_token.py b/cantok/tokens/timeout_token.py index 9944c89..f7a54e0 100644 --- a/cantok/tokens/timeout_token.py +++ b/cantok/tokens/timeout_token.py @@ -2,7 +2,7 @@ from typing import Union, Callable -from cantok.tokens.abstract_token import AbstractToken +from cantok import AbstractToken from cantok import ConditionToken diff --git a/tests/units/tokens/test_abstract_token.py b/tests/units/tokens/test_abstract_token.py index eb378ef..d9c35d9 100644 --- a/tests/units/tokens/test_abstract_token.py +++ b/tests/units/tokens/test_abstract_token.py @@ -2,7 +2,7 @@ import pytest -from cantok.tokens.abstract_token import AbstractToken +from cantok import AbstractToken from cantok import SimpleToken, ConditionToken, TimeoutToken, CounterToken From c45f7ca1db040b33591a85eae2d8c3c3b32432a5 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 02:07:01 +0300 Subject: [PATCH 102/110] readme --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 95eb4b1..f6cc903 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ And use: from random import randint from threading import Thread -from cantok import SimpleToken, ConditionToken, CounterToken, TimeoutToken +from cantok import ConditionToken, CounterToken, TimeoutToken counter = 0 @@ -74,7 +74,81 @@ It is highly desirable for library developers to use this pattern for any long-t ## Tokens -All token classes presented in this library have a uniform interface. And they are all inherited from one class: `AbstractToken`. +All token classes presented in this library have a uniform interface. And they are all inherited from one class: `AbstractToken`. The only reason why you might want to import it is to use it for a type hint. This example illustrates a type hint suitable for any of the tokens: + +```python +from cantok import AbstractToken + +def function(token: AbstractToken): + ... +``` + +Each token object has a `cancelled` attribute and a `cancel()` method. By the attribute, you can find out whether this token has been canceled: + +```python +from cantok import SimpleToken + +token = SimpleToken() +print(token.cancelled) # False +token.cancel() +print(token.cancelled) # True +``` + +The cancelled attribute is dynamically calculated and takes into account, among other things, specific conditions that are checked by a specific token. Here is an example with a [token that measures time](#timeout-token): + +```python +from time import sleep +from cantok import TimeoutToken + +token = TimeoutToken(5) +print(token.cancelled) # False +sleep(10) +print(token.cancelled) # True +``` + +In addition to this attribute, each token implements the `is_cancelled()` method. It does exactly the same thing as the attribute: + +```python +from cantok import SimpleToken + +token = SimpleToken() +print(token.cancelled) # False +print(token.is_cancelled()) # False +token.cancel() +print(token.cancelled) # True +print(token.is_cancelled()) # True +``` + +Choose what you like best. To the author of the library, the use of the attribute seems more beautiful, but the method call more clearly reflects the complexity of the work that is actually being done to answer the question "has the token been canceled?". + +There is another method opposite to `is_cancelled()` - `keep_on()`. It answers the opposite question, and can be used in the same situations: + +```python +from cantok import SimpleToken + +token = SimpleToken() +print(token.cancelled) # False +print(token.keep_on()) # True +token.cancel() +print(token.cancelled) # True +print(token.keep_on()) # False +``` + +An unlimited number of other tokens can be embedded in one token as arguments during initialization. Each time checking whether it has been canceled, the token first checks its cancellation rules, and if it has not been canceled itself, then it checks the tokens nested in it. Thus, one cancelled token nested in another non-cancelled token cancels it: + +```python +from cantok import SimpleToken + +first_token = SimpleToken() +second_token = SimpleToken() +third_token = SimpleToken(first_token, second_token) + +first_token.cancel() + +print(first_token.cancelled) # True +print(second_token.cancelled) # False +print(third_token.cancelled) # True +``` ### Simple token ### Condition token From 5f06293d08bbe686dafefd73b15912955d0eaeba Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 02:10:30 +0300 Subject: [PATCH 103/110] an extra space in the code --- cantok/tokens/abstract_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cantok/tokens/abstract_token.py b/cantok/tokens/abstract_token.py index cc7d5c2..6537de4 100644 --- a/cantok/tokens/abstract_token.py +++ b/cantok/tokens/abstract_token.py @@ -73,4 +73,4 @@ def text_representation_of_superpower(self) -> str: # pragma: no cover pass def text_representation_of_extra_kwargs(self) -> str: - return '' + return '' From e02390d261b0fca50f6897dffb4e08006aae96de Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 02:21:10 +0300 Subject: [PATCH 104/110] readme --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index f6cc903..9b8a84b 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,30 @@ print(second_token.cancelled) # False print(third_token.cancelled) # True ``` +In addition, any tokens can be summed up among themselves. The summation operation generates another [`SimpleToken`](#simple-token) that includes the previous 2: + +```python +from cantok import SimpleToken, TimeoutToken + +print(repr(SimpleToken() + TimeoutToken(5))) +# SimpleToken(SimpleToken(cancelled=False), TimeoutToken(5, cancelled=False, monotonic=False), cancelled=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: + +```python +from cantok import AbstractToken, TimeoutToken + +def function(token: AbstractToken): + ... + another_function(token + TimeoutToken(5)) # Imposes an additional restriction on the function being called: work for no more than 5 seconds. At the same time, it does not know anything about what restrictions were imposed earlier. + ... +``` + + + + + ### Simple token ### Condition token ### Timeout token From 3db654a98a3b2ea4870270bd17b70861967f7ad0 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 02:23:32 +0300 Subject: [PATCH 105/110] readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b8a84b..5ab91fb 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,7 @@ def function(token: AbstractToken): ... ``` - - +Read on about the features of each type of tokens in more detail. ### Simple token From 5645690bee074083c5f5c07fff4aacfea24d272a Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 02:48:52 +0300 Subject: [PATCH 106/110] readme --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index 5ab91fb..c3e7e73 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,76 @@ Read on about the features of each type of tokens in more detail. ### Simple token + +The base token is `SimpleToken`. It has no built-in automation that can cancel it. The only way to cancel `SimpleToken` is to explicitly call the `cancel()` method from it. + +```python +from cantok import SimpleToken + +token = SimpleToken() +print(token.cancelled) # False +token.cancel() +print(token.cancelled) # True +``` + +`SimpleToken` is also implicitly generated by the operation of summing two other tokens: + +```python +from cantok import SimpleToken + +token = SimpleToken() +print(token.cancelled) # False +token.cancel() +print(token.cancelled) # True +``` + +```python +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) +``` + +There is not much more to tell about it if you have read [the story](#tokens) about tokens in general. + + ### Condition token + +A slightly more complex type of token than `SimpleToken` is `ConditionToken`. In addition to everything that `SimpleToken` does, it also checks the condition passed to it as an argument, answering the question whether it has been canceled. + +To initialize `ConditionToken`, pass a function to it that does not accept arguments and returns a boolean value. If it returns `True`, it means that the operation has been canceled: + +```python +from cantok import ConditionToken + +counter = 5 +token = ConditionToken(lambda: counter >= 5) + +while not token.cancelled: + counter += 1 + +print(counter) # 5 +``` + +By default, if the passed function raises an exception, it will be silently suppressed. However, you can make the raised exceptions explicit by setting the `suppress_exceptions` parameter to `False`: + +```python +def function(): raise ValueError + +token = ConditionToken(function, suppress_exceptions=False) + +token.cancelled # ValueError has risen. +``` + +If you still use exception suppression mode, by default, in case of an exception, the `canceled` attribute will contain `False`. If you want to change this, pass it there as the `default` parameter - `True`. + +```python +def function(): raise ValueError + +print(ConditionToken(function).cancelled) # False +print(ConditionToken(function, default=False).cancelled) # False +print(ConditionToken(function, default=True).cancelled) # True +``` + ### Timeout token ### Counter token From 5d2a3249413e4473713640fb90e294a80b35e0a8 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 02:50:37 +0300 Subject: [PATCH 107/110] readme --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index c3e7e73..08072e4 100644 --- a/README.md +++ b/README.md @@ -188,15 +188,6 @@ print(token.cancelled) # True `SimpleToken` is also implicitly generated by the operation of summing two other tokens: -```python -from cantok import SimpleToken - -token = SimpleToken() -print(token.cancelled) # False -token.cancel() -print(token.cancelled) # True -``` - ```python from cantok import CounterToken, TimeoutToken From e9f4e20f3384ecf370bb9afcf74b9300f783f512 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 02:50:58 +0300 Subject: [PATCH 108/110] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08072e4..869e71e 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ There is not much more to tell about it if you have read [the story](#tokens) ab ### Condition token -A slightly more complex type of token than `SimpleToken` is `ConditionToken`. In addition to everything that `SimpleToken` does, it also checks the condition passed to it as an argument, answering the question whether it has been canceled. +A slightly more complex type of token than [`SimpleToken`](#simple-token) is `ConditionToken`. In addition to everything that `SimpleToken` does, it also checks the condition passed to it as an argument, answering the question whether it has been canceled. To initialize `ConditionToken`, pass a function to it that does not accept arguments and returns a boolean value. If it returns `True`, it means that the operation has been canceled: From 17d85f920dd8af1f64f7da477a75d9316289e4e9 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 03:27:21 +0300 Subject: [PATCH 109/110] readme --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 869e71e..2f87817 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ There is not much more to tell about it if you have read [the story](#tokens) ab ### Condition token -A slightly more complex type of token than [`SimpleToken`](#simple-token) is `ConditionToken`. In addition to everything that `SimpleToken` does, it also checks the condition passed to it as an argument, answering the question whether it has been canceled. +A slightly more complex type of token than [`SimpleToken`](#simple-token) is `ConditionToken`. In addition to everything that `SimpleToken` does, it also checks the condition passed to it as a first argument, answering the question whether it has been canceled. To initialize `ConditionToken`, pass a function to it that does not accept arguments and returns a boolean value. If it returns `True`, it means that the operation has been canceled: @@ -236,5 +236,79 @@ print(ConditionToken(function, default=False).cancelled) # False print(ConditionToken(function, default=True).cancelled) # True ``` +`ConditionToken` may include other tokens during initialization: + +```python +token = ConditionToken(lambda: False, SimpleToken(), TimeoutToken(5), CounterToken(20)) # Includes all additional restrictions of the passed tokens. +``` + ### Timeout token + +`TimeoutToken` is automatically canceled after the time specified in seconds in the class constructor: + +```python +from time import sleep +from cantok import TimeoutToken + +token = TimeoutToken(5) +print(token.cancelled) # False +sleep(10) +print(token.cancelled) # True +``` + +Just like `ConditionToken`, `TimeoutToken` can include other tokens: + +```python +token = TimeoutToken(45, SimpleToken(), TimeoutToken(5), CounterToken(20)) # Includes all additional restrictions of the passed tokens. +``` + +By default, time is measured using [`perf_counter`](https://docs.python.org/3/library/time.html#time.perf_counter) as the most accurate way to measure time. In extremely rare cases, you may need to use [monotonic](https://docs.python.org/3/library/time.html#time.monotonic_ns)-time, for this use the appropriate initialization argument: + +```python +token = TimeoutToken(33, monotonic=True) +``` + ### Counter token + +`CounterToken` is the most ambiguous of the tokens presented by this library. Do not use it if you are not sure that you understand how it works correctly. However, it can be very useful in situations where you want to limit the number of attempts to perform an operation. + +`CounterToken` is initialized with an integer greater than zero. At each calculation of the answer to the question whether it is canceled, this number is reduced by one. When this number becomes zero, the token is considered canceled: + +```python +from cantok import CounterToken + +token = CounterToken(5) +counter = 0 + +while not token.cancelled: + counter += 1 + +print(counter) # 5 +``` + +The counter inside the `CounterToken` is reduced under one of three conditions: + +- Access to the `cancelled` attribute. +- Calling the `is_cancelled()` method. +- Calling the `keep_on()` method. + +If you use `CounterToken` inside other tokens, the wrapping token can specify the status of the `CounterToken`. For security reasons, this operation does not decrease the counter. However, if for some reason you need it to decrease, pass `direct` - `False` as an argument: + +```python +from cantok import SimpleToken, CounterToken + +first_counter_token = CounterToken(1, direct=False) +second_counter_token = CounterToken(1, direct=True) + +print(SimpleToken(first_counter_token, second_counter_token).cancelled) # False +print(first_counter_token.cancelled) # True +print(second_counter_token.cancelled) # False +``` + +Like all other tokens, `CounterToken` can accept other tokens as parameters during initialization: + +```python +from cantok import SimpleToken, CounterToken, TimeoutToken + +token = CounterToken(15, SimpleToken(), TimeoutToken(5)) +``` From abfbb9ff40d008d29f374a80c64249acea2a2b78 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Sat, 23 Sep 2023 03:29:54 +0300 Subject: [PATCH 110/110] readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2f87817..4ced750 100644 --- a/README.md +++ b/README.md @@ -312,3 +312,5 @@ from cantok import SimpleToken, CounterToken, TimeoutToken token = CounterToken(15, SimpleToken(), TimeoutToken(5)) ``` + +`CounterToken` is thread-safe.