diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f97df83d31d68690041d4b29984e28c29feaa8e..2faa51cf65159938f1640df962d35eb75363ce6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ * slight optimization for Heap.push_many * bugfix for Heap.push and a deprecation +* changed how CallableGroup works - now every add adds a CancellableCallback + and it's always true that CallableGroup has it's own cancellable callbacks. # v2.25.4 diff --git a/README.md b/README.md index 43d9aca7cc7769e015bb04f28b084b7748eddadb..d579aedd13ffba1db81daf4df27d06d3a00addea 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ satella ======== -[](https://codeclimate.com/github/piotrmaslanka/satella) -[](https://codeclimate.com/github/piotrmaslanka/satella) [](https://pypi.python.org/pypi/satella) [](https://badge.fury.io/py/satella) [](https://pypi.python.org/pypi/satella) @@ -39,10 +37,10 @@ Satella contains, among other things: Most Satella objects make heavy use of `__slots__`, so they are memory friendly and usable on embedded systems, where memory is at premium. -Change log is kept as part of [release notes](https://github.com/piotrmaslanka/satella/releases). -The [CHANGELOG.md](CHANGELOG.md) file is only to track changes since last release. +Change log is kept as part of [old release notes](https://github.com/piotrmaslanka/satella/releases), +new [CHANGELOG.md](CHANGELOG.md) is to remain for all posteriority. -Full [documentation](http://satella.readthedocs.io/en/latest/?badge=latest) +Full [documentation](http://smokserwis.docs.smok.co/satella) is available for the brave souls that do decide to use this library. See [LICENSE](LICENSE) for text of the license. This library may contain code taken from elsewhere on the internets, so @@ -50,20 +48,21 @@ this is copyright (c) respective authors. If you want to install extra modules, just run +Installation +------------ + ```bash -pip install satella[extras] +pip install --extra-index-url https://git.dms-serwis.com.pl/api/v4/groups/330/-/packages/pypi/simple satella ``` Running unit tests ------------------ -Tests run by default on GitHub Actions. +Tests run by default on local CI/CD. They should pass on Windows too, but some tests requiring POSIX-like functionality are skipped. Automatic release system ------------------------ -Releases happen automatically. Just add a tag with the name of the version. - -**NOTE that changes from 2.25 will be numbered as tags without the prefix ```v```**! \ No newline at end of file +Releases happen to our local PyPI. Just add a tag for it to work. diff --git a/satella/__init__.py b/satella/__init__.py index 55bf16c7835909cac26f5c1e0f91d480c37df0ae..abc7a08e1bdf66365b73d9c933b780c63a98acac 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.25.5' +__version__ = '2.25.6a1' diff --git a/satella/coding/concurrent/callablegroup.py b/satella/coding/concurrent/callablegroup.py index 4f468223296dd13362c50f82417a12e639261565..87053433a68b6e62e6f68208813a96743d5c999b 100644 --- a/satella/coding/concurrent/callablegroup.py +++ b/satella/coding/concurrent/callablegroup.py @@ -4,10 +4,11 @@ import time import typing as tp from satella.coding.deleters import DictDeleter +from satella.coding.structures.mixins import HashableMixin from satella.coding.typing import T, NoArgCallable -class CancellableCallback: +class CancellableCallback(HashableMixin): """ A callback that you can cancel. @@ -17,7 +18,8 @@ class CancellableCallback: If called, the function itself won't be called as well if this was cancelled. In this case a None will be returned instead of the result of callback_fun() - This short circuits __bool__ to return not .cancelled. + This short circuits __bool__ to return not .cancelled. So, the bool value of this callback depends on whether it + has been cancelled or not. Hashable and __eq__-able by identity. @@ -25,14 +27,11 @@ class CancellableCallback: :ivar cancelled: whether this callback was cancelled (bool) """ - __slots__ = ('cancelled', 'callback_fun') + __slots__ = 'cancelled', 'callback_fun' def __bool__(self) -> bool: return not self.cancelled - def __hash__(self) -> int: - return hash(id(self)) - def __init__(self, callback_fun: tp.Callable): self.callback_fun = callback_fun self.cancelled = False @@ -46,6 +45,35 @@ class CancellableCallback: Cancel this callback. """ self.cancelled = True +# +# class CancellableCallbackGroup: +# """ +# +# A group of callbacks that you can simultaneously cancel. +# +# Immutable. Also, hashable and __eq__able. +# +# Regarding it's truth value - it's True if at least one callback has not been cancelled. +# """ +# +# def __init__(self, callbacks: tp.Iterable[CancellableCallback]): +# self.callbacks = list(callbacks) # type: tp.List[CancellableCallback] +# +# def cancel(self) -> None: +# """ +# Cancel all of the callbacks. +# """ +# for callback in self.callbacks: +# callback.cancel() +# +# def __bool__(self) -> bool: +# return any(not callback.cancelled for callback in self.callbacks) +# +# def __hash__(self): +# y = 0 +# for callback in self.callbacks: +# y ^= hash(callback) +# return y class CallableGroup(tp.Generic[T]): @@ -64,21 +92,13 @@ class CallableGroup(tp.Generic[T]): will be propagated. """ - __slots__ = ('callables', 'gather', 'swallow_exceptions', - '_has_cancellable_callbacks') + __slots__ = 'callables', 'gather', 'swallow_exceptions', def __init__(self, gather: bool = True, swallow_exceptions: bool = False): - """ - :param gather: if True, results from all callables will be gathered - into a list and returned from __call__ - :param swallow_exceptions: if True, exceptions from callables will be - silently ignored. If gather is set, - result will be the exception instance - """ - self.callables = collections.OrderedDict() # type: tp.Dict[tp.Callable, bool] + + self.callables = collections.OrderedDict() # type: tp.Dict[tp.Callable, tuple[bool, int]] self.gather = gather # type: bool self.swallow_exceptions = swallow_exceptions # type: bool - self._has_cancellable_callbacks = False @property def has_cancelled_callbacks(self) -> bool: @@ -87,10 +107,8 @@ class CallableGroup(tp.Generic[T]): :class:`~satella.coding.concurrent.CancellableCallback` instances and whether any of them was cancelled """ - if not self._has_cancellable_callbacks: - return False for clb in self.callables: - if isinstance(clb, CancellableCallback) and not clb: + if clb: return True return False @@ -98,16 +116,13 @@ class CallableGroup(tp.Generic[T]): """ Remove it's entries that are CancelledCallbacks and that were cancelled """ - if not self.has_cancelled_callbacks: - return - with DictDeleter(self.callables) as dd: for callable_ in dd: if isinstance(callable_, CancellableCallback) and not callable_: dd.delete() def add(self, callable_: tp.Union[CancellableCallback, NoArgCallable[T]], - one_shot: bool = False): + one_shot: bool = False) -> CancellableCallback: """ Add a callable. @@ -117,16 +132,21 @@ class CallableGroup(tp.Generic[T]): method :meth:`~satella.coding.concurrent.CallableGroup.remove_cancelled` might be useful. + Basically every callback is cancellable. + :param callable_: callable :param one_shot: if True, callable will be unregistered after single call + :returns: callable_ if it was a cancellable callback, else one constructed after it + + .. deprecated:: v2.25.5 + Do not pass a CancellableCallback, you'll get your own """ - if isinstance(callable_, CancellableCallback): - self._has_cancellable_callbacks = True - from ..structures.hashable_objects import HashableWrapper - callable_ = HashableWrapper(callable_) + if not isinstance(callable_, CancellableCallback): + callable_ = CancellableCallback(callable_) if callable_ in self.callables: - return + return callable_ self.callables[callable_] = one_shot + return callable_ def __call__(self, *args, **kwargs) -> tp.Optional[tp.List[T]]: """ diff --git a/satella/time/backoff.py b/satella/time/backoff.py index 06587cac52a96d2d890d1f0561c690b92339935f..74c3a419d2d72c4500d54233f000b3c10fbb2a42 100644 --- a/satella/time/backoff.py +++ b/satella/time/backoff.py @@ -1,7 +1,6 @@ import time import typing as tp -from satella.coding.concurrent.thread import Condition from satella.coding.decorators.decorators import wraps from satella.coding.typing import ExceptionList from satella.exceptions import WouldWaitMore @@ -43,6 +42,7 @@ class ExponentialBackoff: def __init__(self, start: float = 1, limit: float = 30, sleep_fun: tp.Callable[[float], None] = time.sleep, grace_amount: int = 0): + from satella.coding.concurrent.thread import Condition self.start = start self.grace_amount = grace_amount self.grace_counter = 0 diff --git a/tests/test_coding/test_concurrent.py b/tests/test_coding/test_concurrent.py index 51f9bd29675d68c47d2ec74b27176502da0950f2..7dce062743df4fe7cfd83f7ddbd832333ac140f5 100644 --- a/tests/test_coding/test_concurrent.py +++ b/tests/test_coding/test_concurrent.py @@ -218,6 +218,19 @@ class TestConcurrent(unittest.TestCase): 9: True, 10: True, 11: True}) + def test_auto_cancellable_callback(self): + a = {'test': True} + + def y(): + nonlocal a + a['test'] = False + + cg = CallableGroup() + can = cg.add(y) + can.cancel() + cg() + self.assertTrue(a['test']) + def test_cancellable_callback(self): a = {'test': True}