diff --git a/cantok/tokens/abstract/abstract_token.py b/cantok/tokens/abstract/abstract_token.py index 411d8aa..6697744 100644 --- a/cantok/tokens/abstract/abstract_token.py +++ b/cantok/tokens/abstract/abstract_token.py @@ -1,10 +1,10 @@ from abc import ABC, abstractmethod from threading import RLock -from typing import Any, Awaitable, Dict, List, Optional, Union +from time import sleep +from typing import Any, Dict, List, Optional, Union from cantok.errors import CancellationError from cantok.tokens.abstract.cancel_cause import CancelCause -from cantok.tokens.abstract.coroutine_wrapper import WaitCoroutineWrapper from cantok.tokens.abstract.report import CancellationReport @@ -156,22 +156,18 @@ def wait( self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, float]] = None, - ) -> Awaitable: # type: ignore[type-arg] + ) -> None: """ Waits until the token is cancelled. - When used with ``await``, runs non-blocking inside an asyncio event loop. - When called without ``await``, blocks the current thread. + Blocks the current thread until the token is cancelled. :param step: Interval between status checks, in seconds. Defaults to 0.0001. :param timeout: Maximum time to wait, in seconds. If exceeded, raises TimeoutCancellationError. Defaults to None (no limit). - >>> import asyncio - >>> >>> token = TimeoutToken(5) >>> token.wait() # blocks for ~5 seconds, then returns - >>> asyncio.run(TimeoutToken(5).wait()) # non-blocking, inside an asyncio event loop """ if step < 0: raise ValueError( @@ -193,7 +189,12 @@ def wait( token = TimeoutToken(timeout) - return WaitCoroutineWrapper(step, self + token, token) + token_for_wait = self + token + + while token_for_wait: + sleep(step) + + token.check() def cancel(self) -> 'AbstractToken': """ diff --git a/cantok/tokens/abstract/coroutine_wrapper.py b/cantok/tokens/abstract/coroutine_wrapper.py deleted file mode 100644 index 400e562..0000000 --- a/cantok/tokens/abstract/coroutine_wrapper.py +++ /dev/null @@ -1,67 +0,0 @@ -import sys -import weakref -from asyncio import sleep as async_sleep -from collections.abc import Coroutine -from time import sleep as sync_sleep -from types import TracebackType -from typing import Any, Dict, Optional, Union - -from displayhooks import not_display - - -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): - # In Python <=3.13, LOAD_FAST increments refcount, so getrefcount() returns - # true_refs + 2; threshold < 5 means "fewer than 3 external refs" (i.e. only - # the finalize args-tuple and this parameter hold the coroutine, indicating it - # is not being actively awaited by the event loop). - # In Python 3.14+, LOAD_FAST_BORROW does not increment refcount, AND - # native_coro.__await__() returns a new coroutine_wrapper object instead of - # the coroutine itself, so getrefcount() returns true_refs + 1; threshold < 4 - # expresses the same "fewer than 3 external refs" condition. - _refcount_threshold = 4 if sys.version_info >= (3, 14) else 5 - if sys.getrefcount(wrapped_coroutine) < _refcount_threshold: - 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: # noqa: ASYNC110 - await async_sleep(step) - - await async_sleep(0) - - token_for_check.check() - - -not_display(WaitCoroutineWrapper) diff --git a/docs/what_are_tokens/exceptions.md b/docs/what_are_tokens/exceptions.md index 34b3b77..78261c7 100644 --- a/docs/what_are_tokens/exceptions.md +++ b/docs/what_are_tokens/exceptions.md @@ -10,6 +10,19 @@ token.check() #> cantok.errors.TimeoutCancellationError: The timeout of 1 second has expired. ``` +The `wait()` method can also raise an exception directly when its own waiting timeout expires: + +```python +from cantok import SimpleToken, TimeoutCancellationError + +token = SimpleToken() + +try: + token.wait(timeout=1) +except TimeoutCancellationError: + print('Waiting took too long.') +``` + 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` diff --git a/docs/what_are_tokens/waiting.md b/docs/what_are_tokens/waiting.md index d655921..8dc841b 100644 --- a/docs/what_are_tokens/waiting.md +++ b/docs/what_are_tokens/waiting.md @@ -1,4 +1,4 @@ -Each token has a `wait()` method, which allows you to wait for its cancellation. +Each token has a `wait()` method, which allows you to block the current thread until the token is cancelled. ```python from cantok import TimeoutToken @@ -10,26 +10,26 @@ token.check() # Since the timeout has expired, an exception will be raised. #> cantok.errors.TimeoutCancellationError: The timeout of 5 seconds has expired. ``` -If you add the `await` keyword before calling `wait()`, the method will be automatically used in non-blocking mode: +This is useful when one thread needs to wait for cancellation requested from another thread: ```python -import asyncio +from threading import Thread +from time import sleep from cantok import SimpleToken -async def do_something(token): - await asyncio.sleep(3) # Imitation of some real async activity. +def do_something(token): + sleep(3) # Imitation of some real activity. token.cancel() -async def main(): - token = SimpleToken() - await asyncio.gather(do_something(token), token.wait()) - print('Something has been done!') +token = SimpleToken() +thread = Thread(target=do_something, args=(token,)) +thread.start() -asyncio.run(main()) +token.wait() +thread.join() +print('Something has been done!') ``` -Yes, it looks like magic — it is magic. The method itself finds out how it was used: inside an expression with or without the `await` keyword. In the first case, it runs in CPU-saving mode, in the second — in non-blocking event-loop mode. - In addition to the above, the `wait()` method has two optional arguments: - **`timeout`** (`int` or `float`) — the maximum waiting time in seconds. If this time is exceeded, a [`TimeoutCancellationError` exception](../what_are_tokens/exceptions.md) will be raised. By default, the `timeout` is not set. diff --git a/pyproject.toml b/pyproject.toml index 3d841c0..93aedc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "cantok" -version = "0.0.37" +version = "0.0.38" authors = [{ name = "Evgeniy Blinov", email = "zheni-b@yandex.ru" }] description = 'Implementation of the "Cancellation Token" pattern' readme = "README.md" requires-python = ">=3.8" dependencies = [ - 'displayhooks>=0.0.6', 'transfunctions>=0.0.12', 'typing_extensions ; python_version <= "3.9"', ] diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 46d4276..fc64da2 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -1,10 +1,7 @@ -import asyncio -from contextlib import redirect_stdout -from io import StringIO from random import randint from threading import Thread -from cantok import ConditionToken, CounterToken, SimpleToken, TimeoutToken +from cantok import ConditionToken, CounterToken, TimeoutToken counter = 0 @@ -30,38 +27,3 @@ def test_cancel_simple_token_with_function_and_thread_2(): counter += 1 assert counter - - -def test_waiting_of_cancelled_token(): - async def do_something(token): - await asyncio.sleep(0.1) # Imitation of some real async activity. - token.cancel() - - async def main(): - token = SimpleToken() - await do_something(token) - await token.wait() - print('Something has been done!') # noqa: T201 - - buffer = StringIO() - with redirect_stdout(buffer): - asyncio.run(main()) - - assert buffer.getvalue() == 'Something has been done!\n' - - -def test_waiting_of_cancelled_token_with_gather(): - async def do_something(token): - await asyncio.sleep(0.1) # Imitation of some real async activity. - token.cancel() - - async def main(): - token = SimpleToken() - await asyncio.gather(do_something(token), token.wait()) - print('Something has been done!') # noqa: T201 - - buffer = StringIO() - with redirect_stdout(buffer): - asyncio.run(main()) - - assert buffer.getvalue() == 'Something has been done!\n' diff --git a/tests/skipped/tokens/__init__.py b/tests/skipped/sum_optimization/__init__.py similarity index 100% rename from tests/skipped/tokens/__init__.py rename to tests/skipped/sum_optimization/__init__.py diff --git a/tests/skipped/tokens/abstract/__init__.py b/tests/skipped/sum_optimization/tokens/__init__.py similarity index 100% rename from tests/skipped/tokens/abstract/__init__.py rename to tests/skipped/sum_optimization/tokens/__init__.py diff --git a/tests/skipped/sum_optimization/tokens/abstract/__init__.py b/tests/skipped/sum_optimization/tokens/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/skipped/tokens/abstract/test_abstract_token.py b/tests/skipped/sum_optimization/tokens/abstract/test_abstract_token.py similarity index 100% rename from tests/skipped/tokens/abstract/test_abstract_token.py rename to tests/skipped/sum_optimization/tokens/abstract/test_abstract_token.py diff --git a/tests/skipped/tokens/test_condition_token.py b/tests/skipped/sum_optimization/tokens/test_condition_token.py similarity index 100% rename from tests/skipped/tokens/test_condition_token.py rename to tests/skipped/sum_optimization/tokens/test_condition_token.py diff --git a/tests/skipped/tokens/test_counter_token.py b/tests/skipped/sum_optimization/tokens/test_counter_token.py similarity index 100% rename from tests/skipped/tokens/test_counter_token.py rename to tests/skipped/sum_optimization/tokens/test_counter_token.py diff --git a/tests/skipped/tokens/test_default_token.py b/tests/skipped/sum_optimization/tokens/test_default_token.py similarity index 100% rename from tests/skipped/tokens/test_default_token.py rename to tests/skipped/sum_optimization/tokens/test_default_token.py diff --git a/tests/skipped/tokens/test_simple_token.py b/tests/skipped/sum_optimization/tokens/test_simple_token.py similarity index 100% rename from tests/skipped/tokens/test_simple_token.py rename to tests/skipped/sum_optimization/tokens/test_simple_token.py diff --git a/tests/skipped/tokens/test_timeout_token.py b/tests/skipped/sum_optimization/tokens/test_timeout_token.py similarity index 100% rename from tests/skipped/tokens/test_timeout_token.py rename to tests/skipped/sum_optimization/tokens/test_timeout_token.py diff --git a/tests/skipped/sync_and_async_wait/__init__.py b/tests/skipped/sync_and_async_wait/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/skipped/sync_and_async_wait/examples/__init__.py b/tests/skipped/sync_and_async_wait/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/skipped/sync_and_async_wait/examples/test_examples.py b/tests/skipped/sync_and_async_wait/examples/test_examples.py new file mode 100644 index 0000000..f093216 --- /dev/null +++ b/tests/skipped/sync_and_async_wait/examples/test_examples.py @@ -0,0 +1,44 @@ +import asyncio +from contextlib import redirect_stdout +from io import StringIO + +import pytest + +from cantok import SimpleToken + + +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_waiting_of_cancelled_token(): + async def do_something(token): + await asyncio.sleep(0.1) # Imitation of some real async activity. + token.cancel() + + async def main(): + token = SimpleToken() + await do_something(token) + await token.wait() + print('Something has been done!') # noqa: T201 + + buffer = StringIO() + with redirect_stdout(buffer): + asyncio.run(main()) + + assert buffer.getvalue() == 'Something has been done!\n' + + +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_waiting_of_cancelled_token_with_gather(): + async def do_something(token): + await asyncio.sleep(0.1) # Imitation of some real async activity. + token.cancel() + + async def main(): + token = SimpleToken() + await asyncio.gather(do_something(token), token.wait()) + print('Something has been done!') # noqa: T201 + + buffer = StringIO() + with redirect_stdout(buffer): + asyncio.run(main()) + + assert buffer.getvalue() == 'Something has been done!\n' diff --git a/tests/skipped/sync_and_async_wait/units/__init__.py b/tests/skipped/sync_and_async_wait/units/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/skipped/sync_and_async_wait/units/tokens/__init__.py b/tests/skipped/sync_and_async_wait/units/tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/skipped/sync_and_async_wait/units/tokens/abstract/__init__.py b/tests/skipped/sync_and_async_wait/units/tokens/abstract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/skipped/sync_and_async_wait/units/tokens/abstract/test_abstract_token.py b/tests/skipped/sync_and_async_wait/units/tokens/abstract/test_abstract_token.py new file mode 100644 index 0000000..5567082 --- /dev/null +++ b/tests/skipped/sync_and_async_wait/units/tokens/abstract/test_abstract_token.py @@ -0,0 +1,89 @@ +import asyncio +from functools import partial +from time import perf_counter + +import pytest + +from cantok import ( + ConditionToken, + CounterToken, + DefaultToken, + SimpleToken, + TimeoutToken, +) + +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)] + + +@pytest.mark.parametrize( + 'parameters', + [ + {'step': -1}, + {'step': -1, 'timeout': -1}, + {'step': -1, 'timeout': 0}, + {'timeout': -1}, + {'step': 1, 'timeout': -1}, + {'step': -1, 'timeout': 1}, + {'step': 2, 'timeout': 1}, + ], +) +@pytest.mark.parametrize( + 'token_fabric', + [*ALL_TOKENS_FABRICS, DefaultToken], +) +@pytest.mark.parametrize( + 'do_await', + [ + True, + ], +) +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_wait_wrong_parameters(token_fabric, parameters, do_await): + token = token_fabric() + + if do_await: + with pytest.raises(ValueError, match=r'.'): + asyncio.run(token.wait(**parameters)) + else: + with pytest.raises(ValueError, match=r'.'): + token.wait(**parameters) + + +@pytest.mark.parametrize( + 'token_fabric', + [*ALL_TOKENS_FABRICS, DefaultToken], +) +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_async_wait_timeout(token_fabric): + timeout = 0.0001 + token = token_fabric() + + with pytest.raises(TimeoutToken.exception): + asyncio.run(token.wait(timeout=timeout)) + + +@pytest.mark.parametrize( + 'token_fabric', + ALL_TOKENS_FABRICS, +) +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_async_wait_with_cancel(token_fabric): + timeout = 0.001 + token = token_fabric() + + async def cancel_with_timeout(token): + await asyncio.sleep(timeout) + token.cancel() + + async def runner(token): + coroutines = [cancel_with_timeout(token), token.wait() ] + return await asyncio.gather(*coroutines) + + start_time = perf_counter() + asyncio.run(runner(token)) + finish_time = perf_counter() + + assert not token + assert finish_time - start_time >= timeout diff --git a/tests/units/tokens/abstract/test_coroutine_wrapper.py b/tests/skipped/sync_and_async_wait/units/tokens/abstract/test_coroutine_wrapper.py similarity index 82% rename from tests/units/tokens/abstract/test_coroutine_wrapper.py rename to tests/skipped/sync_and_async_wait/units/tokens/abstract/test_coroutine_wrapper.py index 108e0fc..1d376a6 100644 --- a/tests/units/tokens/abstract/test_coroutine_wrapper.py +++ b/tests/skipped/sync_and_async_wait/units/tokens/abstract/test_coroutine_wrapper.py @@ -18,6 +18,7 @@ (lambda: 'kek', f'{"kek"!r}\n'), ], ) +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') def test_displayhook_printing_coroutine_wrappers_and_other_objects(create_value, expected_string): buffer = io.StringIO() with redirect_stdout(buffer): diff --git a/tests/skipped/sync_and_async_wait/units/tokens/test_condition_token.py b/tests/skipped/sync_and_async_wait/units/tokens/test_condition_token.py new file mode 100644 index 0000000..d6cd90f --- /dev/null +++ b/tests/skipped/sync_and_async_wait/units/tokens/test_condition_token.py @@ -0,0 +1,27 @@ +import asyncio +from time import perf_counter + +import pytest + +from cantok import ConditionToken + + +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_async_wait_condition(): + flag = False + timeout = 0.001 + token = ConditionToken(lambda: flag) + + async def cancel_with_timeout(_token): + nonlocal flag + await asyncio.sleep(timeout) + flag = True + + async def runner(): + return await asyncio.gather(token.wait(), cancel_with_timeout(token)) + + start_time = perf_counter() + asyncio.run(runner()) + finish_time = perf_counter() + + assert finish_time - start_time >= timeout diff --git a/tests/skipped/sync_and_async_wait/units/tokens/test_timeout_token.py b/tests/skipped/sync_and_async_wait/units/tokens/test_timeout_token.py new file mode 100644 index 0000000..36a5c52 --- /dev/null +++ b/tests/skipped/sync_and_async_wait/units/tokens/test_timeout_token.py @@ -0,0 +1,35 @@ +import asyncio +from time import perf_counter + +import pytest + +from cantok import TimeoutToken + + +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_async_wait_timeout(): + sleep_duration = 0.0001 + token = TimeoutToken(sleep_duration) + + start_time = perf_counter() + asyncio.run(token.wait()) + finish_time = perf_counter() + + assert sleep_duration <= finish_time - start_time + + +@pytest.mark.skip(reason='The universal sync/async wait() method is no longer supported because it suppressed exceptions raised during wait-time cancellation checks.') +def test_run_async_multiple_timeouts(): + sleep_duration = 0.001 + number_of_tokens = 100 + + tokens = [TimeoutToken(sleep_duration) for x in range(number_of_tokens)] + + async def runner(): + return await asyncio.gather(*(x.wait() for x in tokens)) + + start_time = perf_counter() + asyncio.run(runner()) + finish_time = perf_counter() + + assert (finish_time - start_time) < (sleep_duration * number_of_tokens) diff --git a/tests/units/tokens/abstract/test_abstract_token.py b/tests/units/tokens/abstract/test_abstract_token.py index 1af628d..cf79339 100644 --- a/tests/units/tokens/abstract/test_abstract_token.py +++ b/tests/units/tokens/abstract/test_abstract_token.py @@ -1,4 +1,3 @@ -import asyncio from functools import partial from threading import Thread from time import perf_counter, sleep @@ -761,80 +760,101 @@ def test_repr_if_nested_token_is_cancelled(token_fabric_1, token_fabric_2, cance 'token_fabric', [*ALL_TOKENS_FABRICS, DefaultToken], ) +def test_wait_wrong_parameters(token_fabric, parameters): + token = token_fabric() + + with pytest.raises(ValueError, match=r'.'): + token.wait(**parameters) + + @pytest.mark.parametrize( - 'do_await', - [ - True, - False, - ], + 'token_fabric', + ALL_TOKENS_FABRICS, ) -def test_wait_wrong_parameters(token_fabric, parameters, do_await): +def test_sync_wait_with_cancel(token_fabric): + timeout = 0.001 token = token_fabric() - if do_await: - with pytest.raises(ValueError, match=r'.'): - asyncio.run(token.wait(**parameters)) - else: - with pytest.raises(ValueError, match=r'.'): - token.wait(**parameters) + def cancel_with_timeout(token): + sleep(timeout) + token.cancel() + + start_time = perf_counter() + thread = Thread(target=cancel_with_timeout, args=(token,)) + thread.start() + token.wait() + thread.join() + finish_time = perf_counter() + + assert finish_time - start_time >= timeout @pytest.mark.parametrize( 'token_fabric', [*ALL_TOKENS_FABRICS, DefaultToken], ) -def test_async_wait_timeout(token_fabric): +def test_wait_timeout_exception_is_raised_synchronously(token_fabric): + """ + `wait(timeout=...)` must raise the timeout exception in the caller's frame. + + The old universal sync/async wrapper ran the synchronous wait from a + finalizer, so `TimeoutCancellationError` could be ignored by Python instead + of being delivered to the caller. This test waits for a token that will not + cancel by itself before the auxiliary timeout and verifies that the exception + is raised directly by `wait()`. + """ timeout = 0.0001 token = token_fabric() - with pytest.raises(TimeoutToken.exception): - asyncio.run(token.wait(timeout=timeout)) + with pytest.raises(TimeoutToken.exception) as exc_info: + token.wait(timeout=timeout) + + assert isinstance(exc_info.value.token, TimeoutToken) + assert exc_info.value.token is not token @pytest.mark.parametrize( 'token_fabric', ALL_TOKENS_FABRICS, ) -def test_async_wait_with_cancel(token_fabric): - timeout = 0.001 - token = token_fabric() +def test_wait_timeout_returns_when_waited_token_cancellation_wins(token_fabric): + """ + `wait(timeout=...)` must return normally when waited-token cancellation wins. - async def cancel_with_timeout(token): - await asyncio.sleep(timeout) - token.cancel() + The timeout token created inside `wait()` is only a maximum waiting limit, + so a cancellation reported by the waited token must make `wait()` complete + without raising `TimeoutCancellationError`. The test embeds a `CounterToken` + into each waited token, then uses the cached report to prove that `wait()` + itself observed the waited-token cancellation without an extra + `CounterToken` poll in the assertion. + """ + nested_token = CounterToken(1, direct=False) + token = token_fabric(nested_token) + result = token.wait(step=0, timeout=1) - async def runner(token): - coroutines = [cancel_with_timeout(token), token.wait() ] - return await asyncio.gather(*coroutines) - - start_time = perf_counter() - asyncio.run(runner(token)) - finish_time = perf_counter() - - assert not token - assert finish_time - start_time >= timeout + assert result is None + assert token._cached_report == CancellationReport( + cause=CancelCause.SUPERPOWER, + from_token=nested_token, + ) @pytest.mark.parametrize( 'token_fabric', ALL_TOKENS_FABRICS, ) -def test_sync_wait_with_cancel(token_fabric): - timeout = 0.001 - token = token_fabric() +def test_wait_without_timeout_returns_none(token_fabric): + """ + Synchronous `wait()` must return `None`. - def cancel_with_timeout(token): - sleep(timeout) - token.cancel() - - start_time = perf_counter() - thread = Thread(target=cancel_with_timeout, args=(token,)) - thread.start() - token.wait() - thread.join() - finish_time = perf_counter() + A pre-cancelled token makes the wait finish immediately, so the test isolates + the public return value from timing concerns and fixes the sync-only API: + callers get completion, not an object to await. + """ + token = token_fabric(cancelled=True) + result = token.wait() - assert finish_time - start_time >= timeout + assert result is None @pytest.mark.parametrize( @@ -976,4 +996,3 @@ def test_just_neste_simple_token_to_another_token(token_fabric): assert len(token._tokens) == 1 assert isinstance(token._tokens[0], SimpleToken) assert token - diff --git a/tests/units/tokens/test_condition_token.py b/tests/units/tokens/test_condition_token.py index 6db177b..45d6b4d 100644 --- a/tests/units/tokens/test_condition_token.py +++ b/tests/units/tokens/test_condition_token.py @@ -1,6 +1,4 @@ -import asyncio from functools import partial -from time import perf_counter import pytest @@ -183,26 +181,6 @@ def test_get_report_cancelled_nested(cancelled, cancelled_nested, from_token_is_ assert report.from_token is token -def test_async_wait_condition(): - flag = False - timeout = 0.001 - token = ConditionToken(lambda: flag) - - async def cancel_with_timeout(_token): - nonlocal flag - await asyncio.sleep(timeout) - flag = True - - async def runner(): - return await asyncio.gather(token.wait(), cancel_with_timeout(token)) - - start_time = perf_counter() - asyncio.run(runner()) - finish_time = perf_counter() - - assert finish_time - start_time >= timeout - - @pytest.mark.parametrize( 'options', [ diff --git a/tests/units/tokens/test_timeout_token.py b/tests/units/tokens/test_timeout_token.py index 018e2f3..ce5c0cb 100644 --- a/tests/units/tokens/test_timeout_token.py +++ b/tests/units/tokens/test_timeout_token.py @@ -1,4 +1,3 @@ -import asyncio from time import perf_counter, sleep import pytest @@ -185,33 +184,6 @@ def test_get_report_cancelled_nested(timeout, timeout_nested, from_token_is_nest assert report.from_token is token -def test_async_wait_timeout(): - sleep_duration = 0.0001 - token = TimeoutToken(sleep_duration) - - start_time = perf_counter() - asyncio.run(token.wait()) - finish_time = perf_counter() - - assert sleep_duration <= finish_time - start_time - - -def test_run_async_multiple_timeouts(): - sleep_duration = 0.001 - number_of_tokens = 100 - - tokens = [TimeoutToken(sleep_duration) for x in range(number_of_tokens)] - - async def runner(): - return await asyncio.gather(*(x.wait() for x in tokens)) - - start_time = perf_counter() - asyncio.run(runner()) - finish_time = perf_counter() - - assert (finish_time - start_time) < (sleep_duration * number_of_tokens) - - def test_timeout_wait(): sleep_duration = 1 token = TimeoutToken(sleep_duration)