# -*- coding: utf-8 -*- # Copyright 2018 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Functions for automatic caching of expensive function calls.""" from __future__ import print_function import functools import sys import six def MemoizedSingleCall(functor): """Decorator for simple functor targets, caching the results The functor must accept no arguments beyond either a class or self (depending on if this is used in a classmethod/instancemethod context). Results of the wrapped method will be written to the class/instance namespace in a specially named cached value. All future invocations will just reuse that value. Note that this cache is per-process, so sibling and parent processes won't notice updates to the cache. """ # TODO(build): Should we rebase to snakeoil.klass.cached* functionality? # pylint: disable=protected-access @functools.wraps(functor) def wrapper(obj): key = wrapper._cache_key val = getattr(obj, key, None) if val is None: val = functor(obj) setattr(obj, key, val) return val # Use name mangling to store the cached value in a (hopefully) unique place. wrapper._cache_key = '_%s_cached' % (functor.__name__.lstrip('_'),) return wrapper def Memoize(f): """Decorator for memoizing a function. Caches all calls to the function using a ._memo_cache dict mapping (args, kwargs) to the results of the first function call with those args and kwargs. If any of args or kwargs are not hashable, trying to store them in a dict will cause a ValueError. Note that this cache is per-process, so sibling and parent processes won't notice updates to the cache. """ # pylint: disable=protected-access f._memo_cache = {} @functools.wraps(f) def wrapper(*args, **kwargs): # Make sure that the key is hashable... as long as the contents of args and # kwargs are hashable. # TODO(phobbs) we could add an option to use the id(...) of an object if # it's not hashable. Then "MemoizedSingleCall" would be obsolete. key = (tuple(args), tuple(sorted(kwargs.items()))) if key in f._memo_cache: return f._memo_cache[key] result = f(*args, **kwargs) f._memo_cache[key] = result return result return wrapper def SafeRun(functors, combine_exceptions=False): """Executes a list of functors, continuing on exceptions. Args: functors: An iterable of functors to call. combine_exceptions: If set, and multiple exceptions are encountered, SafeRun will raise a RuntimeError containing a list of all the exceptions. If only one exception is encountered, then the default behavior of re-raising the original exception with unmodified stack trace will be kept. Raises: The first exception encountered, with corresponding backtrace, unless |combine_exceptions| is specified and there is more than one exception encountered, in which case a RuntimeError containing a list of all the exceptions that were encountered is raised. """ errors = [] for f in functors: try: f() except Exception as e: # Append the exception object and the traceback. errors.append((e, sys.exc_info()[2])) if errors: if len(errors) == 1 or not combine_exceptions: # To preserve the traceback. inst, tb = errors[0] six.reraise(inst, None, tb) else: raise RuntimeError([e[0] for e in errors])