diff --git a/.gitignore b/.gitignore index b7522274b68c279154e6049d7fef8a887b10c1f2..c542b97aab1d6d40f84bdef9d0144c9ddf9493cb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,14 @@ test*.json test*.txt .pytest_cache +test-* .eggs satella.sublime* hs_err_pid*.log *.pydevproject .coverage.* .project +tests/test_coding/__pycache__ venv coverage.xml .coverage.* diff --git a/satella/coding/concurrent/callablegroup.py b/satella/coding/concurrent/callablegroup.py index ac79846da369925556c36c7575a6a9bf531d7116..175aba0489e256ac72f6a971e49d7fbba236dfd1 100644 --- a/satella/coding/concurrent/callablegroup.py +++ b/satella/coding/concurrent/callablegroup.py @@ -6,6 +6,7 @@ import typing as tp from satella.coding.deleters import DictDeleter, IterMode from satella.coding.typing import T, NoArgCallable +import inspect class CancellableCallback: @@ -24,6 +25,7 @@ class CancellableCallback: Hashable and __eq__-able by identity. Equal only to itself. :param callback_fun: function to call + :param hash: reserved for internal use, don't use :ivar cancelled: whether this callback was cancelled (bool) :ivar one_shotted: whether this callback was invoked (bool) @@ -39,7 +41,7 @@ class CancellableCallback: self.one_shotted = False def __hash__(self): - return hash(id(self)) + return hash(id(self)) ^ self.__hash def __eq__(self, other: CancellableCallback) -> bool: return id(self) == id(other) and self.cancelled == other.cancelled @@ -55,11 +57,12 @@ class CancellableCallback: self.cancelled = True -def _callable_to_cancellablecallback(callback: NoArgCallable[[T], None]) -> CancellableCallback: - if isinstance(callback, NoArgCallable[[T], None]): - return CancellableCallback(callback) - elif isinstance(callback, CancellableCallback): +def _callable_to_cancellablecallback(callback: NoArgCallable) -> CancellableCallback: + if isinstance(callback, CancellableCallback): return callback + if callable(callback): + return CancellableCallback(callback) + raise ValueError('Invalid callable') class CancellableCallbackGroup: @@ -111,7 +114,7 @@ class CallableGroup(tp.Generic[T]): __slots__ = 'callables', 'gather', 'swallow_exceptions', def __init__(self, gather: bool = True, swallow_exceptions: bool = False): - self.callables = collections.OrderedDict() # type: tp.Dict[tp.Callable, tuple[bool]] + self.callables = collections.OrderedDict() # type: tp.Dict[CancellableCallback, bool] self.gather = gather # type: bool self.swallow_exceptions = swallow_exceptions # type: bool @@ -131,23 +134,22 @@ class CallableGroup(tp.Generic[T]): """ Remove it's entries that are CancelledCallbacks and that were cancelled and move one shots """ - with DictDeleter(self.callables, iter_mode=IterMode.ITER_VALUES) as dd: + with DictDeleter(self.callables, iter_mode=IterMode.ITER_ITEMS) as dd: for callable_, oneshot in dd: - if isinstance(callable_, CancellableCallback) and not callable_: + if isinstance(callable_, CancellableCallback) and not callable_.cancelled: dd.delete() if oneshot: dd.delete() - def add_many(self, callable_: tp.Sequence[tp.Union[NoArgCallable[T], - tp.Tuple[NoArgCallable[T], bool]]]) -> CancellableCallbackGroup: + def add_many(self, callable_: tp.Sequence[tp.Union[NoArgCallable, + tp.Tuple[NoArgCallable, bool]]]) -> CancellableCallbackGroup: """ Add multiple callbacks - .. note:: Same callable can't be added twice. It will silently fail. - Note that already called one-shots can be added twice - Basically every callback is cancellable. + .. warning: Same callable cannot be added twice. A RuntimeError will be raised in that case. + :param callable_: sequence of either callables with will be registered as multiple-shots or a tuple of callback (with an argument to register it as a one-shot) :returns: CancellableCallbackGroup to cancel all of the callbacks @@ -169,17 +171,17 @@ class CallableGroup(tp.Generic[T]): """ Add a callable. - .. note:: Same callable can't be added twice. It will silently fail, and return an existing callbacks. - Note that already called one-shots can be added twice - Can be a :class:`~satella.coding.concurrent.CancellableCallback`, in that case method :meth:`~satella.coding.concurrent.CallableGroup.remove_cancelled` might be useful. + .. warning: Same callable cannot be added twice. A RuntimeError will be raised in that case. + Basically every callback is cancellable. :param callable_: callable :param one_shot: if True, callable will be unregistered after single call + :param hash: internal, don't use :returns: callable_ if it was a cancellable callback, else one constructed after it .. deprecated:: v2.25.5 @@ -189,7 +191,7 @@ class CallableGroup(tp.Generic[T]): callable_ = _callable_to_cancellablecallback(callable_) self.callables[callable_] = one_shot if callable_ in self.callables: - return callable_ + raise RuntimeError('Same callable added twice') return callable_ def __call__(self, *args, **kwargs) -> tp.Optional[tp.List[T]]: @@ -231,6 +233,10 @@ class CallableGroup(tp.Generic[T]): if self.gather: return results + def __len__(self): + self.remove_cancelled() + return len(self.callables) + class CallNoOftenThan: """ diff --git a/satella/coding/concurrent/functions.py b/satella/coding/concurrent/functions.py index cb72b14ace5a80167190baac44773cc324425cf6..99014b038d24ccf8da822d06b4ae39f5f31db807 100644 --- a/satella/coding/concurrent/functions.py +++ b/satella/coding/concurrent/functions.py @@ -2,7 +2,7 @@ import typing as tp from concurrent.futures import Future from threading import Thread -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.sequences.sequences import infinite_iterator from satella.coding.typing import T diff --git a/satella/coding/concurrent/monitor.py b/satella/coding/concurrent/monitor.py index d9d2bee1b424edf41085270992ea8bd85b605615..394c8a891b735274ea55b943b6d149ba62846749 100644 --- a/satella/coding/concurrent/monitor.py +++ b/satella/coding/concurrent/monitor.py @@ -3,7 +3,7 @@ import copy import threading import typing as tp -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.typing import K, V, T diff --git a/satella/coding/concurrent/queue.py b/satella/coding/concurrent/queue.py index 264e1f4401e9b43db08eb3d6c118e82e95a2ace1..2db251ccf66134d80128b47fff08d1539c7d7803 100644 --- a/satella/coding/concurrent/queue.py +++ b/satella/coding/concurrent/queue.py @@ -55,6 +55,7 @@ class PeekableQueue(tp.Generic[T]): try: return item_getter(self.queue) finally: + self.items_count -= 1 self.lock.release() def __get_timeout(self, item_getter, timeout): @@ -68,6 +69,7 @@ class PeekableQueue(tp.Generic[T]): try: return item_getter(self.queue) finally: + self.items_count -= 1 self.lock.release() else: self.lock.release() @@ -75,21 +77,19 @@ class PeekableQueue(tp.Generic[T]): @rethrow_as(WouldWaitMore, Empty) def __get(self, timeout, item_getter) -> T: - try: - self.lock.acquire() - if len(self.queue): - # Fast path - try: - return item_getter(self.queue) - finally: - self.lock.release() + self.lock.acquire() + if len(self.queue): + # Fast path + try: + return item_getter(self.queue) + finally: + self.items_count -= 1 + self.lock.release() + else: + if timeout is None: + return self.__get_timeout_none(item_getter) else: - if timeout is None: - return self.__get_timeout_none(item_getter) - else: - return self.__get_timeout(item_getter, timeout) - finally: - self.items_count -= 1 + return self.__get_timeout(item_getter, timeout) def get(self, timeout: tp.Optional[float] = None) -> T: """ @@ -100,10 +100,7 @@ class PeekableQueue(tp.Generic[T]): :return: the item :raise Empty: queue was empty """ - try: - return self.__get(timeout, lambda queue: queue.popleft()) - finally: - self.items_count -= 1 + return self.__get(timeout, lambda queue: queue.popleft()) def peek(self, timeout: tp.Optional[float] = None) -> T: """ @@ -125,4 +122,4 @@ class PeekableQueue(tp.Generic[T]): return self.items_count def __len__(self): - return self.items_count \ No newline at end of file + return self.items_count diff --git a/satella/coding/decorators/__init__.py b/satella/coding/decorators/__init__.py index 4589d29cd20f6e0ce2e34e2e5ae19b2d888c32a2..4ad8d19f0ae74449cc762d2068ca0a175fb08324 100644 --- a/satella/coding/decorators/__init__.py +++ b/satella/coding/decorators/__init__.py @@ -2,11 +2,12 @@ from .arguments import auto_adapt_to_methods, attach_arguments, for_argument, \ execute_before, copy_arguments, replace_argument_if, transform_result, \ transform_arguments, execute_if_attribute_none, execute_if_attribute_not_none, \ cached_property -from .decorators import wraps, chain_functions, has_keys, short_none, memoize, return_as_list, \ +from .decorators import chain_functions, has_keys, short_none, memoize, return_as_list, \ default_return, cache_memoize, call_method_on_exception from .flow_control import loop_while, queue_get, repeat_forever from .preconditions import postcondition, precondition from .retry_dec import retry +from .wraps import wraps __all__ = ['retry', 'transform_result', 'transform_arguments', 'repeat_forever', 'execute_before', 'postcondition', 'precondition', 'wraps', 'queue_get', diff --git a/satella/coding/decorators/arguments.py b/satella/coding/decorators/arguments.py index 32fed98e48ebf2b0029347e2faa761d389b2da63..99e9058325ca6b3554e1c11bceaf8a09f2fb9ed0 100644 --- a/satella/coding/decorators/arguments.py +++ b/satella/coding/decorators/arguments.py @@ -4,7 +4,7 @@ import itertools import typing as tp from inspect import Parameter -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.misc import source_to_function, get_arguments, call_with_arguments, _get_arguments from satella.coding.predicates import PredicateClass, build_structure from satella.coding.typing import T, Predicate diff --git a/satella/coding/decorators/decorators.py b/satella/coding/decorators/decorators.py index a896e9ab296fdd3764125fd341eb47703edec262..523c8e3c463543fe9134549f8e4b09359570e531 100644 --- a/satella/coding/decorators/decorators.py +++ b/satella/coding/decorators/decorators.py @@ -1,8 +1,9 @@ -import inspect import time import typing as tp import warnings +from satella.coding.decorators.wraps import wraps + from satella.coding.typing import T, U from satella.exceptions import PreconditionError @@ -221,52 +222,6 @@ def memoize(fun): return inner -def wraps(cls_to_wrap: tp.Type) -> tp.Callable[[tp.Type], tp.Type]: - """ - A functools.wraps() but for classes. - - As a matter of fact, this can replace functools.wraps() entirely. - This replaces __doc__, __name__, __module__ and __annotations__. - It also sets a correct __wrapped__. - - :param cls_to_wrap: class to wrap - """ - - def outer(cls: tp.Type) -> tp.Type: - if hasattr(cls_to_wrap, '__doc__'): - try: - cls.__doc__ = cls_to_wrap.__doc__ - except AttributeError: - pass - if hasattr(cls_to_wrap, '__name__'): - try: - cls.__name__ = cls_to_wrap.__name__ - except (AttributeError, TypeError): - pass - if hasattr(cls_to_wrap, '__module__'): - try: - cls.__module__ = cls_to_wrap.__module__ - except AttributeError: - pass - if hasattr(cls_to_wrap, '__annotations__'): - try: - cls.__annotations__ = cls_to_wrap.__annotations__ - except (AttributeError, TypeError): - pass - try: - sig = inspect.signature(cls_to_wrap) - cls.__signature__ = sig - except (TypeError, ValueError, RecursionError, AttributeError): - pass - try: - cls.__wrapped__ = cls_to_wrap - except AttributeError: - pass - return cls - - return outer - - def has_keys(keys: tp.List[str]): """ A decorator for asserting that a dictionary has given keys. Will raise PreconditionError if diff --git a/satella/coding/decorators/flow_control.py b/satella/coding/decorators/flow_control.py index 5ad83c6e6ea9f8bb34b41ef9e028d645f51d6b99..9aff4f7179acede18b0335ee796e1b6520830926 100644 --- a/satella/coding/decorators/flow_control.py +++ b/satella/coding/decorators/flow_control.py @@ -1,7 +1,7 @@ import queue import typing as tp -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.typing import ExceptionClassType, NoArgCallable, Predicate Queue = tp.TypeVar('Queue') diff --git a/satella/coding/decorators/preconditions.py b/satella/coding/decorators/preconditions.py index f27d4315d54bee6755186278c142a02659d6dd97..6f38c02914a8e08f49713a07f71d5c34a1399ca9 100644 --- a/satella/coding/decorators/preconditions.py +++ b/satella/coding/decorators/preconditions.py @@ -4,7 +4,7 @@ import typing as tp from satella.coding.typing import T, Predicate from satella.exceptions import PreconditionError from .arguments import for_argument -from .decorators import wraps +from .wraps import wraps from ..misc import source_to_function Expression = tp.NewType('Expression', str) diff --git a/satella/coding/decorators/retry_dec.py b/satella/coding/decorators/retry_dec.py index 5a7de6874d3bfc574f44d8646da767ae916e4498..70a755f3bd6b525b85b2cbbcb53ac6e9288eaac1 100644 --- a/satella/coding/decorators/retry_dec.py +++ b/satella/coding/decorators/retry_dec.py @@ -2,7 +2,7 @@ import itertools import typing as tp import warnings -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.typing import ExceptionClassType diff --git a/satella/coding/decorators/wraps.py b/satella/coding/decorators/wraps.py new file mode 100644 index 0000000000000000000000000000000000000000..aa2105515c63e969c8a0550ada04ef02a1d84960 --- /dev/null +++ b/satella/coding/decorators/wraps.py @@ -0,0 +1,47 @@ +import inspect +import typing as tp + +def wraps(cls_to_wrap: tp.Type) -> tp.Callable[[tp.Type], tp.Type]: + """ + A functools.wraps() but for classes. + + As a matter of fact, this can replace functools.wraps() entirely. + This replaces __doc__, __name__, __module__ and __annotations__. + It also sets a correct __wrapped__. + + :param cls_to_wrap: class to wrap + """ + + def outer(cls: tp.Type) -> tp.Type: + if hasattr(cls_to_wrap, '__doc__'): + try: + cls.__doc__ = cls_to_wrap.__doc__ + except AttributeError: + pass + if hasattr(cls_to_wrap, '__name__'): + try: + cls.__name__ = cls_to_wrap.__name__ + except (AttributeError, TypeError): + pass + if hasattr(cls_to_wrap, '__module__'): + try: + cls.__module__ = cls_to_wrap.__module__ + except AttributeError: + pass + if hasattr(cls_to_wrap, '__annotations__'): + try: + cls.__annotations__ = cls_to_wrap.__annotations__ + except (AttributeError, TypeError): + pass + try: + sig = inspect.signature(cls_to_wrap) + cls.__signature__ = sig + except (TypeError, ValueError, RecursionError, AttributeError): + pass + try: + cls.__wrapped__ = cls_to_wrap + except AttributeError: + pass + return cls + + return outer diff --git a/satella/coding/deleters.py b/satella/coding/deleters.py index a5a27af3ffd25f88ebcf8d374bf7fd2bbac8d852..55363067024659f3952942ab8d055b8822c97b3c 100644 --- a/satella/coding/deleters.py +++ b/satella/coding/deleters.py @@ -39,7 +39,7 @@ class DictDeleter: __slots__ = ('dict_to_process', 'current_iterator', 'keys_to_delete', 'iter_mode', 'current_key') - def __init__(self, dict_to_process: collections.abc.MutableMapping, iter_mode: IterMode.ITER_KEYS): + def __init__(self, dict_to_process: collections.abc.MutableMapping, iter_mode: IterMode = IterMode.ITER_KEYS): self.dict_to_process = dict_to_process self.iter_mode = iter_mode diff --git a/satella/coding/metaclasses.py b/satella/coding/metaclasses.py index 9e35cf5b05fc1a9d1e90573152cdb7ee1b3eaf9e..86981b5ed4a523d646390d4ad2ebe34c4eb8b36b 100644 --- a/satella/coding/metaclasses.py +++ b/satella/coding/metaclasses.py @@ -1,6 +1,6 @@ import inspect -from .decorators import wraps +from .decorators.wraps import wraps from .sequences.iterators import walk from .typing import Predicate diff --git a/satella/coding/predicates.py b/satella/coding/predicates.py index 46b238769384b3e5a667442c78a145a286877de7..c0d440e8d634de42b7aa9f20bb1d9f673afe37f7 100644 --- a/satella/coding/predicates.py +++ b/satella/coding/predicates.py @@ -2,7 +2,7 @@ import operator import typing as tp from satella.coding.typing import Predicate -from satella.configuration.schema import Descriptor +from satella.configuration.schema.base import Descriptor __all__ = ['x', 'build_structure', 'PredicateClass'] diff --git a/satella/coding/recast_exceptions.py b/satella/coding/recast_exceptions.py index 738ced9fe63ea26df01470d9b3c73e9fb082278a..734201aa59163008dd79b27085571d038c5396ae 100644 --- a/satella/coding/recast_exceptions.py +++ b/satella/coding/recast_exceptions.py @@ -3,7 +3,7 @@ import logging import threading import typing as tp -from .decorators.decorators import wraps +from .decorators.wraps import wraps from .typing import ExceptionClassType, T, NoArgCallable, ExceptionList diff --git a/satella/coding/sequences/sequences.py b/satella/coding/sequences/sequences.py index 99be8c89f9e94de804f381514ddb83c92cb32c86..37bde4cd4530f8bdb464dceff7d26f8ee6ea7800 100644 --- a/satella/coding/sequences/sequences.py +++ b/satella/coding/sequences/sequences.py @@ -1 +1 @@ -import copy import typing as tp from satella.coding.decorators.decorators import wraps from satella.coding.recast_exceptions import rethrow_as from .iterators import n_th from ..typing import T, Iteratable, NoArgCallable, Predicate def infinite_iterator(returns: tp.Optional[T] = None, return_factory: tp.Optional[NoArgCallable[T]] = None) -> tp.Iterator[T]: """ Return an infinite number of objects. :param returns: object to return. Note that this will be this very object, it will not be copied. :param return_factory: a callable that takes 0 args and returns an element to return. :return: an infinite iterator of provided values """ while True: if returns is None: yield None if return_factory is None else return_factory() else: yield returns def make_list(element: T, n: int, deep_copy: bool = False) -> tp.List[T]: """ Make a list consisting of n times element. Element will be copied via copy.copy before adding to list. :param element: element :param n: times to repeat the element :param deep_copy: whether to use copy.deepcopy instead of copy.copy :return: list of length n """ output = [] if deep_copy: copy_op = copy.deepcopy else: copy_op = copy.copy for _ in range(n): output.append(copy_op(element)) return output # shamelessly copied from # https://medium.com/better-programming/is-this-the-last-element-of-my-python-for-loop-784f5ff90bb5 def is_last(lst: Iteratable) -> tp.Iterator[tp.Tuple[bool, T]]: """ Return every element of the list, alongside a flag telling is this the last element. Use like: >>> for is_last, element in is_last(my_list): >>> if is_last: >>> ... :param lst: list to iterate thru :return: a p_gen returning (bool, T) Note that this returns a nice, O(1) iterator. """ iterable = iter(lst) ret_var = next(iterable) for val in iterable: yield False, ret_var ret_var = val yield True, ret_var def add_next(lst: Iteratable, wrap_over: bool = False, skip_last: bool = False) -> tp.Iterator[tp.Tuple[T, tp.Optional[T]]]: """ Yields a 2-tuple of given iterable, presenting the next element as second element of the tuple. The last element will be the last element alongside with a None, if wrap_over is False, or the first element if wrap_over was True Example: >>> list(add_next([1, 2, 3, 4, 5])) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, None)] >>> list(add_next([1, 2, 3, 4, 5], True)) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, 1)] :param lst: iterable to iterate over :param wrap_over: whether to attach the first element to the pair of the last element instead of None :param skip_last: if this is True, then last element, alongside with a None, won't be output """ iterator = iter(lst) try: first_val = prev_val = next(iterator) except StopIteration: return for val in iterator: yield prev_val, val prev_val = val if wrap_over: yield prev_val, first_val else: if not skip_last: yield prev_val, None def half_cartesian(seq: tp.Iterable[T], include_same_pairs: bool = True) -> tp.Iterator[tp.Tuple[T, T]]: """ Generate half of the Cartesian product of both sequences. Useful when you have a commutative operation that you'd like to execute on both elements (eg. checking for collisions). Example: >>> list(half_cartesian([1, 2, 3], [1, 2, 3])) == \ >>> [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] :param seq: The sequence :param include_same_pairs: if True, then pairs returning two of the same objects will be returned. For example, if False, the following will be true: >>> list(half_cartesian([1, 2, 3], [1, 2, 3], include_same_pairs=False)) == \ >>> [(1, 2), (1, 3), (2, 3)] """ for i, elem1 in enumerate(seq): for j, elem2 in enumerate(seq): if include_same_pairs: if j >= i: yield elem1, elem2 else: if j > i: yield elem1, elem2 def group_quantity(length: int, seq: Iteratable) -> tp.Iterator[tp.List[T]]: """ Slice an iterable into lists containing at most len entries. Eg. >>> assert list(group_quantity(3, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) == [[1, 2, 3], [4, 5, 6], >>> [7, 8, 9], [10]] This correctly detects sequences, and uses an optimized variant via slicing if a sequence is passed. You can safely pass ranges :param length: p_len for the returning sequences :param seq: sequence to split """ if isinstance(seq, tp.Sequence) and not isinstance(seq, range): i = 0 while i < len(seq): yield seq[i:i + length] i += length else: entries = [] for elem in seq: if len(entries) == length: yield entries entries = [elem] else: entries.append(elem) if entries: yield entries def filter_out_nones(y: tp.Sequence[T]) -> tp.List[T]: """ Return all elements, as a list, that are not None :param y: a sequence of items :return: a list of all subelements, in order, that are not None """ output = [] for item in y: if item is not None: output.append(item) return output def filter_out_false(y: tp.Sequence[T]) -> tp.List[T]: """ Return all elements, as a list, that are True :param y: a sequence of items :return: a list of all subelements, in order, that are not None """ output = [] for item in y: if item: output.append(item) return output @rethrow_as(IndexError, ValueError) def index_of_max(seq: tp.Sequence[T]) -> int: """ Return the index of the maximum element :param seq: sequence to examine :return: index of the maximum element :raise ValueError: sequence was empty """ max_index = 0 max_elem = seq[0] for i, elem in enumerate(seq): if elem > max_elem: max_index = i max_elem = elem return max_index def index_of(predicate: Predicate, seq: tp.Sequence[T]) -> int: """ Return an index of first met element that calling predicate on it returns True :param predicate: predicate to apply :param seq: sequence to examine :return: index of the element :raises ValueError: if no element found """ i = 0 for elem in seq: if predicate(elem): return i i += 1 raise ValueError('Element not found') class Multirun: """ A class to launch the same operation on the entire sequence. Consider: >>> class Counter: >>> def __init__(self, value=0): >>> self.count = value >>> def add(self, v): >>> self.count += 1 >>> def __eq__(self, other): >>> return self.count == other.count >>> def __iadd__(self, other): >>> self.add(other) >>> a = [Counter(), Counter()] The following: >>> for b in a: >>> b.add(2) Can be replaced with >>> Multirun(a).add(2) And the following: >>> for b in a: >>> b += 3 With this >>> b = Mulirun(a) >>> b += 3 Furthermore note that: >>> Multirun(a).add(2) == [Counter(2), Counter(2)] :param sequence: sequence to execute these operations for :param dont_return_list: the operation won't return a list if this is True """ __slots__ = 'sequence', 'dont_return_list' def __bool__(self) -> bool: return bool(self.sequence) def __init__(self, sequence: tp.Iterable, dont_return_list: bool = False): self.sequence = sequence self.dont_return_list = dont_return_list def __iter__(self): return iter(self.sequence) def __getattr__(self, item): def inner(*args, **kwargs): if not self.dont_return_list: results = [] for element in self: getattr(element, item)(*args, **kwargs) results.append(element) return results else: for element in self: getattr(element, item)(*args, **kwargs) # Take care: the array might just be empty... try: fun = getattr(n_th(self), item) inner = wraps(fun)(inner) except IndexError: pass return inner def __iadd__(self, other): for element in self: element += other return self def __isub__(self, other): for element in self: element -= other return self def __imul__(self, other): for element in self: element *= other return self def __itruediv__(self, other): for element in self: element /= other return self def __ifloordiv__(self, other): for element in self: element //= other return self def __ilshift__(self, other): for element in self: element <<= other return self def __irshift__(self, other): for element in self: element >>= other return self def __ipow__(self, other): for element in self: element **= other return self \ No newline at end of file +import copy import typing as tp from satella.coding.decorators.wraps import wraps from satella.coding.recast_exceptions import rethrow_as from .iterators import n_th from ..typing import T, Iteratable, NoArgCallable, Predicate def infinite_iterator(returns: tp.Optional[T] = None, return_factory: tp.Optional[NoArgCallable[T]] = None) -> tp.Iterator[T]: """ Return an infinite number of objects. :param returns: object to return. Note that this will be this very object, it will not be copied. :param return_factory: a callable that takes 0 args and returns an element to return. :return: an infinite iterator of provided values """ while True: if returns is None: yield None if return_factory is None else return_factory() else: yield returns def make_list(element: T, n: int, deep_copy: bool = False) -> tp.List[T]: """ Make a list consisting of n times element. Element will be copied via copy.copy before adding to list. :param element: element :param n: times to repeat the element :param deep_copy: whether to use copy.deepcopy instead of copy.copy :return: list of length n """ output = [] if deep_copy: copy_op = copy.deepcopy else: copy_op = copy.copy for _ in range(n): output.append(copy_op(element)) return output # shamelessly copied from # https://medium.com/better-programming/is-this-the-last-element-of-my-python-for-loop-784f5ff90bb5 def is_last(lst: Iteratable) -> tp.Iterator[tp.Tuple[bool, T]]: """ Return every element of the list, alongside a flag telling is this the last element. Use like: >>> for is_last, element in is_last(my_list): >>> if is_last: >>> ... :param lst: list to iterate thru :return: a p_gen returning (bool, T) Note that this returns a nice, O(1) iterator. """ iterable = iter(lst) ret_var = next(iterable) for val in iterable: yield False, ret_var ret_var = val yield True, ret_var def add_next(lst: Iteratable, wrap_over: bool = False, skip_last: bool = False) -> tp.Iterator[tp.Tuple[T, tp.Optional[T]]]: """ Yields a 2-tuple of given iterable, presenting the next element as second element of the tuple. The last element will be the last element alongside with a None, if wrap_over is False, or the first element if wrap_over was True Example: >>> list(add_next([1, 2, 3, 4, 5])) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, None)] >>> list(add_next([1, 2, 3, 4, 5], True)) == [(1, 2), (2, 3), (3, 4), (4, 5), (5, 1)] :param lst: iterable to iterate over :param wrap_over: whether to attach the first element to the pair of the last element instead of None :param skip_last: if this is True, then last element, alongside with a None, won't be output """ iterator = iter(lst) try: first_val = prev_val = next(iterator) except StopIteration: return for val in iterator: yield prev_val, val prev_val = val if wrap_over: yield prev_val, first_val else: if not skip_last: yield prev_val, None def half_cartesian(seq: tp.Iterable[T], include_same_pairs: bool = True) -> tp.Iterator[tp.Tuple[T, T]]: """ Generate half of the Cartesian product of both sequences. Useful when you have a commutative operation that you'd like to execute on both elements (eg. checking for collisions). Example: >>> list(half_cartesian([1, 2, 3], [1, 2, 3])) == \ >>> [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] :param seq: The sequence :param include_same_pairs: if True, then pairs returning two of the same objects will be returned. For example, if False, the following will be true: >>> list(half_cartesian([1, 2, 3], [1, 2, 3], include_same_pairs=False)) == \ >>> [(1, 2), (1, 3), (2, 3)] """ for i, elem1 in enumerate(seq): for j, elem2 in enumerate(seq): if include_same_pairs: if j >= i: yield elem1, elem2 else: if j > i: yield elem1, elem2 def group_quantity(length: int, seq: Iteratable) -> tp.Iterator[tp.List[T]]: """ Slice an iterable into lists containing at most len entries. Eg. >>> assert list(group_quantity(3, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) == [[1, 2, 3], [4, 5, 6], >>> [7, 8, 9], [10]] This correctly detects sequences, and uses an optimized variant via slicing if a sequence is passed. You can safely pass ranges :param length: p_len for the returning sequences :param seq: sequence to split """ if isinstance(seq, tp.Sequence) and not isinstance(seq, range): i = 0 while i < len(seq): yield seq[i:i + length] i += length else: entries = [] for elem in seq: if len(entries) == length: yield entries entries = [elem] else: entries.append(elem) if entries: yield entries def filter_out_nones(y: tp.Sequence[T]) -> tp.List[T]: """ Return all elements, as a list, that are not None :param y: a sequence of items :return: a list of all subelements, in order, that are not None """ output = [] for item in y: if item is not None: output.append(item) return output def filter_out_false(y: tp.Sequence[T]) -> tp.List[T]: """ Return all elements, as a list, that are True :param y: a sequence of items :return: a list of all subelements, in order, that are not None """ output = [] for item in y: if item: output.append(item) return output @rethrow_as(IndexError, ValueError) def index_of_max(seq: tp.Sequence[T]) -> int: """ Return the index of the maximum element :param seq: sequence to examine :return: index of the maximum element :raise ValueError: sequence was empty """ max_index = 0 max_elem = seq[0] for i, elem in enumerate(seq): if elem > max_elem: max_index = i max_elem = elem return max_index def index_of(predicate: Predicate, seq: tp.Sequence[T]) -> int: """ Return an index of first met element that calling predicate on it returns True :param predicate: predicate to apply :param seq: sequence to examine :return: index of the element :raises ValueError: if no element found """ i = 0 for elem in seq: if predicate(elem): return i i += 1 raise ValueError('Element not found') class Multirun: """ A class to launch the same operation on the entire sequence. Consider: >>> class Counter: >>> def __init__(self, value=0): >>> self.count = value >>> def add(self, v): >>> self.count += 1 >>> def __eq__(self, other): >>> return self.count == other.count >>> def __iadd__(self, other): >>> self.add(other) >>> a = [Counter(), Counter()] The following: >>> for b in a: >>> b.add(2) Can be replaced with >>> Multirun(a).add(2) And the following: >>> for b in a: >>> b += 3 With this >>> b = Mulirun(a) >>> b += 3 Furthermore note that: >>> Multirun(a).add(2) == [Counter(2), Counter(2)] :param sequence: sequence to execute these operations for :param dont_return_list: the operation won't return a list if this is True """ __slots__ = 'sequence', 'dont_return_list' def __bool__(self) -> bool: return bool(self.sequence) def __init__(self, sequence: tp.Iterable, dont_return_list: bool = False): self.sequence = sequence self.dont_return_list = dont_return_list def __iter__(self): return iter(self.sequence) def __getattr__(self, item): def inner(*args, **kwargs): if not self.dont_return_list: results = [] for element in self: getattr(element, item)(*args, **kwargs) results.append(element) return results else: for element in self: getattr(element, item)(*args, **kwargs) # Take care: the array might just be empty... try: fun = getattr(n_th(self), item) inner = wraps(fun)(inner) except IndexError: pass return inner def __iadd__(self, other): for element in self: element += other return self def __isub__(self, other): for element in self: element -= other return self def __imul__(self, other): for element in self: element *= other return self def __itruediv__(self, other): for element in self: element /= other return self def __ifloordiv__(self, other): for element in self: element //= other return self def __ilshift__(self, other): for element in self: element <<= other return self def __irshift__(self, other): for element in self: element >>= other return self def __ipow__(self, other): for element in self: element **= other return self \ No newline at end of file diff --git a/satella/coding/structures/heaps/base.py b/satella/coding/structures/heaps/base.py index 41c5da12cb9921b559f75797a1f55840e5a81397..4053619a7f716721718646ffa2123884630f231b 100644 --- a/satella/coding/structures/heaps/base.py +++ b/satella/coding/structures/heaps/base.py @@ -3,7 +3,7 @@ import copy import heapq import typing as tp -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.typing import T, Predicate diff --git a/satella/coding/structures/proxy.py b/satella/coding/structures/proxy.py index 54ec9b5aa9fc3feb785a97f531f89288560dd1dd..20873dfa9b67ab536c0dbf7f3ca9d90ad55d159d 100644 --- a/satella/coding/structures/proxy.py +++ b/satella/coding/structures/proxy.py @@ -2,7 +2,7 @@ import logging import math import typing as tp -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.recast_exceptions import rethrow_as from satella.coding.typing import T diff --git a/satella/coding/structures/singleton.py b/satella/coding/structures/singleton.py index 1744c96995394bc47706604733f7f9b2501a78d2..527ee91217a00f4c99c4029667a24c8a3f3dd17d 100644 --- a/satella/coding/structures/singleton.py +++ b/satella/coding/structures/singleton.py @@ -1,7 +1,7 @@ import typing as tp import weakref -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps # noinspection PyPep8Naming diff --git a/satella/dao.py b/satella/dao.py index 329cea3be6aa2814744a2ac281731c4542deffa0..8393246c31778d452cd4cb1956ff2ff8a283d993 100644 --- a/satella/dao.py +++ b/satella/dao.py @@ -1,6 +1,6 @@ from abc import ABCMeta, abstractmethod -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps __all__ = ['Loadable', 'must_be_loaded'] diff --git a/satella/instrumentation/metrics/metric_types/measurable_mixin.py b/satella/instrumentation/metrics/metric_types/measurable_mixin.py index 5b3c519e736076044867175eed35c2aeb5957c9b..2ab73900c332b0392092f946694e92ec1f6e12b0 100644 --- a/satella/instrumentation/metrics/metric_types/measurable_mixin.py +++ b/satella/instrumentation/metrics/metric_types/measurable_mixin.py @@ -2,7 +2,7 @@ import inspect import time from concurrent.futures import Future -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.typing import NoArgCallable from .base import MetricLevel diff --git a/satella/time/backoff.py b/satella/time/backoff.py index 74c3a419d2d72c4500d54233f000b3c10fbb2a42..6b1b4f9d82d9fa07129a9e240c87446bd37d7ada 100644 --- a/satella/time/backoff.py +++ b/satella/time/backoff.py @@ -1,7 +1,7 @@ import time import typing as tp -from satella.coding.decorators.decorators import wraps +from satella.coding.decorators.wraps import wraps from satella.coding.typing import ExceptionList from satella.exceptions import WouldWaitMore from satella.time.measure import measure diff --git a/tests/test_cassandra.py b/tests/test_cassandra.py index 608eef3488f917fd24b18d9d65f587197b4af9c0..da929599c320f92f4e71bcbdda0ab5ed0d55f74f 100644 --- a/tests/test_cassandra.py +++ b/tests/test_cassandra.py @@ -1,4 +1,4 @@ -from satella.coding.concurrent import CallableGroup +from satella.coding.concurrent import CallableGroup, IDAllocator from satella.cassandra import parallel_for, wrap_future import unittest @@ -6,6 +6,7 @@ import unittest class TestCassandra(unittest.TestCase): def test_wrap_future(self): + class MockCassandraFuture: def __init__(self): self.value = None diff --git a/tests/test_coding/test_concurrent.py b/tests/test_coding/test_concurrent.py index a0c58a36d6c081e6c715a6a8f0f00135b01520a0..2b47e13d64b5b8aa63331f0adfcb9e5124d51227 100644 --- a/tests/test_coding/test_concurrent.py +++ b/tests/test_coding/test_concurrent.py @@ -270,7 +270,7 @@ class TestConcurrent(unittest.TestCase): nonlocal a a[4] += 1 - cbgroup.add_many(y, z) + cb = cbgroup.add_many(y, z) cbgroup.add(CancellableCallback(p, one_shotted=False)) cbgroup() self.assertEqual(a[1],3) @@ -278,6 +278,9 @@ class TestConcurrent(unittest.TestCase): self.assertEqual(a[4], 7) cbgroup() self.assertEqual(a[4], 8) + cb.cancel() + cbgroup() + self.assertEqual(a[4], 7) def test_peekable_queue_put_many(self): pkb = PeekableQueue() @@ -765,8 +768,11 @@ class TestConcurrent(unittest.TestCase): def test_cg_proforma(self): cg = CallableGroup() a = {} - cg.add(lambda: a.__setitem__('test', 'value')) - cg() + def cg_proforma(val): + nonlocal a + a['test'] = val + cg.add(cg_proforma) + cg('value') self.assertEqual(a['test'], 'value') def test_terminable_thread(self): @@ -877,18 +883,18 @@ class TestConcurrent(unittest.TestCase): def test_callable_group(self): a = { - 'a': False, - 'b': False + 'a': 1 } def op_f(what): - return lambda: a.__setitem__(what, True) + what['a'] += 1 cg = CallableGroup() - cg.add(op_f('a')) - cg.add(op_f('b')) + cg.add(op_f) + self.assertRaises(RuntimeError, cg.add, op_f) + self.assertEqual(len(cg), 2) - cg() + cg(a) - self.assertTrue(all(a.values())) + self.assertEqual(a, {'a': 3}) diff --git a/tests/test_configuration/test_schema.py b/tests/test_configuration/test_schema.py index e1cdb81b23a7b0d2c8627f712cb74214eaaaf188..0268baae60401fd7f820e74f3109a28b1df305a7 100644 --- a/tests/test_configuration/test_schema.py +++ b/tests/test_configuration/test_schema.py @@ -15,6 +15,11 @@ class Environment(enum.IntEnum): class TestSchema(unittest.TestCase): + def setUp(self): + for file in ['test']: + if os.path.exists(file): + os.unlink(file) + def test_file(self): schema = { "key": "file" diff --git a/tests/test_files.py b/tests/test_files.py index 6206d6f02b99fa2f53df3c1688915be45869fff6..517eb0fd1f39d803f3cc4ed3f56fe29e636a58a7 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,5 +1,6 @@ import io import os +import uuid from os.path import join import tempfile import unittest @@ -114,15 +115,17 @@ class TestFiles(unittest.TestCase): self.assertFalse(try_unlink('test.txt')) def test_write_out_file_if_different(self): - try: - os.unlink('test') - except FileNotFoundError: - pass - try: - self.assertTrue(write_out_file_if_different('test', 'test', 'UTF-8')) - self.assertFalse(write_out_file_if_different('test', 'test', 'UTF-8')) - finally: - os.unlink('test') + # Wait until Windows releases the file + while True: + try: + os.unlink('test') + except (PermissionError, FileNotFoundError): + pass + try: + self.assertTrue(write_out_file_if_different('test-'+uuid.uuid4().hex, 'test', 'UTF-8')) + self.assertFalse(write_out_file_if_different('test-'+uuid.uuid4().hex, 'test', 'UTF-8')) + finally: + os.unlink('test') def test_read_in_and_write_to(self): data = 'żażółć gęślą jaźń'