summaryrefslogtreecommitdiff
path: root/bps/cache.py
diff options
context:
space:
mode:
Diffstat (limited to 'bps/cache.py')
-rw-r--r--bps/cache.py370
1 files changed, 370 insertions, 0 deletions
diff --git a/bps/cache.py b/bps/cache.py
new file mode 100644
index 0000000..875276b
--- /dev/null
+++ b/bps/cache.py
@@ -0,0 +1,370 @@
+"""bps.cache -- caching tools"""
+#===================================================
+#imports
+#===================================================
+#core
+import inspect
+from functools import update_wrapper
+import time
+from warnings import warn
+#needed imports
+#legacy imports
+from bps.undef import Undef
+from bps.meta import find_attribute, decorate_per_instance, instancemethod
+#XXX: bps3.misc.finalmethod?
+#TODO: bps3.misc.AbstractMethodError - here or in bps.exc?
+
+__all__ = [
+ #cached decorators
+ 'cached_function',
+ 'cached_method',
+
+ #stateful decorators
+ 'stateful_function',
+ 'stateful_method',
+ 'is_stateful',
+]
+
+#=========================================================
+#function caching decorator
+#=========================================================
+
+def cached_function(key=None, args=None, lifetime=None, tick=time.time):
+ """decorator that caches a function's output.
+
+ This decorator creates an dictionary which caches the return values
+ of the wrapped function, so that successive calls hit the cache
+ rather than calling the function itself. This decorator
+ supports numerous features, including time-limited caching,
+ and customization how the cache key is calculated.
+
+ :param key:
+ This should be a function which takes the wrapper func's inputs,
+ and maps them to a hashable value to identify inputs for caching purposes.
+ If ``key(*args,**kwds)`` returns the ``NotImplemented`` singleton, caching will be bypassed.
+
+ :param args:
+ Alternately, instead of specifying a `key`, this option can be used
+ to specify the number of positional arguments expected, which will be formed into a tuple,
+ and used as the cache key. This option is mutually exlusive with *key*.
+
+ :param lifetime:
+ Amount of time (as measured by `tick` function)
+ before cached values should expire.
+ If lifetime is ``None`` (the default), cached values will never expire,
+ unless ``func.clear()`` is explicitly called by your application.
+
+ :param tick:
+ Function returning arbitrary objects for timestamping,
+ used by `lifetime`. By default, this uses ``time.time()``
+
+ The resulting decorated function object will have a some extra attributes:
+ ``func.key(*args,**kwds)``
+ Calling this with a set of the function's arguments
+ will return the key used to cache the result for those parameters.
+
+ ``func.clear(keys=None)``
+ Calling this will clear the internal cache of function results.
+ If *keys* is specified, only those cache keys will be cleared.
+
+ ``func.set(key,value)``
+ Allows writing to the function cache directly.
+
+ ``func.cache``
+ This is an exposed reference to the actual cache dictionary.
+ Please use this only if you *really* have to.
+
+ .. caution::
+ If your code does access the dictionary, be aware that the
+ ``lifetime`` option will change the organization of the dict from
+ ``key -> result`` to ``key -> (mtime,result)``.
+
+ A simple usage example::
+
+ >>> import time
+ >>> from bps.cache import cached_function
+ >>> #an example which has an expiring cache
+ >>> @cached_function(args=1, lifetime=2)
+ >>> def myfunc(value):
+ >>> print "myfunc called:", value
+ >>> return value*2
+ >>> #the first call won't be cached
+ >>> print "result:", myfunc(2)
+ myfunc called: 2
+ result: 4
+ >>> #but the next one will
+ >>> print "result:", myfunc(2)
+ result: 4
+ >>> #if we wait a bit and try again, the cache will expire
+ >>> time.sleep(2)
+ >>> print "result:", myfunc(2)
+ myfunc called: 2
+ result: 4
+ >>> #or we can manually flush the entire cache
+ >>> myfunc.clear()
+ >>> print "result:", myfunc(2)
+ myfunc called: 2
+ result: 4
+
+ .. seealso::
+ :func:`cached_method`
+ :func:`stateful_function`
+ """
+ if key is None:
+ if args is None:
+ warn("one of `key` or `args` will be required for cached_function() in the future, the bare version is deprecated", DeprecationWarning, stacklevel=3)
+ def key():
+ return None
+ elif args == 0:
+ def key():
+ return None
+ elif args == 1:
+ def key(value):
+ return value
+ else:
+ def key(*a):
+ if len(a) != args:
+ raise ValueError, "expected exactly %s arguments: %r" % (args, a)
+ return a
+ else:
+ assert args is None, "args and key() function are mutually exlusive"
+ assert callable(key), "key() function must be callable"
+ def builder(func):
+ #validate the function
+ if hasattr(func, "on_changed"):
+ warn("cached_function() is wrapping a function that was wrapped with stateful_function()... the opposite wrapping order is recommended for correct behavior", stacklevel=1)
+ #NOTE: why the warning?
+ # because stateful's changed() can call clear_cache(),
+ # but cache_func will hide any state changes which occur.
+ # so you want to decorator your function the other way around.
+
+ #init locals
+ cache = {}
+
+ #create wrapper...
+ if lifetime is None:
+ #...with no expiration
+ def wrapper(*args, **kwds):
+ value = key(*args, **kwds)
+ if value is NotImplemented:
+ return func(*args, **kwds)
+ elif value in cache:
+ return cache[value]
+ result = cache[value] = func(*args, **kwds)
+ return result
+ wrapper.set = cache.__setitem__ #for easy overriding of cache
+
+ else:
+ #...with predefined expiration
+ def wrapper(*args, **kwds):
+ value = key(*args, **kwds)
+ if value is NotImplemented:
+ return func(*args, **kwds)
+ now = tick()
+ if value in cache:
+ expires, result = cache[value]
+ if expires > now:
+ return result
+ result = func(*args, **kwds)
+ cache[value] = (now+lifetime, result)
+ return result
+
+ def set(key, value, expires=None):
+ if expires is None:
+ expires = tick() + lifetime
+ cache[key] = (expires, value)
+ wrapper.set = set #for easy overriding of cache
+ wrapper.tick = tick #in case it's useful
+
+ #fill in common attributes
+ def clear(keys=None):
+ if keys:
+ for key in keys:
+ if key in cache:
+ del cache[key]
+ else:
+ cache.clear()
+ wrapper.expire = clear #legacy ref, do not use
+ wrapper.clear = clear
+ wrapper.key = key #expose the key func
+ wrapper.cache = cache #for times when you really need direct cache access
+
+ #return wrapper
+ update_wrapper(wrapper, func)
+ return wrapper
+
+ return builder
+
+def cached_method(key=None, args=None, lifetime=None, tick=time.time):
+ """decorator that created instance-level cached functions.
+
+ This a wrapper for :func:`cached_function`, which is designed
+ to wrap methods, not functions, by providing a per-instance
+ caching dictionary.
+
+ The options for this are the same as :func:`cached_function`.
+
+ .. note::
+
+ By default, the *self* argument will not be present in the arguments
+ passed to the key function. That can be fixed if needed,
+ but it simplifies the current internal implementation.
+ """
+ #TODO: we use "method" binding so the .clear() etc attributes are instance specific.
+ # so "function" binding can't be used.
+ # but we could expose a bind="function" to fake things,
+ # and artificially insert *self* into the key func's arguments.
+ builder = cached_function(key=key, args=args, lifetime=lifetime, tick=tick)
+ return decorate_per_instance(builder, bind="method")
+
+#=========================================================
+#stateful decorators
+#=========================================================
+
+def stateful_function(func=None):
+ """decorator which adds methods to function allows callbacks
+ to be attached to detect when it changes it's output.
+
+ This decorator is primarily useful for functions
+ which consistently return the same value when called
+ called multiple times, but occasionally change
+ what the value is due to some internal event.
+ Examples of this include functions returning
+ filesystem listings, or in gui programming.
+
+ This decorator adds a simple callback / signalling system
+ by instrumenting the function object with the following methods:
+
+ ``func.changed()``
+ Your program should call this method after a resource has changed
+ which would alter what the function would returned.
+ It will cause all registered callbacks to be fired.
+
+ ``func.on_change(callback, data=Undef, tag=None) -> tag``
+ This attached a callback to the function,
+ Callback are called in FIFO order when ``func.changed()`` is invoked.
+ *on_change* will return a unique tag object to identify the registration
+ of your callback, for use with ``func.forget_callbacks``.
+
+ *callback*
+ This should be a function with the prototype ``callback()``,
+ or ``callback(data)`` if the *data* parameter is set.
+ *data*
+ This is an optional value passed as a positional parameter
+ to your callback.
+ *tag*
+ Optional value specifying custom tag object to use.
+ If not specified, an anonymous object will be created.
+ This option allows you to gang a bunch of callbacks
+ together on one tag, for mass-removal.
+ No restrictions are placed on the nature of the object you provide.
+
+ ``func.forget_callbacks(*tags)``
+ Remove all callbacks attached to this function using any
+ of the specified tags. Any tags that aren't found will be silently ignored.
+
+ Usage example::
+
+ >>> from bps.cache import stateful_function
+ >>> #stateful functions typically have no arguments, but they can if they want
+ >>> @stateful_function
+ >>> def getpi():
+ >>> print "getpi called"
+ >>> return 3.14159
+ >>> getpi()
+ getpi called
+ 3.14159
+ >>> #say we want to attach a callback
+ >>> def hook():
+ >>> print "divide by cucumber error, please reboot universe"
+ >>> print "pi is now:", getpi()
+ >>> getpi.on_change(hook)
+ >>> #and say pi changes value for some reason, our hook will be called..
+ >>> getpi.changed()
+ divide by cucumber error, please reboot universe
+ pi is now: 3.14159
+
+ """
+ #
+ #just return prepared builder if function isn't present
+ #
+ if func is None:
+ return stateful_function
+
+ if isinstance(func, instancemethod):
+ #instance methods have read-only attributes,
+ #so we have to create a wrapper whose attributes we _can_ set
+ orig = func
+ def func(*args, **kwds):
+ return orig(*args, **kwds)
+ update_wrapper(func, orig)
+
+ #
+ #state
+ #
+ hooks = [] #list of (tag, hook, data) callbacks
+
+ #
+ #methods
+ #
+ def on_change(hook, data=Undef, tag=None):
+ "mattaching a callback to be invoked when function state changes"
+ if tag is None: tag = object()
+ entry = (tag, hook, data)
+ hooks.append(entry)
+ return tag
+ func.on_change = on_change
+
+ #clear_cache provides integration with cachemethod() above,
+ #but cachemethod MUST BE CALLED FIRST
+ clear_cache = getattr(func, "clear", None)
+
+ ##func.changing = False
+ def changed():
+ "signal that function's state has changed, triggering callbacks"
+ if clear_cache: clear_cache()
+ ##func.changing = True
+ ##try:
+ for tag, hook, data in hooks:
+ if data is Undef: hook()
+ else: hook(data)
+ ##finally:
+ ## func.changing = False
+ func.changed = changed
+
+ def forget_callbacks(*tags):
+ "remove specified callbacks hooks"
+ pos = 0
+ while pos < len(hooks):
+ if hooks[pos][0] in tags:
+ del hooks[pos]
+ else:
+ pos += 1
+ func.forget_callbacks = forget_callbacks
+
+ #return func
+ return func
+
+def stateful_method(func=None):
+ """Per-instance version of :func:`stateful_function`.
+
+ Unlike stateful_function, all callbacks / triggers will be unique per-instance,
+ rather than shared globally. See stateful_function for more details.
+ """
+ decorator = decorate_per_instance(stateful_function, bind="method")
+ if func:
+ return decorator(func)
+ else:
+ return decorator
+
+def is_stateful(func):
+ """returns ``True`` if function has been instrumentated by @stateful_function or compatible protocol"""
+ return (
+ hasattr(func, "changed")
+ and hasattr(func, "on_change")
+ and hasattr(func, "forget_callbacks")
+ )
+
+#=========================================================
+#
+#=========================================================