Skip to content

Commit

Permalink
Merge pull request #16 from pomponchik/develop
Browse files Browse the repository at this point in the history
0.0.11
  • Loading branch information
pomponchik authored Dec 19, 2023
2 parents 121cb5e + 0d5bd62 commit 53ccb1c
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 24 deletions.
3 changes: 3 additions & 0 deletions cantok/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ class CounterCancellationError(CancellationError):

class TimeoutCancellationError(CancellationError):
pass

class SynchronousWaitingError(Exception):
pass
44 changes: 38 additions & 6 deletions cantok/tokens/abstract_token.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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, Optional, Union, Any
from typing import List, Dict, Awaitable, Optional, Union, Any
from collections.abc import Coroutine

from cantok.errors import CancellationError

from cantok.errors import CancellationError, SynchronousWaitingError


class CancelCause(Enum):
Expand All @@ -18,6 +21,20 @@ class CancellationReport:
cause: CancelCause
from_token: 'AbstractToken'

class AngryAwaitable(Coroutine): # type: ignore[type-arg]
def __await__(self): # type: ignore[no-untyped-def]
raise SynchronousWaitingError('You cannot use the "await" keyword in the synchronous mode of the method. Add the "is_async" (bool) argument.')
yield self

def send(self, value: Any) -> None:
raise SynchronousWaitingError('You cannot use the "await" keyword in the synchronous mode of the method. Add the "is_async" (bool) argument.')

def throw(self, value: Any) -> Any: # type: ignore[override]
pass

def close(self) -> Any:
pass


class AbstractToken(ABC):
exception = CancellationError
Expand Down Expand Up @@ -85,7 +102,7 @@ def keep_on(self) -> bool:
def is_cancelled(self, direct: bool = True) -> bool:
return self.get_report(direct=direct).cause != CancelCause.NOT_CANCELLED

async def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, float]] = None) -> None:
def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[int, float]] = None, is_async: bool = False) -> Awaitable: # type: ignore[type-arg]
if step < 0:
raise ValueError('The token polling iteration time cannot be less than zero.')
if timeout is not None and timeout < 0:
Expand All @@ -102,10 +119,25 @@ async def wait(self, step: Union[int, float] = 0.0001, timeout: Optional[Union[i

token = self + local_token

while token:
await async_sleep(step)
async def async_wait() -> Awaitable: # type: ignore[return, type-arg]
while token:
await async_sleep(step)

local_token.check()

def sync_wait() -> None:
while token:
sync_sleep(step)

local_token.check()

if is_async:
return async_wait()

else:
sync_wait()
return AngryAwaitable()

local_token.check()

def get_report(self, direct: bool = True) -> CancellationReport:
if self._cancelled:
Expand Down
24 changes: 21 additions & 3 deletions docs/docs/what_are_tokens/waiting.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
Each token has an async method, `wait()`, which transfers control until the token is canceled. It can be passed to one or more asynchronous functions, then wait until they cancel the operation, and then continue further work, something like this:
Each token has `wait()` method, which allows you to wait for its cancellation.

```python
from cantok import TimeoutToken

token = TimeoutToken(5)
token.wait() # It will take about 5 seconds.
token.check() # Since the timeout has expired, an exception will be raised.
# cantok.errors.TimeoutCancellationError: The timeout of 5 seconds has expired.
```

This method also has an async version. To make the wait asynchronous, simply add the `is_async=True` parameter. After that, you can use this method in expressions using the `await` statement:

```python
import asyncio
Expand All @@ -11,13 +22,20 @@ async def do_something(token):
async def main():
token = SimpleToken()
await do_something(token)
await token.wait()
await token.wait(is_async=True)
print('Something has been done!')

asyncio.run(main())
```

The `wait()` method has two optional arguments:
If you mistakenly use the `await` statement in the synchronous mode of the method, you will receive a `cantok.errors.SynchronousWaitingError`:

```python
await token.wait()
# cantok.errors.SynchronousWaitingError: You cannot use the "await" keyword in the synchronous mode of the method. Add the "is_async" (bool) argument.
```

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/waiting/) will be raised. By default, the `timeout` is not set.
- **`step`** (`int` or `float`, by default `0.0001`) - the duration of the iteration, once in which the token state is polled, in seconds. For obvious reasons, you cannot set this value to a number that exceeds the `timeout`.
2 changes: 1 addition & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ nav:
- Cancel and read the status: what_are_tokens/cancel_and_read_the_status.md
- Exceptions: what_are_tokens/exceptions.md
- Summation: what_are_tokens/summation.md
- Asynchronous waiting for cancellation: what_are_tokens/waiting.md
- Waiting for cancellation: what_are_tokens/waiting.md
- Types of tokens:
- SimpleToken: types_of_tokens/SimpleToken.md
- ConditionToken: types_of_tokens/ConditionToken.md
Expand Down
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "cantok"
version = "0.0.10"
version = "0.0.11"
authors = [
{ name="Evgeniy Blinov", email="zheni-b@yandex.ru" },
]
Expand All @@ -29,9 +29,6 @@ classifiers = [
'Topic :: Software Development :: Libraries',
]

[tool.setuptools.packages.find]
where = ["cantok"]

[project.urls]
'Source' = 'https://github.com/pomponchik/cantok'
'Documentation' = 'http://cantok.readthedocs.io/'
Expand Down
91 changes: 83 additions & 8 deletions tests/units/tokens/test_abstract_token.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import asyncio
from functools import partial
from time import perf_counter
from time import perf_counter, sleep
from threading import Thread
from queue import Queue

import pytest

from cantok.tokens.abstract_token import AbstractToken, CancelCause, CancellationReport
from cantok.tokens.abstract_token import AbstractToken, CancelCause, CancellationReport, AngryAwaitable
from cantok import SimpleToken, ConditionToken, TimeoutToken, CounterToken, CancellationError
from cantok.errors import SynchronousWaitingError


ALL_TOKEN_CLASSES = [SimpleToken, ConditionToken, TimeoutToken, CounterToken]
Expand Down Expand Up @@ -324,36 +327,72 @@ def test_repr_if_nested_token_is_cancelled(token_fabric_1, token_fabric_2, cance
{'step': 1, 'timeout': -1},
{'step': -1, 'timeout': 1},
{'step': 2, 'timeout': 1},
{'step': -1, 'is_async': True},
{'step': -1, 'timeout': -1, 'is_async': True},
{'step': -1, 'timeout': 0, 'is_async': True},
{'timeout': -1, 'is_async': True},
{'step': 1, 'timeout': -1, 'is_async': True},
{'step': -1, 'timeout': 1, 'is_async': True},
{'step': 2, 'timeout': 1, 'is_async': True},
{'step': -1, 'is_async': False},
{'step': -1, 'timeout': -1, 'is_async': False},
{'step': -1, 'timeout': 0, 'is_async': False},
{'timeout': -1, 'is_async': False},
{'step': 1, 'timeout': -1, 'is_async': False},
{'step': -1, 'timeout': 1, 'is_async': False},
{'step': 2, 'timeout': 1, 'is_async': False},
],
)
@pytest.mark.parametrize(
'token_fabric',
ALL_TOKENS_FABRICS,
)
def test_wait_wrong_parameters(token_fabric, parameters):
@pytest.mark.parametrize(
'do_await',
[
True,
False,
],
)
def test_wait_wrong_parameters(token_fabric, parameters, do_await):
token = token_fabric()

with pytest.raises(ValueError):
asyncio.run(token.wait(**parameters))
if do_await:
asyncio.run(token.wait(**parameters))
else:
token.wait(**parameters)


@pytest.mark.parametrize(
'token_fabric',
ALL_TOKENS_FABRICS,
)
def test_async_wait_timeout(token_fabric):
timeout = 0.0001
token = token_fabric()

with pytest.raises(TimeoutToken.exception):
asyncio.run(token.wait(timeout=timeout, is_async=True))


@pytest.mark.parametrize(
'token_fabric',
ALL_TOKENS_FABRICS,
)
def test_wait_timeout(token_fabric):
def test_sync_wait_timeout(token_fabric):
timeout = 0.0001
token = token_fabric()

with pytest.raises(TimeoutToken.exception):
asyncio.run(token.wait(timeout=timeout))
token.wait(timeout=timeout)


@pytest.mark.parametrize(
'token_fabric',
ALL_TOKENS_FABRICS,
)
def test_wait_with_cancel(token_fabric):
def test_async_wait_with_cancel(token_fabric):
timeout = 0.001
token = token_fabric()

Expand All @@ -362,10 +401,46 @@ async def cancel_with_timeout(token):
token.cancel()

async def runner():
return await asyncio.gather(token.wait(), cancel_with_timeout(token))
return await asyncio.gather(token.wait(is_async=True), cancel_with_timeout(token))

start_time = perf_counter()
asyncio.run(runner())
finish_time = perf_counter()

assert finish_time - start_time >= timeout


@pytest.mark.parametrize(
'token_fabric',
ALL_TOKENS_FABRICS,
)
def test_sync_wait_with_cancel(token_fabric):
timeout = 0.001
token = token_fabric()

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()
finish_time = perf_counter()

assert finish_time - start_time >= timeout


def test_pseudo_awaitable():
with pytest.raises(SynchronousWaitingError):
asyncio.run(AngryAwaitable())


@pytest.mark.parametrize(
'token_fabric',
ALL_TOKENS_FABRICS,
)
def test_sync_run_returns_angry_awaitable(token_fabric):
token = token_fabric(cancelled=True)

assert isinstance(token.wait(timeout=0.001), AngryAwaitable)
2 changes: 1 addition & 1 deletion tests/units/tokens/test_condition_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ async def cancel_with_timeout(token):
flag = True

async def runner():
return await asyncio.gather(token.wait(), cancel_with_timeout(token))
return await asyncio.gather(token.wait(is_async=True), cancel_with_timeout(token))

start_time = perf_counter()
asyncio.run(runner())
Expand Down
2 changes: 1 addition & 1 deletion tests/units/tokens/test_timeout_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def test_async_wait_timeout():
token = TimeoutToken(sleep_duration)

start_time = perf_counter()
asyncio.run(token.wait())
asyncio.run(token.wait(is_async=True))
finish_time = perf_counter()

assert sleep_duration <= finish_time - start_time

0 comments on commit 53ccb1c

Please sign in to comment.