X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=decorator_utils.py;h=4615fec6f8960e0083ce48546ba9421c25243d42;hb=119692e4487278e5d19c06cfe7dc062c2bd7efc5;hp=80aec4aaae788023f04588d4a78327a761cf1638;hpb=9940cf42dbd246211f34c7d3eecf9fa5be4dd642;p=python_utils.git diff --git a/decorator_utils.py b/decorator_utils.py index 80aec4a..4615fec 100644 --- a/decorator_utils.py +++ b/decorator_utils.py @@ -3,7 +3,7 @@ # © Copyright 2021-2022, Scott Gasch # Portions (marked) below retain the original author's copyright. -"""Decorators.""" +"""Useful(?) decorators.""" import enum import functools @@ -33,7 +33,7 @@ def timed(func: Callable) -> Callable: >>> @timed ... def foo(): ... import time - ... time.sleep(0.1) + ... time.sleep(0.01) >>> foo() # doctest: +ELLIPSIS Finished foo in ... @@ -55,7 +55,7 @@ def timed(func: Callable) -> Callable: def invocation_logged(func: Callable) -> Callable: - """Log the call of a function. + """Log the call of a function on stdout and the info log. >>> @invocation_logged ... def foo(): @@ -83,9 +83,9 @@ def invocation_logged(func: Callable) -> Callable: def rate_limited(n_calls: int, *, per_period_in_seconds: float = 1.0) -> Callable: - """Limit invocation of a wrapped function to n calls per period. + """Limit invocation of a wrapped function to n calls per time period. Thread safe. In testing this was relatively fair with multiple - threads using it though that hasn't been measured. + threads using it though that hasn't been measured in detail. >>> import time >>> import decorator_utils @@ -246,9 +246,7 @@ def delay( seconds: float = 1.0, when: DelayWhen = DelayWhen.BEFORE_CALL, ) -> Callable: - """Delay the execution of a function by sleeping before and/or after. - - Slow down a function by inserting a delay before and/or after its + """Slow down a function by inserting a delay before and/or after its invocation. >>> import time @@ -330,8 +328,9 @@ def memoized(func: Callable) -> Callable: """Keep a cache of previous function call results. The cache here is a dict with a key based on the arguments to the - call. Consider also: functools.lru_cache for a more advanced - implementation. + call. Consider also: functools.cache for a more advanced + implementation. See: + https://docs.python.org/3/library/functools.html#functools.cache >>> import time @@ -382,17 +381,20 @@ def retry_predicate( delay_sec: float = 3.0, backoff: float = 2.0, ): - """Retries a function or method up to a certain number of times - with a prescribed initial delay period and backoff rate. - - tries is the maximum number of attempts to run the function. - delay_sec sets the initial delay period in seconds. - backoff is a multiplied (must be >1) used to modify the delay. - predicate is a function that will be passed the retval of the - decorated function and must return True to stop or False to - retry. - + """Retries a function or method up to a certain number of times with a + prescribed initial delay period and backoff rate (multiplier). + + Args: + tries: the maximum number of attempts to run the function + delay_sec: sets the initial delay period in seconds + backoff: a multiplier (must be >=1.0) used to modify the + delay at each subsequent invocation + predicate: a Callable that will be passed the retval of + the decorated function and must return True to indicate + that we should stop calling or False to indicate a retry + is necessary """ + if backoff < 1.0: msg = f"backoff must be greater than or equal to 1, got {backoff}" logger.critical(msg) @@ -438,7 +440,6 @@ def retry_if_false(tries: int, *, delay_sec=3.0, backoff=2.0): >>> import time >>> counter = 0 - >>> @retry_if_false(5, delay_sec=1.0, backoff=1.1) ... def foo(): ... global counter @@ -470,8 +471,8 @@ def retry_if_none(tries: int, *, delay_sec=3.0, backoff=2.0): """Another helper for @retry_predicate above. Retries up to N times so long as the wrapped function returns None with a delay between each retry and a backoff that can increase the delay. - """ + return retry_predicate( tries, predicate=lambda x: x is not None, @@ -484,7 +485,6 @@ def deprecated(func): """This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used. - """ @functools.wraps(func) @@ -647,6 +647,13 @@ def timeout( main thread). When not using signals, timeout granularity will be rounded to the nearest 0.1s. + Beware that an @timeout on a function inside a module will be + evaluated at module load time and not when the wrapped function is + invoked. This can lead to problems when relying on the automatic + main thread detection code (use_signals=None, the default) since + the import probably happens on the main thread and the invocation + can happen on a different thread (which can't use signals). + Raises an exception when/if the timeout is reached. It is illegal to pass anything other than a function as the first @@ -708,6 +715,11 @@ def timeout( def synchronized(lock): + """Emulates java's synchronized keyword: given a lock, require that + threads take that lock (or wait) before invoking the wrapped + function and automatically releases the lock afterwards. + """ + def wrap(f): @functools.wraps(f) def _gatekeeper(*args, **kw): @@ -723,6 +735,10 @@ def synchronized(lock): def call_with_sample_rate(sample_rate: float) -> Callable: + """Calls the wrapped function probabilistically given a rate between + 0.0 and 1.0 inclusive (0% probability and 100% probability). + """ + if not 0.0 <= sample_rate <= 1.0: msg = f"sample_rate must be between [0, 1]. Got {sample_rate}." logger.critical(msg) @@ -743,9 +759,9 @@ def call_with_sample_rate(sample_rate: float) -> Callable: def decorate_matching_methods_with(decorator, acl=None): - """Apply decorator to all methods in a class whose names begin with - prefix. If prefix is None (default), decorate all methods in the - class. + """Apply the given decorator to all methods in a class whose names + begin with prefix. If prefix is None (default), decorate all + methods in the class. """ def decorate_the_class(cls):