8 import pkg_resources.extern.more_itertools
10 from typing import Callable, TypeVar
13 CallableT = TypeVar("CallableT", bound=Callable[..., object])
18 Compose any number of unary functions into a single unary function.
21 >>> expected = str.strip(textwrap.dedent(compose.__doc__))
22 >>> strip_and_dedent = compose(str.strip, textwrap.dedent)
23 >>> strip_and_dedent(compose.__doc__) == expected
26 Compose also allows the innermost function to take arbitrary arguments.
28 >>> round_three = lambda x: round(x, ndigits=3)
29 >>> f = compose(round_three, int.__truediv__)
30 >>> [f(3*x, x+1) for x in range(1,10)]
31 [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7]
34 def compose_two(f1, f2):
35 return lambda *args, **kwargs: f1(f2(*args, **kwargs))
37 return functools.reduce(compose_two, funcs)
40 def method_caller(method_name, *args, **kwargs):
42 Return a function that will call a named method on the
43 target object with optional positional and keyword
46 >>> lower = method_caller('lower')
51 def call_method(target):
52 func = getattr(target, method_name)
53 return func(*args, **kwargs)
60 Decorate func so it's only ever called the first time.
62 This decorator can ensure that an expensive or non-idempotent function
63 will not be expensive on subsequent calls and is idempotent.
65 >>> add_three = once(lambda a: a+3)
73 To reset the stored value, simply clear the property ``saved_result``.
75 >>> del add_three.saved_result
81 Or invoke 'reset()' on it.
90 @functools.wraps(func)
91 def wrapper(*args, **kwargs):
92 if not hasattr(wrapper, 'saved_result'):
93 wrapper.saved_result = func(*args, **kwargs)
94 return wrapper.saved_result
96 wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result')
102 cache_wrapper: Callable[
103 [CallableT], CallableT
104 ] = functools.lru_cache(), # type: ignore[assignment]
107 Wrap lru_cache to support storing the cache data in the object instances.
109 Abstracts the common paradigm where the method explicitly saves an
110 underscore-prefixed protected property on first call and returns that
117 ... def method(self, value):
124 >>> for x in range(75):
125 ... res = a.method(x)
129 Note that the apparent behavior will be exactly like that of lru_cache
130 except that the cache is stored on each instance, so values in one
131 instance will not flush values from another, and when an instance is
132 deleted, so are the cached values for that instance.
135 >>> for x in range(35):
136 ... res = b.method(x)
144 Note that if method had been decorated with ``functools.lru_cache()``,
145 a.calls would have been 76 (due to the cached value of 0 having been
146 flushed by the 'b' instance).
148 Clear the cache with ``.cache_clear()``
150 >>> a.method.cache_clear()
152 Same for a method that hasn't yet been called.
155 >>> c.method.cache_clear()
157 Another cache wrapper may be supplied:
159 >>> cache = functools.lru_cache(maxsize=2)
160 >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
165 Caution - do not subsequently wrap the method with another decorator, such
166 as ``@property``, which changes the semantics of the function.
169 http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
170 for another implementation and additional justification.
173 def wrapper(self: object, *args: object, **kwargs: object) -> object:
174 # it's the first call, replace the method with a cached, bound method
175 bound_method: CallableT = types.MethodType( # type: ignore[assignment]
178 cached_method = cache_wrapper(bound_method)
179 setattr(self, method.__name__, cached_method)
180 return cached_method(*args, **kwargs)
182 # Support cache clear even before cache has been created.
183 wrapper.cache_clear = lambda: None # type: ignore[attr-defined]
185 return ( # type: ignore[return-value]
186 _special_method_cache(method, cache_wrapper) or wrapper
190 def _special_method_cache(method, cache_wrapper):
192 Because Python treats special methods differently, it's not
193 possible to use instance attributes to implement the cached
196 Instead, install the wrapper method under a different name
197 and return a simple proxy to that wrapper.
199 https://github.com/jaraco/jaraco.functools/issues/5
201 name = method.__name__
202 special_names = '__getattr__', '__getitem__'
203 if name not in special_names:
206 wrapper_name = '__cached' + name
208 def proxy(self, *args, **kwargs):
209 if wrapper_name not in vars(self):
210 bound = types.MethodType(method, self)
211 cache = cache_wrapper(bound)
212 setattr(self, wrapper_name, cache)
214 cache = getattr(self, wrapper_name)
215 return cache(*args, **kwargs)
220 def apply(transform):
222 Decorate a function with a transform function that is
223 invoked on results returned from the decorated function.
226 ... def get_numbers(start):
227 ... "doc for get_numbers"
228 ... return range(start, start+3)
229 >>> list(get_numbers(4))
231 >>> get_numbers.__doc__
232 'doc for get_numbers'
236 return functools.wraps(func)(compose(transform, func))
241 def result_invoke(action):
243 Decorate a function with an action function that is
244 invoked on the results returned from the decorated
245 function (for its side-effect), then return the original
248 >>> @result_invoke(print)
249 ... def add_two(a, b):
251 >>> x = add_two(2, 3)
258 @functools.wraps(func)
259 def wrapper(*args, **kwargs):
260 result = func(*args, **kwargs)
269 def call_aside(f, *args, **kwargs):
271 Call a function for its side effect after initialization.
274 ... def func(): print("called")
279 Use functools.partial to pass parameters to the initial call
281 >>> @functools.partial(call_aside, name='bingo')
282 ... def func(name): print("called with", name)
291 Rate-limit a function (or other callable)
294 def __init__(self, func, max_rate=float('Inf')):
295 if isinstance(func, Throttler):
298 self.max_rate = max_rate
304 def __call__(self, *args, **kwargs):
306 return self.func(*args, **kwargs)
309 "ensure at least 1/max_rate seconds from last call"
310 elapsed = time.time() - self.last_called
311 must_wait = 1 / self.max_rate - elapsed
312 time.sleep(max(0, must_wait))
313 self.last_called = time.time()
315 def __get__(self, obj, type=None):
316 return first_invoke(self._wait, functools.partial(self.func, obj))
319 def first_invoke(func1, func2):
321 Return a function that when invoked will invoke func1 without
322 any parameters (for its side-effect) and then invoke func2
323 with whatever parameters were passed, returning its result.
326 def wrapper(*args, **kwargs):
328 return func2(*args, **kwargs)
333 def retry_call(func, cleanup=lambda: None, retries=0, trap=()):
335 Given a callable func, trap the indicated exceptions
336 for up to 'retries' times, invoking cleanup on the
337 exception. On the final attempt, allow any exceptions
340 attempts = itertools.count() if retries == float('inf') else range(retries)
341 for attempt in attempts:
350 def retry(*r_args, **r_kwargs):
352 Decorator wrapper for retry_call. Accepts arguments to retry_call
353 except func and then returns a decorator for the decorated function.
357 >>> @retry(retries=3)
358 ... def my_func(a, b):
359 ... "this is my funk"
366 @functools.wraps(func)
367 def wrapper(*f_args, **f_kwargs):
368 bound = functools.partial(func, *f_args, **f_kwargs)
369 return retry_call(bound, *r_args, **r_kwargs)
376 def print_yielded(func):
378 Convert a generator into a function that prints all yielded elements
382 ... yield 3; yield None
387 print_all = functools.partial(map, print)
388 print_results = compose(more_itertools.consume, print_all, func)
389 return functools.wraps(func)(print_results)
394 Wrap func so it's not called if its first param is None
396 >>> print_text = pass_none(print)
397 >>> print_text('text')
402 @functools.wraps(func)
403 def wrapper(param, *args, **kwargs):
404 if param is not None:
405 return func(param, *args, **kwargs)
410 def assign_params(func, namespace):
412 Assign parameters from namespace where func solicits.
414 >>> def func(x, y=3):
416 >>> assigned = assign_params(func, dict(x=2, z=4))
420 The usual errors are raised if a function doesn't receive
421 its required parameters:
423 >>> assigned = assign_params(func, dict(y=3, z=4))
425 Traceback (most recent call last):
426 TypeError: func() ...argument...
428 It even works on methods:
431 ... def meth(self, arg):
433 >>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))()
436 sig = inspect.signature(func)
437 params = sig.parameters.keys()
438 call_ns = {k: namespace[k] for k in params if k in namespace}
439 return functools.partial(func, **call_ns)
442 def save_method_args(method):
444 Wrap a method such that when it is called, the args and kwargs are
448 ... @save_method_args
449 ... def method(self, a, b):
451 >>> my_ob = MyClass()
452 >>> my_ob.method(1, 2)
454 >>> my_ob._saved_method.args
456 >>> my_ob._saved_method.kwargs
458 >>> my_ob.method(a=3, b='foo')
460 >>> my_ob._saved_method.args
462 >>> my_ob._saved_method.kwargs == dict(a=3, b='foo')
465 The arguments are stored on the instance, allowing for
466 different instance to save different args.
468 >>> your_ob = MyClass()
469 >>> your_ob.method({str('x'): 3}, b=[4])
471 >>> your_ob._saved_method.args
473 >>> my_ob._saved_method.args
476 args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs')
478 @functools.wraps(method)
479 def wrapper(self, *args, **kwargs):
480 attr_name = '_saved_' + method.__name__
481 attr = args_and_kwargs(args, kwargs)
482 setattr(self, attr_name, attr)
483 return method(self, *args, **kwargs)
488 def except_(*exceptions, replace=None, use=None):
490 Replace the indicated exceptions, if raised, with the indicated
491 literal replacement or evaluated expression (if present).
493 >>> safe_int = except_(ValueError)(int)
498 Specify a literal replacement with ``replace``.
500 >>> safe_int_r = except_(ValueError, replace=0)(int)
501 >>> safe_int_r('five')
504 Provide an expression to ``use`` to pass through particular parameters.
506 >>> safe_int_pt = except_(ValueError, use='args[0]')(int)
507 >>> safe_int_pt('five')
513 @functools.wraps(func)
514 def wrapper(*args, **kwargs):
516 return func(*args, **kwargs)