Update jaraco.functools-3.3.0

This commit is contained in:
JonnyWong16 2021-10-14 21:15:17 -07:00
commit b3ae6bd695
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
2 changed files with 400 additions and 362 deletions

View file

@ -1 +0,0 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)

View file

@ -1,467 +1,506 @@
from __future__ import (
absolute_import, unicode_literals, print_function, division,
)
import functools import functools
import time import time
import warnings
import inspect import inspect
import collections import collections
from itertools import count import types
import itertools
__metaclass__ = type import more_itertools
try:
from functools import lru_cache
except ImportError:
try:
from backports.functools_lru_cache import lru_cache
except ImportError:
try:
from functools32 import lru_cache
except ImportError:
warnings.warn("No lru_cache available")
import more_itertools.recipes
def compose(*funcs): def compose(*funcs):
""" """
Compose any number of unary functions into a single unary function. Compose any number of unary functions into a single unary function.
>>> import textwrap >>> import textwrap
>>> from six import text_type >>> stripped = str.strip(textwrap.dedent(compose.__doc__))
>>> stripped = text_type.strip(textwrap.dedent(compose.__doc__)) >>> compose(str.strip, textwrap.dedent)(compose.__doc__) == stripped
>>> compose(text_type.strip, textwrap.dedent)(compose.__doc__) == stripped True
True
Compose also allows the innermost function to take arbitrary arguments. Compose also allows the innermost function to take arbitrary arguments.
>>> round_three = lambda x: round(x, ndigits=3) >>> round_three = lambda x: round(x, ndigits=3)
>>> f = compose(round_three, int.__truediv__) >>> f = compose(round_three, int.__truediv__)
>>> [f(3*x, x+1) for x in range(1,10)] >>> [f(3*x, x+1) for x in range(1,10)]
[1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7] [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7]
""" """
def compose_two(f1, f2): def compose_two(f1, f2):
return lambda *args, **kwargs: f1(f2(*args, **kwargs)) return lambda *args, **kwargs: f1(f2(*args, **kwargs))
return functools.reduce(compose_two, funcs)
return functools.reduce(compose_two, funcs)
def method_caller(method_name, *args, **kwargs): def method_caller(method_name, *args, **kwargs):
""" """
Return a function that will call a named method on the Return a function that will call a named method on the
target object with optional positional and keyword target object with optional positional and keyword
arguments. arguments.
>>> lower = method_caller('lower') >>> lower = method_caller('lower')
>>> lower('MyString') >>> lower('MyString')
'mystring' 'mystring'
""" """
def call_method(target):
func = getattr(target, method_name) def call_method(target):
return func(*args, **kwargs) func = getattr(target, method_name)
return call_method return func(*args, **kwargs)
return call_method
def once(func): def once(func):
""" """
Decorate func so it's only ever called the first time. Decorate func so it's only ever called the first time.
This decorator can ensure that an expensive or non-idempotent function This decorator can ensure that an expensive or non-idempotent function
will not be expensive on subsequent calls and is idempotent. will not be expensive on subsequent calls and is idempotent.
>>> add_three = once(lambda a: a+3) >>> add_three = once(lambda a: a+3)
>>> add_three(3) >>> add_three(3)
6 6
>>> add_three(9) >>> add_three(9)
6 6
>>> add_three('12') >>> add_three('12')
6 6
To reset the stored value, simply clear the property ``saved_result``. To reset the stored value, simply clear the property ``saved_result``.
>>> del add_three.saved_result >>> del add_three.saved_result
>>> add_three(9) >>> add_three(9)
12 12
>>> add_three(8) >>> add_three(8)
12 12
Or invoke 'reset()' on it. Or invoke 'reset()' on it.
>>> add_three.reset() >>> add_three.reset()
>>> add_three(-3) >>> add_three(-3)
0 0
>>> add_three(0) >>> add_three(0)
0 0
""" """
@functools.wraps(func)
def wrapper(*args, **kwargs): @functools.wraps(func)
if not hasattr(wrapper, 'saved_result'): def wrapper(*args, **kwargs):
wrapper.saved_result = func(*args, **kwargs) if not hasattr(wrapper, 'saved_result'):
return wrapper.saved_result wrapper.saved_result = func(*args, **kwargs)
wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result') return wrapper.saved_result
return wrapper
wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result')
return wrapper
def method_cache(method, cache_wrapper=None): def method_cache(method, cache_wrapper=None):
""" """
Wrap lru_cache to support storing the cache data in the object instances. Wrap lru_cache to support storing the cache data in the object instances.
Abstracts the common paradigm where the method explicitly saves an Abstracts the common paradigm where the method explicitly saves an
underscore-prefixed protected property on first call and returns that underscore-prefixed protected property on first call and returns that
subsequently. subsequently.
>>> class MyClass: >>> class MyClass:
... calls = 0 ... calls = 0
... ...
... @method_cache ... @method_cache
... def method(self, value): ... def method(self, value):
... self.calls += 1 ... self.calls += 1
... return value ... return value
>>> a = MyClass() >>> a = MyClass()
>>> a.method(3) >>> a.method(3)
3 3
>>> for x in range(75): >>> for x in range(75):
... res = a.method(x) ... res = a.method(x)
>>> a.calls >>> a.calls
75 75
Note that the apparent behavior will be exactly like that of lru_cache Note that the apparent behavior will be exactly like that of lru_cache
except that the cache is stored on each instance, so values in one except that the cache is stored on each instance, so values in one
instance will not flush values from another, and when an instance is instance will not flush values from another, and when an instance is
deleted, so are the cached values for that instance. deleted, so are the cached values for that instance.
>>> b = MyClass() >>> b = MyClass()
>>> for x in range(35): >>> for x in range(35):
... res = b.method(x) ... res = b.method(x)
>>> b.calls >>> b.calls
35 35
>>> a.method(0) >>> a.method(0)
0 0
>>> a.calls >>> a.calls
75 75
Note that if method had been decorated with ``functools.lru_cache()``, Note that if method had been decorated with ``functools.lru_cache()``,
a.calls would have been 76 (due to the cached value of 0 having been a.calls would have been 76 (due to the cached value of 0 having been
flushed by the 'b' instance). flushed by the 'b' instance).
Clear the cache with ``.cache_clear()`` Clear the cache with ``.cache_clear()``
>>> a.method.cache_clear() >>> a.method.cache_clear()
Another cache wrapper may be supplied: Same for a method that hasn't yet been called.
>>> cache = lru_cache(maxsize=2) >>> c = MyClass()
>>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) >>> c.method.cache_clear()
>>> a = MyClass()
>>> a.method2()
3
Caution - do not subsequently wrap the method with another decorator, such Another cache wrapper may be supplied:
as ``@property``, which changes the semantics of the function.
See also >>> cache = functools.lru_cache(maxsize=2)
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
for another implementation and additional justification. >>> a = MyClass()
""" >>> a.method2()
cache_wrapper = cache_wrapper or lru_cache() 3
def wrapper(self, *args, **kwargs): Caution - do not subsequently wrap the method with another decorator, such
# it's the first call, replace the method with a cached, bound method as ``@property``, which changes the semantics of the function.
bound_method = functools.partial(method, self)
cached_method = cache_wrapper(bound_method)
setattr(self, method.__name__, cached_method)
return cached_method(*args, **kwargs)
return _special_method_cache(method, cache_wrapper) or wrapper See also
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
for another implementation and additional justification.
"""
cache_wrapper = cache_wrapper or functools.lru_cache()
def wrapper(self, *args, **kwargs):
# it's the first call, replace the method with a cached, bound method
bound_method = types.MethodType(method, self)
cached_method = cache_wrapper(bound_method)
setattr(self, method.__name__, cached_method)
return cached_method(*args, **kwargs)
# Support cache clear even before cache has been created.
wrapper.cache_clear = lambda: None
return _special_method_cache(method, cache_wrapper) or wrapper
def _special_method_cache(method, cache_wrapper): def _special_method_cache(method, cache_wrapper):
""" """
Because Python treats special methods differently, it's not Because Python treats special methods differently, it's not
possible to use instance attributes to implement the cached possible to use instance attributes to implement the cached
methods. methods.
Instead, install the wrapper method under a different name Instead, install the wrapper method under a different name
and return a simple proxy to that wrapper. and return a simple proxy to that wrapper.
https://github.com/jaraco/jaraco.functools/issues/5 https://github.com/jaraco/jaraco.functools/issues/5
""" """
name = method.__name__ name = method.__name__
special_names = '__getattr__', '__getitem__' special_names = '__getattr__', '__getitem__'
if name not in special_names: if name not in special_names:
return return
wrapper_name = '__cached' + name wrapper_name = '__cached' + name
def proxy(self, *args, **kwargs): def proxy(self, *args, **kwargs):
if wrapper_name not in vars(self): if wrapper_name not in vars(self):
bound = functools.partial(method, self) bound = types.MethodType(method, self)
cache = cache_wrapper(bound) cache = cache_wrapper(bound)
setattr(self, wrapper_name, cache) setattr(self, wrapper_name, cache)
else: else:
cache = getattr(self, wrapper_name) cache = getattr(self, wrapper_name)
return cache(*args, **kwargs) return cache(*args, **kwargs)
return proxy return proxy
def apply(transform): def apply(transform):
""" """
Decorate a function with a transform function that is Decorate a function with a transform function that is
invoked on results returned from the decorated function. invoked on results returned from the decorated function.
>>> @apply(reversed) >>> @apply(reversed)
... def get_numbers(start): ... def get_numbers(start):
... return range(start, start+3) ... return range(start, start+3)
>>> list(get_numbers(4)) >>> list(get_numbers(4))
[6, 5, 4] [6, 5, 4]
""" """
def wrap(func):
return compose(transform, func) def wrap(func):
return wrap return compose(transform, func)
return wrap
def result_invoke(action): def result_invoke(action):
r""" r"""
Decorate a function with an action function that is Decorate a function with an action function that is
invoked on the results returned from the decorated invoked on the results returned from the decorated
function (for its side-effect), then return the original function (for its side-effect), then return the original
result. result.
>>> @result_invoke(print) >>> @result_invoke(print)
... def add_two(a, b): ... def add_two(a, b):
... return a + b ... return a + b
>>> x = add_two(2, 3) >>> x = add_two(2, 3)
5 5
""" """
def wrap(func):
@functools.wraps(func) def wrap(func):
def wrapper(*args, **kwargs): @functools.wraps(func)
result = func(*args, **kwargs) def wrapper(*args, **kwargs):
action(result) result = func(*args, **kwargs)
return result action(result)
return wrapper return result
return wrap
return wrapper
return wrap
def call_aside(f, *args, **kwargs): def call_aside(f, *args, **kwargs):
""" """
Call a function for its side effect after initialization. Call a function for its side effect after initialization.
>>> @call_aside >>> @call_aside
... def func(): print("called") ... def func(): print("called")
called called
>>> func() >>> func()
called called
Use functools.partial to pass parameters to the initial call Use functools.partial to pass parameters to the initial call
>>> @functools.partial(call_aside, name='bingo') >>> @functools.partial(call_aside, name='bingo')
... def func(name): print("called with", name) ... def func(name): print("called with", name)
called with bingo called with bingo
""" """
f(*args, **kwargs) f(*args, **kwargs)
return f return f
class Throttler: class Throttler:
""" """
Rate-limit a function (or other callable) Rate-limit a function (or other callable)
""" """
def __init__(self, func, max_rate=float('Inf')):
if isinstance(func, Throttler):
func = func.func
self.func = func
self.max_rate = max_rate
self.reset()
def reset(self): def __init__(self, func, max_rate=float('Inf')):
self.last_called = 0 if isinstance(func, Throttler):
func = func.func
self.func = func
self.max_rate = max_rate
self.reset()
def __call__(self, *args, **kwargs): def reset(self):
self._wait() self.last_called = 0
return self.func(*args, **kwargs)
def _wait(self): def __call__(self, *args, **kwargs):
"ensure at least 1/max_rate seconds from last call" self._wait()
elapsed = time.time() - self.last_called return self.func(*args, **kwargs)
must_wait = 1 / self.max_rate - elapsed
time.sleep(max(0, must_wait))
self.last_called = time.time()
def __get__(self, obj, type=None): def _wait(self):
return first_invoke(self._wait, functools.partial(self.func, obj)) "ensure at least 1/max_rate seconds from last call"
elapsed = time.time() - self.last_called
must_wait = 1 / self.max_rate - elapsed
time.sleep(max(0, must_wait))
self.last_called = time.time()
def __get__(self, obj, type=None):
return first_invoke(self._wait, functools.partial(self.func, obj))
def first_invoke(func1, func2): def first_invoke(func1, func2):
""" """
Return a function that when invoked will invoke func1 without Return a function that when invoked will invoke func1 without
any parameters (for its side-effect) and then invoke func2 any parameters (for its side-effect) and then invoke func2
with whatever parameters were passed, returning its result. with whatever parameters were passed, returning its result.
""" """
def wrapper(*args, **kwargs):
func1() def wrapper(*args, **kwargs):
return func2(*args, **kwargs) func1()
return wrapper return func2(*args, **kwargs)
return wrapper
def retry_call(func, cleanup=lambda: None, retries=0, trap=()): def retry_call(func, cleanup=lambda: None, retries=0, trap=()):
""" """
Given a callable func, trap the indicated exceptions Given a callable func, trap the indicated exceptions
for up to 'retries' times, invoking cleanup on the for up to 'retries' times, invoking cleanup on the
exception. On the final attempt, allow any exceptions exception. On the final attempt, allow any exceptions
to propagate. to propagate.
""" """
attempts = count() if retries == float('inf') else range(retries) attempts = itertools.count() if retries == float('inf') else range(retries)
for attempt in attempts: for attempt in attempts:
try: try:
return func() return func()
except trap: except trap:
cleanup() cleanup()
return func() return func()
def retry(*r_args, **r_kwargs): def retry(*r_args, **r_kwargs):
""" """
Decorator wrapper for retry_call. Accepts arguments to retry_call Decorator wrapper for retry_call. Accepts arguments to retry_call
except func and then returns a decorator for the decorated function. except func and then returns a decorator for the decorated function.
Ex: Ex:
>>> @retry(retries=3) >>> @retry(retries=3)
... def my_func(a, b): ... def my_func(a, b):
... "this is my funk" ... "this is my funk"
... print(a, b) ... print(a, b)
>>> my_func.__doc__ >>> my_func.__doc__
'this is my funk' 'this is my funk'
""" """
def decorate(func):
@functools.wraps(func) def decorate(func):
def wrapper(*f_args, **f_kwargs): @functools.wraps(func)
bound = functools.partial(func, *f_args, **f_kwargs) def wrapper(*f_args, **f_kwargs):
return retry_call(bound, *r_args, **r_kwargs) bound = functools.partial(func, *f_args, **f_kwargs)
return wrapper return retry_call(bound, *r_args, **r_kwargs)
return decorate
return wrapper
return decorate
def print_yielded(func): def print_yielded(func):
""" """
Convert a generator into a function that prints all yielded elements Convert a generator into a function that prints all yielded elements
>>> @print_yielded >>> @print_yielded
... def x(): ... def x():
... yield 3; yield None ... yield 3; yield None
>>> x() >>> x()
3 3
None None
""" """
print_all = functools.partial(map, print) print_all = functools.partial(map, print)
print_results = compose(more_itertools.recipes.consume, print_all, func) print_results = compose(more_itertools.consume, print_all, func)
return functools.wraps(func)(print_results) return functools.wraps(func)(print_results)
def pass_none(func): def pass_none(func):
""" """
Wrap func so it's not called if its first param is None Wrap func so it's not called if its first param is None
>>> print_text = pass_none(print) >>> print_text = pass_none(print)
>>> print_text('text') >>> print_text('text')
text text
>>> print_text(None) >>> print_text(None)
""" """
@functools.wraps(func)
def wrapper(param, *args, **kwargs): @functools.wraps(func)
if param is not None: def wrapper(param, *args, **kwargs):
return func(param, *args, **kwargs) if param is not None:
return wrapper return func(param, *args, **kwargs)
return wrapper
def assign_params(func, namespace): def assign_params(func, namespace):
""" """
Assign parameters from namespace where func solicits. Assign parameters from namespace where func solicits.
>>> def func(x, y=3): >>> def func(x, y=3):
... print(x, y) ... print(x, y)
>>> assigned = assign_params(func, dict(x=2, z=4)) >>> assigned = assign_params(func, dict(x=2, z=4))
>>> assigned() >>> assigned()
2 3 2 3
The usual errors are raised if a function doesn't receive The usual errors are raised if a function doesn't receive
its required parameters: its required parameters:
>>> assigned = assign_params(func, dict(y=3, z=4)) >>> assigned = assign_params(func, dict(y=3, z=4))
>>> assigned() >>> assigned()
Traceback (most recent call last): Traceback (most recent call last):
TypeError: func() ...argument... TypeError: func() ...argument...
It even works on methods: It even works on methods:
>>> class Handler: >>> class Handler:
... def meth(self, arg): ... def meth(self, arg):
... print(arg) ... print(arg)
>>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))() >>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))()
crystal crystal
""" """
try: sig = inspect.signature(func)
sig = inspect.signature(func) params = sig.parameters.keys()
params = sig.parameters.keys() call_ns = {k: namespace[k] for k in params if k in namespace}
except AttributeError: return functools.partial(func, **call_ns)
spec = inspect.getargspec(func)
params = spec.args
call_ns = {
k: namespace[k]
for k in params
if k in namespace
}
return functools.partial(func, **call_ns)
def save_method_args(method): def save_method_args(method):
""" """
Wrap a method such that when it is called, the args and kwargs are Wrap a method such that when it is called, the args and kwargs are
saved on the method. saved on the method.
>>> class MyClass: >>> class MyClass:
... @save_method_args ... @save_method_args
... def method(self, a, b): ... def method(self, a, b):
... print(a, b) ... print(a, b)
>>> my_ob = MyClass() >>> my_ob = MyClass()
>>> my_ob.method(1, 2) >>> my_ob.method(1, 2)
1 2 1 2
>>> my_ob._saved_method.args >>> my_ob._saved_method.args
(1, 2) (1, 2)
>>> my_ob._saved_method.kwargs >>> my_ob._saved_method.kwargs
{} {}
>>> my_ob.method(a=3, b='foo') >>> my_ob.method(a=3, b='foo')
3 foo 3 foo
>>> my_ob._saved_method.args >>> my_ob._saved_method.args
() ()
>>> my_ob._saved_method.kwargs == dict(a=3, b='foo') >>> my_ob._saved_method.kwargs == dict(a=3, b='foo')
True True
The arguments are stored on the instance, allowing for The arguments are stored on the instance, allowing for
different instance to save different args. different instance to save different args.
>>> your_ob = MyClass() >>> your_ob = MyClass()
>>> your_ob.method({str('x'): 3}, b=[4]) >>> your_ob.method({str('x'): 3}, b=[4])
{'x': 3} [4] {'x': 3} [4]
>>> your_ob._saved_method.args >>> your_ob._saved_method.args
({'x': 3},) ({'x': 3},)
>>> my_ob._saved_method.args >>> my_ob._saved_method.args
() ()
""" """
args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs')
@functools.wraps(method) @functools.wraps(method)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
attr_name = '_saved_' + method.__name__ attr_name = '_saved_' + method.__name__
attr = args_and_kwargs(args, kwargs) attr = args_and_kwargs(args, kwargs)
setattr(self, attr_name, attr) setattr(self, attr_name, attr)
return method(self, *args, **kwargs) return method(self, *args, **kwargs)
return wrapper
return wrapper
def except_(*exceptions, replace=None, use=None):
"""
Replace the indicated exceptions, if raised, with the indicated
literal replacement or evaluated expression (if present).
>>> safe_int = except_(ValueError)(int)
>>> safe_int('five')
>>> safe_int('5')
5
Specify a literal replacement with ``replace``.
>>> safe_int_r = except_(ValueError, replace=0)(int)
>>> safe_int_r('five')
0
Provide an expression to ``use`` to pass through particular parameters.
>>> safe_int_pt = except_(ValueError, use='args[0]')(int)
>>> safe_int_pt('five')
'five'
"""
def decorate(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except exceptions:
try:
return eval(use)
except TypeError:
return replace
return wrapper
return decorate