diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml index dcfd3ac..3f0dba0 100644 --- a/.github/workflows/tests_and_coverage.yml +++ b/.github/workflows/tests_and_coverage.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + os: [macos-13, ubuntu-latest, windows-latest] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: diff --git a/README.md b/README.md index a4de4fa..1ba0ab1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ And use: from random import randint from cantok import ConditionToken, CounterToken, TimeoutToken - token = ConditionToken(lambda: randint(1, 100_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1) counter = 0 diff --git a/cantok/__init__.py b/cantok/__init__.py index 14b19c0..7f6c695 100644 --- a/cantok/__init__.py +++ b/cantok/__init__.py @@ -2,9 +2,10 @@ from cantok.tokens.simple_token import SimpleToken as SimpleToken # noqa: F401 from cantok.tokens.condition_token import ConditionToken as ConditionToken # noqa: F401 from cantok.tokens.counter_token import CounterToken as CounterToken # noqa: F401 +from cantok.tokens.default_token import DefaultToken as DefaultToken # noqa: F401 from cantok.tokens.timeout_token import TimeoutToken as TimeoutToken -from cantok.errors import CancellationError as CancellationError, ConditionCancellationError as ConditionCancellationError, CounterCancellationError as CounterCancellationError, TimeoutCancellationError as TimeoutCancellationError # noqa: F401 +from cantok.errors import CancellationError as CancellationError, ConditionCancellationError as ConditionCancellationError, CounterCancellationError as CounterCancellationError, TimeoutCancellationError as TimeoutCancellationError, ImpossibleCancelError as ImpossibleCancelError # noqa: F401 TimeOutToken = TimeoutToken diff --git a/cantok/errors.py b/cantok/errors.py index 26da270..5c57dbc 100644 --- a/cantok/errors.py +++ b/cantok/errors.py @@ -1,5 +1,6 @@ from typing import Any + class CancellationError(Exception): def __init__(self, message: str, token: Any): self.token = token @@ -13,3 +14,6 @@ class CounterCancellationError(CancellationError): class TimeoutCancellationError(CancellationError): pass + +class ImpossibleCancelError(CancellationError): + pass diff --git a/cantok/tokens/default_token.py b/cantok/tokens/default_token.py new file mode 100644 index 0000000..693e39c --- /dev/null +++ b/cantok/tokens/default_token.py @@ -0,0 +1,36 @@ +from cantok import AbstractToken +from cantok.errors import ImpossibleCancelError + + +class DefaultToken(AbstractToken): + exception = ImpossibleCancelError + + def __init__(self) -> None: + super().__init__() + + def superpower(self) -> bool: + return False + + def text_representation_of_superpower(self) -> str: + return '' + + def get_superpower_exception_message(self) -> str: + return 'You cannot cancel a default token.' # pragma: no cover + + @property + def cancelled(self) -> bool: + return False + + @cancelled.setter + def cancelled(self, new_value: bool) -> None: + if new_value == True: + self.raise_superpower_exception() + + def keep_on(self) -> bool: + return True + + def is_cancelled(self, direct: bool = True) -> bool: + return False + + def cancel(self) -> 'AbstractToken': # type: ignore[return] + self.raise_superpower_exception() diff --git a/docs/assets/presentation_1.pptx b/docs/assets/presentation_1.pptx new file mode 100644 index 0000000..cbfdb81 Binary files /dev/null and b/docs/assets/presentation_1.pptx differ diff --git a/docs/ecosystem/projects/regular_functions_calling.md b/docs/ecosystem/projects/regular_functions_calling.md new file mode 100644 index 0000000..544f89a --- /dev/null +++ b/docs/ecosystem/projects/regular_functions_calling.md @@ -0,0 +1,44 @@ +To run a function regularly, use [metronomes](https://github.com/pomponchik/metronomes). Just wrap your code in a context manager: + +```python +from time import sleep +from metronomes import Metronome + +with Metronome(0.2, lambda: print('go!')): + sleep(1) +#> go! +#> go! +#> go! +#> go! +#> go! +``` + +You can also manually control the start and stop of the metronome: + +```python +metronome = Metronome(0.2, lambda: print('go!')) + +metronome.start() +sleep(1) +metronome.stop() +#> go! +#> go! +#> go! +#> go! +#> go! +``` + +And of course, the cancellation token can be used as an optional argument: + +```python +from cantok import TimeoutToken + +metronome = Metronome(0.2, lambda: None, token=TimeoutToken(1)) + +metronome.start() +print(metronome.stopped) +#> False +sleep(1.5) # Here I specify a little more time than in the constructor of the token itself, since a small margin is needed for operations related to the creation of the metronome object itself. +print(metronome.stopped) +#> True +``` diff --git a/docs/types_of_tokens/DefaultToken.md b/docs/types_of_tokens/DefaultToken.md new file mode 100644 index 0000000..3443061 --- /dev/null +++ b/docs/types_of_tokens/DefaultToken.md @@ -0,0 +1,18 @@ +`DefaultToken` is a type of token that cannot be revoked. Otherwise, it behaves like a regular token, but if you try to cancel it, you will get an exception: + +```python +from cantok import AbstractToken, DefaultToken + +DefaultToken().cancel() +#> ... +#> cantok.errors.ImpossibleCancelError: You cannot cancel a default token. +``` + +In addition, you cannot embed other tokens in `DefaultToken`. + +It is best to use `DefaultToken` as the default argument for functions: + +```python +def function(token: AbstractToken = DefaultToken()): + ... +``` diff --git a/docs/what_are_tokens/exceptions.md b/docs/what_are_tokens/exceptions.md index 8fe7c28..c5ee3e4 100644 --- a/docs/what_are_tokens/exceptions.md +++ b/docs/what_are_tokens/exceptions.md @@ -9,7 +9,7 @@ token.check() # cantok.errors.TimeoutCancellationError: The timeout of 1 seconds has expired. ``` -Each type of token has a corresponding type of exception that can be raised in this case: +Each type of token (except [`DefaultToken`](../types_of_tokens/DefaultToken.md)) has a corresponding type of exception that can be raised in this case: - [`SimpleToken`](../types_of_tokens/SimpleToken.md) -> `CancellationError` - [`ConditionToken`](../types_of_tokens/ConditionToken.md) -> `ConditionCancellationError` diff --git a/docs/what_are_tokens/in_general.md b/docs/what_are_tokens/in_general.md index 25f2779..4c4e38b 100644 --- a/docs/what_are_tokens/in_general.md +++ b/docs/what_are_tokens/in_general.md @@ -1,15 +1,19 @@ A token is an object that can tell you whether to continue the action you started, or whether it has already been canceled. -There are 4 types of tokens in this library: +There are 4 main types of tokens in this library: - [`SimpleToken`](../types_of_tokens/SimpleToken.md) - [`ConditionToken`](../types_of_tokens/ConditionToken.md) - [`TimeoutToken`](../types_of_tokens/TimeoutToken.md) - [`CounterToken`](../types_of_tokens/CounterToken.md) +Additionally, there is a 5th type that cannot be cancelled: + +- [`DefaultToken`](../types_of_tokens/DefaultToken.md) + Each of them has its own characteristics, but they also have something in common: -- Each token can be canceled manually, and some types of tokens can cancel themselves when a condition or timeout occurs. It doesn't matter how the token was canceled, you work with it the same way. +- Each token (except [`DefaultToken`](../types_of_tokens/DefaultToken.md)) can be canceled manually, and some types of tokens can cancel themselves when a condition or timeout occurs. It doesn't matter how the token was canceled, you work with it the same way. - All types of tokens are thread-safe and can be used from multiple threads/coroutines. However, they are not intended to be shared from multiple processes. diff --git a/mkdocs.yml b/mkdocs.yml index 47bbbbb..5c85ab3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,10 +33,12 @@ nav: - ConditionToken: types_of_tokens/ConditionToken.md - TimeoutToken: types_of_tokens/TimeoutToken.md - CounterToken: types_of_tokens/CounterToken.md + - DefaultToken: types_of_tokens/DefaultToken.md - Ecosystem: - About the ecosystem: ecosystem/about_ecosystem.md - Projects: - Subprocess Management: ecosystem/projects/subprocess_management.md + - Regular functions calling: ecosystem/projects/regular_functions_calling.md markdown_extensions: - pymdownx.highlight: anchor_linenums: true diff --git a/pyproject.toml b/pyproject.toml index 043421e..9ba8238 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cantok" -version = "0.0.22" +version = "0.0.23" authors = [ { name="Evgeniy Blinov", email="zheni-b@yandex.ru" }, ] diff --git a/requirements_dev.txt b/requirements_dev.txt index 9b4cf7d..18a0162 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -6,3 +6,4 @@ mypy==1.4.1 ruff==0.0.290 mkdocs-material==9.2.7 mutmut==2.4.4 +full_match==0.0.1 diff --git a/tests/units/test_errors.py b/tests/units/test_errors.py index 7252856..aa1de2b 100644 --- a/tests/units/test_errors.py +++ b/tests/units/test_errors.py @@ -1,21 +1,24 @@ import pytest -from cantok import AbstractToken, SimpleToken, ConditionToken, TimeoutToken, CounterToken -from cantok import CancellationError, ConditionCancellationError, TimeoutCancellationError, CounterCancellationError +from cantok import AbstractToken, SimpleToken, ConditionToken, TimeoutToken, CounterToken, DefaultToken +from cantok import CancellationError, ConditionCancellationError, TimeoutCancellationError, CounterCancellationError, ImpossibleCancelError def test_exception_inheritance_hierarchy(): assert issubclass(ConditionCancellationError, CancellationError) assert issubclass(TimeoutCancellationError, CancellationError) assert issubclass(CounterCancellationError, CancellationError) + assert issubclass(ImpossibleCancelError, CancellationError) def test_exception_inheritance_hierarchy_from_view_of_tokens_classes(): assert issubclass(ConditionToken.exception, SimpleToken.exception) assert issubclass(TimeoutToken.exception, SimpleToken.exception) assert issubclass(CounterToken.exception, SimpleToken.exception) + assert issubclass(DefaultToken.exception, SimpleToken.exception) assert SimpleToken.exception is CancellationError assert ConditionToken.exception is ConditionCancellationError assert TimeoutToken.exception is TimeoutCancellationError assert CounterToken.exception is CounterCancellationError + assert DefaultToken.exception is ImpossibleCancelError diff --git a/tests/units/tokens/test_abstract_token.py b/tests/units/tokens/test_abstract_token.py index 1eca953..50851af 100644 --- a/tests/units/tokens/test_abstract_token.py +++ b/tests/units/tokens/test_abstract_token.py @@ -7,7 +7,7 @@ import pytest from cantok.tokens.abstract_token import AbstractToken, CancelCause, CancellationReport -from cantok import SimpleToken, ConditionToken, TimeoutToken, CounterToken, CancellationError +from cantok import SimpleToken, ConditionToken, TimeoutToken, CounterToken, DefaultToken, CancellationError ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken, TimeoutToken, CounterToken] @@ -31,6 +31,7 @@ def test_cant_instantiate_abstract_token(): ) def test_cancelled_true_as_parameter(token_fabric, cancelled_flag): token = token_fabric(cancelled=cancelled_flag) + assert token.cancelled == cancelled_flag assert token.is_cancelled() == cancelled_flag assert token.keep_on() == (not cancelled_flag) @@ -77,7 +78,7 @@ def test_change_attribute_cancelled(token_fabric, first_cancelled_flag, second_c @pytest.mark.parametrize( 'token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) def test_repr(token_fabric): token = token_fabric() @@ -120,11 +121,11 @@ def test_str(token_fabric): @pytest.mark.parametrize( 'first_token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) @pytest.mark.parametrize( 'second_token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) def test_add_tokens(first_token_fabric, second_token_fabric): first_token = first_token_fabric() @@ -140,7 +141,7 @@ def test_add_tokens(first_token_fabric, second_token_fabric): @pytest.mark.parametrize( 'token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) @pytest.mark.parametrize( 'another_object', @@ -179,7 +180,7 @@ def test_check_cancelled_token(token_fabric): @pytest.mark.parametrize( 'token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) def test_check_superpower_not_raised(token_fabric): token = token_fabric() @@ -226,7 +227,7 @@ def test_check_cancelled_token_nested(token_fabric_1, token_fabric_2): @pytest.mark.parametrize( 'token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) def test_get_report_not_cancelled(token_fabric): token = token_fabric() @@ -271,7 +272,7 @@ def test_get_report_cancelled(token_fabric_1, token_fabric_2): @pytest.mark.parametrize( 'token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) def test_type_conversion_not_cancelled(token_fabric): token = token_fabric() @@ -330,7 +331,7 @@ def test_repr_if_nested_token_is_cancelled(token_fabric_1, token_fabric_2, cance ) @pytest.mark.parametrize( 'token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) @pytest.mark.parametrize( 'do_await', @@ -351,7 +352,7 @@ def test_wait_wrong_parameters(token_fabric, parameters, do_await): @pytest.mark.parametrize( 'token_fabric', - ALL_TOKENS_FABRICS, + ALL_TOKENS_FABRICS + [DefaultToken], ) def test_async_wait_timeout(token_fabric): timeout = 0.0001 diff --git a/tests/units/tokens/test_default_token.py b/tests/units/tokens/test_default_token.py new file mode 100644 index 0000000..5c19209 --- /dev/null +++ b/tests/units/tokens/test_default_token.py @@ -0,0 +1,64 @@ +import sys + +import pytest +import full_match + +from cantok import DefaultToken, SimpleToken, ImpossibleCancelError + + +def test_dafault_token_is_not_cancelled_by_default(): + token = DefaultToken() + + assert bool(token) + assert token.cancelled == False + assert token.is_cancelled() == False + assert token.keep_on() == True + + token.check() + + +def test_you_can_set_cancelled_attribute_as_false(): + token = DefaultToken() + + token.cancelled = False + + assert bool(token) + assert token.cancelled == False + assert token.is_cancelled() == False + assert token.keep_on() == True + + token.check() + + +def test_you_cant_set_true_as_cancelled_attribute(): + token = DefaultToken() + + with pytest.raises(ImpossibleCancelError, match=full_match('You cannot cancel a default token.')): + token.cancelled = True + + assert token.cancelled == False + + +def test_you_cannot_cancel_default_token_by_standard_way(): + token = DefaultToken() + + with pytest.raises(ImpossibleCancelError, match=full_match('You cannot cancel a default token.')): + token.cancel() + + assert token.cancelled == False + + +def test_str_for_default_token(): + assert str(DefaultToken()) == '' + + +@pytest.mark.skipif(sys.version_info >= (3, 10), reason='Format of this exception messages was changed.') +def test_you_cannot_neste_another_token_to_default_one_old_pythons(): + with pytest.raises(TypeError, match=full_match('__init__() takes 1 positional argument but 2 were given')): + DefaultToken(SimpleToken(TypeError)) + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason='Format of this exception messages was changed.') +def test_you_cannot_neste_another_token_to_default_one_new_pythons(): + with pytest.raises(TypeError, match=full_match('DefaultToken.__init__() takes 1 positional argument but 2 were given')): + DefaultToken(SimpleToken(TypeError))