diff --git a/.gitignore b/.gitignore index c31a4e1..f96843c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ venv +venv2 .pytest_cache *.egg-info build diff --git a/.ruff.toml b/.ruff.toml index ec5b9a4..af28669 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1 +1 @@ -ignore = ['E501', 'E712'] +ignore = ['E501', 'E712', 'F821'] diff --git a/cantok/__init__.py b/cantok/__init__.py index 7f6c695..c76e1ad 100644 --- a/cantok/__init__.py +++ b/cantok/__init__.py @@ -1,4 +1,4 @@ -from cantok.tokens.abstract_token import AbstractToken as AbstractToken # noqa: F401 +from cantok.tokens.abstract.abstract_token import AbstractToken as AbstractToken # noqa: F401 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 diff --git a/cantok/errors.py b/cantok/errors.py index 5c57dbc..04dbec5 100644 --- a/cantok/errors.py +++ b/cantok/errors.py @@ -1,8 +1,7 @@ -from typing import Any - - class CancellationError(Exception): - def __init__(self, message: str, token: Any): + token: 'AbstractToken' # type: ignore[name-defined] + + def __init__(self, message: str, token: 'AbstractToken') -> None: # type: ignore[name-defined] self.token = token super().__init__(message) diff --git a/cantok/tokens/abstract/__init__.py b/cantok/tokens/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cantok/tokens/abstract_token.py b/cantok/tokens/abstract/abstract_token.py similarity index 67% rename from cantok/tokens/abstract_token.py rename to cantok/tokens/abstract/abstract_token.py index b34483a..d29dbe6 100644 --- a/cantok/tokens/abstract_token.py +++ b/cantok/tokens/abstract/abstract_token.py @@ -1,72 +1,14 @@ -import weakref from sys import getrefcount -from enum import Enum -from time import sleep as sync_sleep -from asyncio import sleep as async_sleep from abc import ABC, abstractmethod from threading import RLock -from dataclasses import dataclass from typing import List, Dict, Awaitable, Optional, Union, Any -from types import TracebackType -from collections.abc import Coroutine -from cantok.errors import CancellationError - - -class CancelCause(Enum): - CANCELLED = 1 - SUPERPOWER = 2 - NOT_CANCELLED = 3 - -class WaitCoroutineWrapper(Coroutine): # type: ignore[type-arg] - def __init__(self, step: Union[int, float], token_for_wait: 'AbstractToken', token_for_check: 'AbstractToken') -> None: - self.step = step - self.token_for_wait = token_for_wait - self.token_for_check = token_for_check - - self.flags: Dict[str, bool] = {} - self.coroutine = self.async_wait(step, self.flags, token_for_wait, token_for_check) - - weakref.finalize(self, self.sync_wait, step, self.flags, token_for_wait, token_for_check, self.coroutine) - - def __await__(self) -> Any: - return self.coroutine.__await__() - - def send(self, value: Any) -> Any: - return self.coroutine.send(value) - - def throw(self, exception_type: Any, value: Optional[Any] = None, traceback: Optional[TracebackType] = None) -> Any: - pass # pragma: no cover - - def close(self) -> None: - pass # pragma: no cover - - @staticmethod - def sync_wait(step: Union[int, float], flags: Dict[str, bool], token_for_wait: 'AbstractToken', token_for_check: 'AbstractToken', wrapped_coroutine: Coroutine) -> None: # type: ignore[type-arg] - if not flags.get('used', False): - if getrefcount(wrapped_coroutine) < 5: - wrapped_coroutine.close() - - while token_for_wait: - sync_sleep(step) - - token_for_check.check() - - @staticmethod - async def async_wait(step: Union[int, float], flags: Dict[str, bool], token_for_wait: 'AbstractToken', token_for_check: 'AbstractToken') -> None: - flags['used'] = True - while token_for_wait: - await async_sleep(step) - - await async_sleep(0) - - token_for_check.check() +from cantok.errors import CancellationError +from cantok.tokens.abstract.cancel_cause import CancelCause +from cantok.tokens.abstract.report import CancellationReport +from cantok.tokens.abstract.coroutine_wrapper import WaitCoroutineWrapper -@dataclass -class CancellationReport: - cause: CancelCause - from_token: 'AbstractToken' class AbstractToken(ABC): exception = CancellationError @@ -75,9 +17,10 @@ class AbstractToken(ABC): def __init__(self, *tokens: 'AbstractToken', cancelled: bool = False) -> None: from cantok import DefaultToken - self.tokens = [token for token in tokens if not isinstance(token, DefaultToken)] - self._cancelled = cancelled - self.lock = RLock() + self.cached_report: Optional[CancellationReport] = None + self.tokens: List[AbstractToken] = [token for token in tokens if not isinstance(token, DefaultToken)] + self._cancelled: bool = cancelled + self.lock: RLock = RLock() def __repr__(self) -> str: chunks = [] @@ -113,7 +56,15 @@ def __add__(self, item: 'AbstractToken') -> 'AbstractToken': from cantok import SimpleToken - return SimpleToken(self, item) + nested_tokens = [] + + for token in self, item: + if isinstance(token, SimpleToken) and getrefcount(token) < 6: + nested_tokens.extend(token.tokens) + else: + nested_tokens.append(token) + + return SimpleToken(*nested_tokens) def __bool__(self) -> bool: return self.keep_on() @@ -124,11 +75,12 @@ def cancelled(self) -> bool: @cancelled.setter def cancelled(self, new_value: bool) -> None: - if new_value == True: - self._cancelled = True - else: - if self._cancelled == True: - raise ValueError('You cannot restore a cancelled token.') + with self.lock: + if new_value == True: + self._cancelled = True + else: + if self.is_cancelled(): + raise ValueError('You cannot restore a cancelled token.') def keep_on(self) -> bool: return not self.is_cancelled() @@ -159,16 +111,18 @@ def get_report(self, direct: bool = True) -> CancellationReport: cause=CancelCause.CANCELLED, from_token=self, ) - else: - if self.check_superpower(direct): - return CancellationReport( - cause=CancelCause.SUPERPOWER, - from_token=self, - ) + elif self.check_superpower(direct): + return CancellationReport( + cause=CancelCause.SUPERPOWER, + from_token=self, + ) + elif self.cached_report is not None: + return self.cached_report for token in self.tokens: report = token.get_report(direct=False) if report.cause != CancelCause.NOT_CANCELLED: + self.cached_report = report return report return CancellationReport( diff --git a/cantok/tokens/abstract/cancel_cause.py b/cantok/tokens/abstract/cancel_cause.py new file mode 100644 index 0000000..127edc6 --- /dev/null +++ b/cantok/tokens/abstract/cancel_cause.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class CancelCause(Enum): + CANCELLED = 1 + SUPERPOWER = 2 + NOT_CANCELLED = 3 diff --git a/cantok/tokens/abstract/coroutine_wrapper.py b/cantok/tokens/abstract/coroutine_wrapper.py new file mode 100644 index 0000000..6016273 --- /dev/null +++ b/cantok/tokens/abstract/coroutine_wrapper.py @@ -0,0 +1,53 @@ +import weakref +from sys import getrefcount +from typing import Dict, Union, Optional, Any +from types import TracebackType +from collections.abc import Coroutine +from time import sleep as sync_sleep +from asyncio import sleep as async_sleep + + +class WaitCoroutineWrapper(Coroutine): # type: ignore[type-arg] + def __init__(self, step: Union[int, float], token_for_wait: 'AbstractToken', token_for_check: 'AbstractToken') -> None: # type: ignore[name-defined] + self.step = step + self.token_for_wait = token_for_wait + self.token_for_check = token_for_check + + self.flags: Dict[str, bool] = {} + self.coroutine = self.async_wait(step, self.flags, token_for_wait, token_for_check) + + weakref.finalize(self, self.sync_wait, step, self.flags, token_for_wait, token_for_check, self.coroutine) + + def __await__(self) -> Any: + return self.coroutine.__await__() + + def send(self, value: Any) -> Any: + return self.coroutine.send(value) + + def throw(self, exception_type: Any, value: Optional[Any] = None, traceback: Optional[TracebackType] = None) -> Any: + pass # pragma: no cover + + def close(self) -> None: + pass # pragma: no cover + + @staticmethod + def sync_wait(step: Union[int, float], flags: Dict[str, bool], token_for_wait: 'AbstractToken', token_for_check: 'AbstractToken', wrapped_coroutine: Coroutine) -> None: # type: ignore[type-arg, name-defined] + if not flags.get('used', False): + if getrefcount(wrapped_coroutine) < 5: + wrapped_coroutine.close() + + while token_for_wait: + sync_sleep(step) + + token_for_check.check() + + @staticmethod + async def async_wait(step: Union[int, float], flags: Dict[str, bool], token_for_wait: 'AbstractToken', token_for_check: 'AbstractToken') -> None: # type: ignore[name-defined] + flags['used'] = True + + while token_for_wait: + await async_sleep(step) + + await async_sleep(0) + + token_for_check.check() diff --git a/cantok/tokens/abstract/report.py b/cantok/tokens/abstract/report.py new file mode 100644 index 0000000..c88312a --- /dev/null +++ b/cantok/tokens/abstract/report.py @@ -0,0 +1,16 @@ +from typing import Dict +from dataclasses import dataclass +from sys import version_info + +from cantok.tokens.abstract.cancel_cause import CancelCause + + +if version_info >= (3, 10): + addictional_fields: Dict[str, bool] = {'slots': True} # pragma: no cover +else: + addictional_fields = {} # pragma: no cover + +@dataclass(frozen=True, **addictional_fields) # type: ignore[call-overload, unused-ignore] +class CancellationReport: + cause: CancelCause + from_token: 'AbstractToken' # type: ignore[name-defined] diff --git a/cantok/tokens/timeout_token.py b/cantok/tokens/timeout_token.py index b6b00d9..026d1ae 100644 --- a/cantok/tokens/timeout_token.py +++ b/cantok/tokens/timeout_token.py @@ -34,9 +34,11 @@ def text_representation_of_superpower(self) -> str: return str(self.timeout) def get_extra_kwargs(self) -> Dict[str, Any]: - return { - 'monotonic': self.monotonic, - } + if self.monotonic: + return { + 'monotonic': self.monotonic, + } + return {} def get_superpower_exception_message(self) -> str: return f'The timeout of {self.timeout} seconds has expired.' diff --git a/docs/types_of_tokens/SimpleToken.md b/docs/types_of_tokens/SimpleToken.md index c984dbc..14e93ff 100644 --- a/docs/types_of_tokens/SimpleToken.md +++ b/docs/types_of_tokens/SimpleToken.md @@ -15,7 +15,7 @@ print(token.cancelled) # True from cantok import CounterToken, TimeoutToken print(repr(CounterToken(5) + TimeoutToken(5))) -# SimpleToken(CounterToken(5, direct=True), TimeoutToken(5, monotonic=False)) +# SimpleToken(CounterToken(5, direct=True), TimeoutToken(5)) ``` There is not much more to tell about it if you have read [the story](../what_are_tokens/in_general.md) about tokens in general. diff --git a/docs/what_are_tokens/summation.md b/docs/what_are_tokens/summation.md index 641c17f..fa4600e 100644 --- a/docs/what_are_tokens/summation.md +++ b/docs/what_are_tokens/summation.md @@ -4,7 +4,7 @@ Any tokens can be summed up among themselves. The summation operation generates from cantok import SimpleToken, TimeoutToken print(repr(SimpleToken() + TimeoutToken(5))) -# SimpleToken(SimpleToken(), TimeoutToken(5, monotonic=False)) +#> SimpleToken(TimeoutToken(5)) ``` 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: diff --git a/pyproject.toml b/pyproject.toml index 66e1ef9..71fba0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cantok" -version = "0.0.25" +version = "0.0.26" authors = [ { name="Evgeniy Blinov", email="zheni-b@yandex.ru" }, ] @@ -28,6 +28,11 @@ classifiers = [ 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', ] +keywords = [ + 'cancellation tokens', + 'parallel programming', + 'concurrency', +] [tool.setuptools.package-data] "cantok" = ["py.typed"] diff --git a/tests/units/tokens/abstract/__init__.py b/tests/units/tokens/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/units/tokens/test_abstract_token.py b/tests/units/tokens/abstract/test_abstract_token.py similarity index 61% rename from tests/units/tokens/test_abstract_token.py rename to tests/units/tokens/abstract/test_abstract_token.py index 5d17dee..8377ce5 100644 --- a/tests/units/tokens/test_abstract_token.py +++ b/tests/units/tokens/abstract/test_abstract_token.py @@ -4,15 +4,20 @@ from threading import Thread import pytest +import full_match -from cantok.tokens.abstract_token import AbstractToken, CancelCause, CancellationReport +from cantok.tokens.abstract.abstract_token import AbstractToken, CancelCause, CancellationReport from cantok import SimpleToken, ConditionToken, TimeoutToken, CounterToken, DefaultToken, CancellationError ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken, TimeoutToken, CounterToken] +ALL_SUPERPOWER_TOKEN_CLASSES = [ConditionToken, TimeoutToken, CounterToken] ALL_ARGUMENTS_FOR_TOKEN_CLASSES = [tuple(), (lambda: False, ), (15, ), (15, )] +ALL_CANCELLING_ARGUMENTS_FOR_TOKEN_CLASSES_WITH_SUPERPOWERS = [(lambda: True, ), (0, ), (0, )] +ALL_NOT_CANCELLING_ARGUMENTS_FOR_TOKEN_CLASSES_WITH_SUPERPOWERS = [(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)] - +ALL_TOKENS_FABRICS_WITH_CANCELLING_SUPERPOWER = [partial(token_class, *arguments) for token_class, arguments in zip(ALL_SUPERPOWER_TOKEN_CLASSES, ALL_CANCELLING_ARGUMENTS_FOR_TOKEN_CLASSES_WITH_SUPERPOWERS)] +ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER = [partial(token_class, *arguments) for token_class, arguments in zip(ALL_SUPERPOWER_TOKEN_CLASSES, ALL_NOT_CANCELLING_ARGUMENTS_FOR_TOKEN_CLASSES_WITH_SUPERPOWERS)] def test_cant_instantiate_abstract_token(): @@ -75,6 +80,17 @@ def test_change_attribute_cancelled(token_fabric, first_cancelled_flag, second_c token.check() +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +def test_set_cancelled_false_if_this_token_is_not_cancelled_but_nested_token_is(token_fabric): + token = token_fabric(SimpleToken(cancelled=True)) + + with pytest.raises(ValueError, match=full_match('You cannot restore a cancelled token.')): + token.cancelled = False + + @pytest.mark.parametrize( 'token_fabric', ALL_TOKENS_FABRICS + [DefaultToken], @@ -138,6 +154,92 @@ def test_add_tokens(first_token_fabric, second_token_fabric): assert tokens_sum.tokens[1] is second_token +@pytest.mark.parametrize( + 'first_token_fabric', + ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER, +) +@pytest.mark.parametrize( + 'second_token_fabric', + ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER, +) +@pytest.mark.parametrize( + 'third_token_fabric', + ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER, +) +def test_add_three_tokens_except_simple_token(first_token_fabric, second_token_fabric, third_token_fabric): + first_token = first_token_fabric() + second_token = second_token_fabric() + third_token = third_token_fabric() + + tokens_sum = first_token + second_token + third_token + + assert isinstance(tokens_sum, SimpleToken) + assert len(tokens_sum.tokens) == 3 + assert tokens_sum.tokens[0] is first_token + assert tokens_sum.tokens[1] is second_token + assert tokens_sum.tokens[2] is third_token + + +@pytest.mark.parametrize( + 'first_token_fabric', + ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER, +) +def test_add_another_token_and_temp_simple_token(first_token_fabric): + first_token = first_token_fabric() + + tokens_sum = first_token + SimpleToken() + + assert isinstance(tokens_sum, SimpleToken) + assert len(tokens_sum.tokens) == 1 + assert tokens_sum.tokens[0] is first_token + + +@pytest.mark.parametrize( + 'second_token_fabric', + ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER, +) +def test_add_temp_simple_token_and_another_token(second_token_fabric): + second_token = second_token_fabric() + + tokens_sum = SimpleToken() + second_token + + assert isinstance(tokens_sum, SimpleToken) + assert len(tokens_sum.tokens) == 1 + assert tokens_sum.tokens[0] is second_token + + +@pytest.mark.parametrize( + 'first_token_fabric', + ALL_TOKENS_FABRICS_WITH_NOT_CANCELLING_SUPERPOWER, +) +def test_add_another_token_and_not_temp_simple_token(first_token_fabric): + simple_token = SimpleToken() + first_token = first_token_fabric() + + tokens_sum = first_token + simple_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 simple_token + + +@pytest.mark.parametrize( + 'second_token_fabric', + [x for x in ALL_TOKENS_FABRICS if x is not SimpleToken], +) +def test_add_not_temp_simple_token_and_another_token(second_token_fabric): + simple_token = SimpleToken() + second_token = second_token_fabric() + + tokens_sum = simple_token + second_token + + assert isinstance(tokens_sum, SimpleToken) + assert len(tokens_sum.tokens) == 2 + assert tokens_sum.tokens[0] is simple_token + assert tokens_sum.tokens[1] is second_token + + @pytest.mark.parametrize( 'second_token_fabric', ALL_TOKENS_FABRICS, @@ -446,3 +548,124 @@ def test_insert_default_token_to_another_tokens(token_fabric): assert not isinstance(token, DefaultToken) assert len(token.tokens) == 0 + + +@pytest.mark.parametrize( + 'first_token_fabric', + ALL_TOKENS_FABRICS, +) +@pytest.mark.parametrize( + 'second_token_fabric', + ALL_TOKENS_FABRICS, +) +@pytest.mark.parametrize( + 'action', + [ + lambda x: x.cancelled, + lambda x: x.keep_on(), + lambda x: bool(x), + lambda x: bool(x), + lambda x: x.is_cancelled(), + lambda x: x.get_report(True), + lambda x: x.get_report(False), + ], +) +def test_report_cache_is_working_in_simple_case(first_token_fabric, second_token_fabric, action): + token = first_token_fabric(second_token_fabric(cancelled=True)) + + assert token.cached_report is None + + action(token) + + cached_report = token.cached_report + + assert cached_report is not None + assert isinstance(cached_report, CancellationReport) + assert cached_report.from_token.is_cancelled() + assert cached_report.cause == CancelCause.CANCELLED + + assert token.get_report(True) is cached_report + assert token.get_report(False) is cached_report + + +@pytest.mark.parametrize( + 'first_token_fabric', + ALL_TOKENS_FABRICS, +) +@pytest.mark.parametrize( + 'second_token_fabric', + ALL_TOKENS_FABRICS, +) +@pytest.mark.parametrize( + 'action', + [ + lambda x: x.cancelled, + lambda x: x.keep_on(), + lambda x: bool(x), + lambda x: bool(x), + lambda x: x.is_cancelled(), + lambda x: x.get_report(True), + lambda x: x.get_report(False), + ], +) +def test_cache_is_using_after_self_flag(first_token_fabric, second_token_fabric, action): + token = first_token_fabric(second_token_fabric(cancelled=True)) + + action(token) + + cached_report = token.cached_report + + token.cancel() + + for new_report in token.get_report(True), token.get_report(False): + assert new_report is not cached_report + assert new_report is not None + assert isinstance(new_report, CancellationReport) + assert new_report.from_token.is_cancelled() + assert new_report.cause == CancelCause.CANCELLED + + +@pytest.mark.parametrize( + 'first_token_fabric', + ALL_TOKENS_FABRICS_WITH_CANCELLING_SUPERPOWER, +) +@pytest.mark.parametrize( + 'second_token_fabric', + ALL_TOKENS_FABRICS_WITH_CANCELLING_SUPERPOWER, +) +@pytest.mark.parametrize( + 'action', + [ + lambda x: x.cancelled, + lambda x: x.keep_on(), + lambda x: bool(x), + lambda x: bool(x), + lambda x: x.is_cancelled(), + lambda x: x.get_report(True), + lambda x: x.get_report(False), + ], +) +def test_superpower_is_more_important_than_cache(first_token_fabric, second_token_fabric, action): + token = first_token_fabric(second_token_fabric(cancelled=True)) + + for report in token.get_report(True), token.get_report(False): + assert report is not None + assert isinstance(report, CancellationReport) + assert report.from_token is token + assert report.cause == CancelCause.SUPERPOWER + + action(token) + + for report in token.get_report(True), token.get_report(False): + assert report is not None + assert isinstance(report, CancellationReport) + assert report.from_token is token + assert report.cause == CancelCause.SUPERPOWER + + token.cancel() + + for report in token.get_report(True), token.get_report(False): + assert report is not None + assert isinstance(report, CancellationReport) + assert report.from_token is token + assert report.cause == CancelCause.CANCELLED diff --git a/tests/units/tokens/abstract/test_report.py b/tests/units/tokens/abstract/test_report.py new file mode 100644 index 0000000..6574553 --- /dev/null +++ b/tests/units/tokens/abstract/test_report.py @@ -0,0 +1,27 @@ +from sys import getsizeof, version_info +from dataclasses import FrozenInstanceError + +import pytest + +from cantok import SimpleToken, TimeoutToken +from cantok.tokens.abstract.abstract_token import CancelCause, CancellationReport + + +def test_cant_change_cancellation_report(): + report = CancellationReport( + cause=CancelCause.NOT_CANCELLED, + from_token=SimpleToken(), + ) + + with pytest.raises(FrozenInstanceError): + report.from_token = TimeoutToken(1) + + +@pytest.mark.skipif(version_info < (3, 8), reason='There is no support of __slots__ for dataclasses in old pythons.') +def test_size_of_report_is_not_so_big(): + report = CancellationReport( + cause=CancelCause.NOT_CANCELLED, + from_token=SimpleToken(), + ) + + assert getsizeof(report) <= 48 diff --git a/tests/units/tokens/test_condition_token.py b/tests/units/tokens/test_condition_token.py index 05c01d4..1126888 100644 --- a/tests/units/tokens/test_condition_token.py +++ b/tests/units/tokens/test_condition_token.py @@ -4,7 +4,7 @@ import pytest -from cantok.tokens.abstract_token import CancelCause, CancellationReport +from cantok.tokens.abstract.abstract_token import CancelCause, CancellationReport from cantok import SimpleToken, ConditionToken, ConditionCancellationError @@ -340,3 +340,107 @@ def condition(): assert counter == 5 assert token.cancelled == False assert counter == 6 + + +def test_quasitemp_condition_token_plus_temp_simple_token(): + token = ConditionToken(lambda: False) + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], ConditionToken) + + +def test_not_quasitemp_condition_token_plus_temp_simple_token(): + condition_token = ConditionToken(lambda: False) + token = condition_token + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], ConditionToken) + assert token.tokens[0] is condition_token + + +def test_quasitemp_condition_token_plus_not_temp_simple_token(): + simple_token = SimpleToken() + token = ConditionToken(lambda: False) + simple_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[0], ConditionToken) + assert token.tokens[1] is simple_token + + +def test_not_quasitemp_condition_token_plus_not_temp_simple_token(): + simple_token = SimpleToken() + condition_token = ConditionToken(lambda: False) + token = condition_token + simple_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[0], ConditionToken) + assert token.tokens[0] is condition_token + assert token.tokens[1] is simple_token + + +def test_quasitemp_condition_token_plus_temp_simple_token_reverse(): + token = SimpleToken() + ConditionToken(lambda: False) + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], ConditionToken) + + +def test_not_quasitemp_condition_token_plus_temp_simple_token_reverse(): + condition_token = ConditionToken(lambda: False) + token = SimpleToken() + condition_token + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], ConditionToken) + assert token.tokens[0] is condition_token + + +def test_quasitemp_condition_token_plus_not_temp_simple_token_reverse(): + simple_token = SimpleToken() + token = simple_token + ConditionToken(lambda: False) + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[1], ConditionToken) + assert token.tokens[0] is simple_token + + +def test_not_quasitemp_condition_token_plus_not_temp_simple_token_reverse(): + simple_token = SimpleToken() + condition_token = ConditionToken(lambda: False) + token = simple_token + condition_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[1], ConditionToken) + assert token.tokens[1] is condition_token + assert token.tokens[0] is simple_token + + +def test_condition_function_is_more_important_than_cache(): + flag = False + inner_token = SimpleToken(cancelled=True) + token = ConditionToken(lambda: flag, inner_token) + + for report in token.get_report(True), token.get_report(False): + assert report is not None + assert isinstance(report, CancellationReport) + assert report.from_token is inner_token + assert report.cause == CancelCause.CANCELLED + + flag = True + + for report in token.get_report(True), token.get_report(False): + assert report is not None + assert isinstance(report, CancellationReport) + assert report.from_token is token + assert report.cause == CancelCause.SUPERPOWER diff --git a/tests/units/tokens/test_counter_token.py b/tests/units/tokens/test_counter_token.py index ea6474b..1fd106e 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.abstract_token import CancelCause, CancellationReport +from cantok.tokens.abstract.abstract_token import CancelCause, CancellationReport from cantok import CounterToken, SimpleToken, CounterCancellationError @@ -227,3 +227,87 @@ def test_decrement_counter_after_zero(): token.is_cancelled() assert token.counter == 0 + + +def test_quasitemp_counter_token_plus_temp_simple_token(): + token = CounterToken(0) + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], CounterToken) + + +def test_not_quasitemp_counter_token_plus_temp_simple_token(): + counter_token = CounterToken(1) + token = counter_token + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], CounterToken) + assert token.tokens[0] is counter_token + + +def test_quasitemp_counter_token_plus_not_temp_simple_token(): + simple_token = SimpleToken() + token = CounterToken(1) + simple_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[0], CounterToken) + assert token.tokens[1] is simple_token + + +def test_not_quasitemp_counter_token_plus_not_temp_simple_token(): + simple_token = SimpleToken() + counter_token = CounterToken(1) + token = counter_token + simple_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[0], CounterToken) + assert token.tokens[0] is counter_token + assert token.tokens[1] is simple_token + + +def test_quasitemp_counter_token_plus_temp_simple_token_reverse(): + token = SimpleToken() + CounterToken(1) + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], CounterToken) + + +def test_not_quasitemp_counter_token_plus_temp_simple_token_reverse(): + counter_token = CounterToken(1) + token = SimpleToken() + counter_token + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], CounterToken) + assert token.tokens[0] is counter_token + + +def test_quasitemp_counter_token_plus_not_temp_simple_token_reverse(): + simple_token = SimpleToken() + token = simple_token + CounterToken(1) + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[1], CounterToken) + assert token.tokens[0] is simple_token + + +def test_not_quasitemp_counter_token_plus_not_temp_simple_token_reverse(): + simple_token = SimpleToken() + counter_token = CounterToken(1) + token = simple_token + counter_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[1], CounterToken) + assert token.tokens[1] is counter_token + assert token.tokens[0] is simple_token diff --git a/tests/units/tokens/test_default_token.py b/tests/units/tokens/test_default_token.py index 45df234..18ae8eb 100644 --- a/tests/units/tokens/test_default_token.py +++ b/tests/units/tokens/test_default_token.py @@ -69,3 +69,27 @@ def test_default_plus_default(): assert isinstance(empty_sum, SimpleToken) assert len(empty_sum.tokens) == 0 + + +def test_default_plus_default_plus_default(): + empty_sum = DefaultToken() + DefaultToken() + DefaultToken() + + assert isinstance(empty_sum, SimpleToken) + assert len(empty_sum.tokens) == 0 + + +def test_default_token_plus_temp_simple_token(): + empty_sum = DefaultToken() + SimpleToken() + + assert isinstance(empty_sum, SimpleToken) + assert len(empty_sum.tokens) == 0 + + +def test_default_token_plus_not_temp_simple_token(): + simple_token = SimpleToken() + sum = DefaultToken() + simple_token + + assert isinstance(sum, SimpleToken) + assert len(sum.tokens) == 1 + assert sum is not simple_token + assert sum.tokens[0] is simple_token diff --git a/tests/units/tokens/test_simple_token.py b/tests/units/tokens/test_simple_token.py index d92c392..5b6ce2a 100644 --- a/tests/units/tokens/test_simple_token.py +++ b/tests/units/tokens/test_simple_token.py @@ -1,6 +1,6 @@ import pytest -from cantok.tokens.abstract_token import CancelCause, CancellationReport +from cantok.tokens.abstract.abstract_token import CancelCause, CancellationReport from cantok import SimpleToken, CancellationError @@ -110,3 +110,59 @@ def test_get_report_cancelled_nested(cancelled_flag, cancelled_flag_nested, from assert report.from_token is nested_token else: assert report.from_token is token + + +def test_sum_of_2_temp_simple_tokens(): + token = SimpleToken() + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 0 + + +def test_sum_of_5_temp_simple_tokens(): + token = SimpleToken() + SimpleToken() + SimpleToken() + SimpleToken() + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 0 + + +def test_sum_of_1_temp_and_1_not_temp_simple_tokens(): + second_token = SimpleToken() + result = SimpleToken() + second_token + + assert isinstance(result, SimpleToken) + assert len(result.tokens) == 1 + assert result.tokens[0] is second_token + + +def test_sum_of_1_not_temp_and_1_temp_simple_tokens(): + first_token = SimpleToken() + result = first_token + SimpleToken() + + assert isinstance(result, SimpleToken) + assert len(result.tokens) == 1 + assert result.tokens[0] is first_token + + +def test_sum_of_2_not_temp_simple_tokens(): + first_token = SimpleToken() + second_token = SimpleToken() + result = first_token + second_token + + assert isinstance(result, SimpleToken) + assert len(result.tokens) == 2 + assert result.tokens[0] is first_token + assert result.tokens[1] is second_token + + +def test_sum_of_3_not_temp_simple_tokens(): + first_token = SimpleToken() + second_token = SimpleToken() + third_token = SimpleToken() + result = first_token + second_token + third_token + + assert isinstance(result, SimpleToken) + assert len(result.tokens) == 3 + assert result.tokens[0] is first_token + assert result.tokens[1] is second_token + assert result.tokens[2] is third_token diff --git a/tests/units/tokens/test_timeout_token.py b/tests/units/tokens/test_timeout_token.py index ef1a432..50f6cf7 100644 --- a/tests/units/tokens/test_timeout_token.py +++ b/tests/units/tokens/test_timeout_token.py @@ -3,7 +3,7 @@ import pytest -from cantok.tokens.abstract_token import CancelCause, CancellationReport +from cantok.tokens.abstract.abstract_token import CancelCause, CancellationReport from cantok import SimpleToken, TimeoutToken, TimeoutCancellationError @@ -88,9 +88,21 @@ def test_timeout_expired(options): def test_text_representaion_of_extra_kwargs(): - assert TimeoutToken(5, monotonic=False).text_representation_of_extra_kwargs() == 'monotonic=False' + assert TimeoutToken(5, monotonic=False).text_representation_of_extra_kwargs() == '' assert TimeoutToken(5, monotonic=True).text_representation_of_extra_kwargs() == 'monotonic=True' - assert TimeoutToken(5).text_representation_of_extra_kwargs() == 'monotonic=False' + assert TimeoutToken(5).text_representation_of_extra_kwargs() == '' + + +@pytest.mark.parametrize( + ['options', 'repr_string'], + [ + ({}, 'TimeoutToken(1)'), + ({'monotonic': True}, 'TimeoutToken(1, monotonic=True)'), + ({'monotonic': False}, 'TimeoutToken(1)'), + ], +) +def test_repr_of_timeout_token(options, repr_string): + assert repr(TimeoutToken(1, **options)) == repr_string def test_check_superpower_raised(): @@ -207,3 +219,107 @@ def test_timeout_wait(): finish_time = perf_counter() assert sleep_duration <= finish_time - start_time + + +def test_quasitemp_timeout_token_plus_temp_simple_token(): + token = TimeoutToken(1) + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], TimeoutToken) + + +def test_not_quasitemp_timeout_token_plus_temp_simple_token(): + timeout_token = TimeoutToken(1) + token = timeout_token + SimpleToken() + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], TimeoutToken) + assert token.tokens[0] is timeout_token + + +def test_quasitemp_timeout_token_plus_not_temp_simple_token(): + simple_token = SimpleToken() + token = TimeoutToken(1) + simple_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[0], TimeoutToken) + assert token.tokens[1] is simple_token + + +def test_not_quasitemp_timeout_token_plus_not_temp_simple_token(): + simple_token = SimpleToken() + timeout_token = TimeoutToken(1) + token = timeout_token + simple_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[0], TimeoutToken) + assert token.tokens[0] is timeout_token + assert token.tokens[1] is simple_token + + +def test_quasitemp_timeout_token_plus_temp_simple_token_reverse(): + token = SimpleToken() + TimeoutToken(1) + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], TimeoutToken) + + +def test_not_quasitemp_timeout_token_plus_temp_simple_token_reverse(): + timeout_token = TimeoutToken(1) + token = SimpleToken() + timeout_token + + assert isinstance(token, SimpleToken) + assert len(token.tokens) == 1 + assert isinstance(token.tokens[0], TimeoutToken) + assert token.tokens[0] is timeout_token + + +def test_quasitemp_timeout_token_plus_not_temp_simple_token_reverse(): + simple_token = SimpleToken() + token = simple_token + TimeoutToken(1) + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[1], TimeoutToken) + assert token.tokens[0] is simple_token + + +def test_not_quasitemp_timeout_token_plus_not_temp_simple_token_reverse(): + simple_token = SimpleToken() + timeout_token = TimeoutToken(1) + token = simple_token + timeout_token + + assert isinstance(token, SimpleToken) + assert token is not simple_token + assert len(token.tokens) == 2 + assert isinstance(token.tokens[1], TimeoutToken) + assert token.tokens[1] is timeout_token + assert token.tokens[0] is simple_token + + +def test_timeout_is_more_important_than_cache(): + sleep_time = 0.0001 + inner_token = SimpleToken(cancelled=True) + token = TimeoutToken(sleep_time, inner_token) + + for report in token.get_report(True), token.get_report(False): + assert report is not None + assert isinstance(report, CancellationReport) + assert report.from_token is inner_token + assert report.cause == CancelCause.CANCELLED + + sleep(sleep_time * 2) + + for report in token.get_report(True), token.get_report(False): + assert report is not None + assert isinstance(report, CancellationReport) + assert report.from_token is token + assert report.cause == CancelCause.SUPERPOWER