Skip to content

Commit

Permalink
cacheutils: (breaking change) adding the scoped argument to @cached a…
Browse files Browse the repository at this point in the history
…nd @cachedmethod (and removing selfish from cachedmethod). also fixed a bug in a cachedmethod test, as well as added docs for scoped and key arguments. all of this to fix #83.
  • Loading branch information
mahmoud committed Jul 19, 2016
1 parent 573387a commit b1cd936
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 22 deletions.
54 changes: 34 additions & 20 deletions boltons/cacheutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ class CachedFunction(object):
"""This type is used by :func:`cached`, below. Instances of this
class are used to wrap functions in caching logic.
"""
def __init__(self, func, cache, typed=False, key=None):
def __init__(self, func, cache, scoped=True, typed=False, key=None):
self.func = func
if callable(cache):
self.get_cache = cache
Expand All @@ -482,6 +482,7 @@ def __init__(self, func, cache, typed=False, key=None):
def _get_cache():
return cache
self.get_cache = _get_cache
self.scoped = scoped
self.typed = typed
self.key_func = key or make_cache_key

Expand All @@ -496,16 +497,17 @@ def __call__(self, *args, **kwargs):

def __repr__(self):
cn = self.__class__.__name__
if self.typed:
return "%s(func=%r, typed=%r)" % (cn, self.func, self.typed)
if self.typed or not self.scoped:
return ("%s(func=%r, scoped=%r, typed=%r)"
% (cn, self.func, self.scoped, self.typed))
return "%s(func=%r)" % (cn, self.func)


class CachedMethod(object):
"""Similar to :class:`CachedFunction`, this type is used by
:func:`cachedmethod` to wrap methods in caching logic.
"""
def __init__(self, func, cache, typed=False, selfish=True, key=None):
def __init__(self, func, cache, scoped=True, typed=False, key=None):
self.func = func
if isinstance(cache, basestring):
self.get_cache = attrgetter(cache)
Expand All @@ -515,30 +517,29 @@ def __init__(self, func, cache, typed=False, selfish=True, key=None):
and callable(getattr(cache, '__setitem__', None))):
raise TypeError('expected cache to be an attribute name,'
' dict-like object, or callable returning'
' a dict-like object, not %r'
% cache)
' a dict-like object, not %r' % cache)
else:
def _get_cache(obj):
return cache
self.get_cache = _get_cache
self.scoped = scoped
self.typed = typed
self.selfish = selfish
self.key_func = key or make_cache_key
self.bound_to = None

def __get__(self, obj, objtype=None):
if obj is None:
return self
cls = self.__class__
ret = cls(self.func, self.get_cache,
typed=self.typed, selfish=self.selfish)
ret = cls(self.func, self.get_cache, typed=self.typed,
scoped=self.scoped, key=self.key_func)
ret.bound_to = obj
return ret

def __call__(self, *args, **kwargs):
obj = args[0] if self.bound_to is None else self.bound_to
cache = self.get_cache(obj)
key_args = (self.bound_to,) + args if self.selfish else args
key_args = (self.bound_to, self.func) + args if self.scoped else args
key = self.key_func(key_args, kwargs, typed=self.typed)
try:
ret = cache[key]
Expand All @@ -550,14 +551,14 @@ def __call__(self, *args, **kwargs):

def __repr__(self):
cn = self.__class__.__name__
args = (cn, self.func, self.typed, self.selfish)
args = (cn, self.func, self.scoped, self.typed)
if self.bound_to is not None:
args += (self.bound_to,)
return ('<%s func=%r typed=%r selfish=%r bound_to=%r>' % args)
return ("%s(func=%r, typed=%r, selfish=%r)" % args)
return ('<%s func=%r scoped=%r typed=%r bound_to=%r>' % args)
return ("%s(func=%r, scoped=%r, typed=%r)" % args)


def cached(cache, typed=False, key=None):
def cached(cache, scoped=True, typed=False, key=None):
"""Cache any function with the cache object of your choosing. Note
that the function wrapped should take only `hashable`_ arguments.
Expand All @@ -567,6 +568,12 @@ def cached(cache, typed=False, key=None):
:class:`LRI` are good choices, but a plain :class:`dict`
can work in some cases, as well. This argument can also be
a callable which accepts no arguments and returns a mapping.
scoped (bool): Whether the function itself is part of the
cache key. ``True`` by default, different functions will
not read one another's cache entries, but can evict one
another's results. ``False`` can be useful for certain
shared cache use cases. More advanced behavior can be
produced through the *key* argument.
typed (bool): Whether to factor argument types into the cache
check. Default ``False``, setting to ``True`` causes the
cache keys for ``3`` and ``3.0`` to be considered unequal.
Expand All @@ -585,11 +592,11 @@ def cached(cache, typed=False, key=None):
"""
def cached_func_decorator(func):
return CachedFunction(func, cache, typed=typed, key=key)
return CachedFunction(func, cache, scoped=scoped, typed=typed, key=key)
return cached_func_decorator


def cachedmethod(cache, typed=False, selfish=True, key=None):
def cachedmethod(cache, scoped=True, typed=False, key=None):
"""Similar to :func:`cached`, ``cachedmethod`` is used to cache
methods based on their arguments, using any :class:`dict`-like
*cache* object.
Expand All @@ -598,12 +605,18 @@ def cachedmethod(cache, typed=False, selfish=True, key=None):
cache (str/Mapping/callable): Can be the name of an attribute
on the instance, any Mapping/:class:`dict`-like object, or
a callable which returns a Mapping.
scoped (bool): Whether the method itself and the object it is
bound to are part of the cache keys. ``True`` by default,
different methods will not read one another's cache
results. ``False`` can be useful for certain shared cache
use cases. More advanced behavior can be produced through
the *key* arguments.
typed (bool): Whether to factor argument types into the cache
check. Default ``False``, setting to ``True`` causes the
cache keys for ``3`` and ``3.0`` to be considered unequal.
selfish (bool): Whether an instance's ``self`` argument should
be considered in the cache key. Use this to share cache
objects across instances.
key (callable): A callable with a signature that matches
:func:`make_cache_key` that returns a tuple of hashable
values to be used as the key in the cache.
>>> class Lowerer(object):
... def __init__(self):
Expand All @@ -618,9 +631,10 @@ def cachedmethod(cache, typed=False, selfish=True, key=None):
'wow who could guess caching could be so neat'
>>> len(lowerer.cache)
1
"""
def cached_method_decorator(func):
return CachedMethod(func, cache, typed=typed, selfish=selfish, key=key)
return CachedMethod(func, cache, scoped=scoped, typed=typed, key=key)
return cached_method_decorator


Expand Down
23 changes: 21 additions & 2 deletions tests/test_cacheutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,25 @@ def test_cached_dec():
assert inner_func.call_count == 1
func('man door hand hook car door')
assert inner_func.call_count == 2

return


def test_unscoped_cached_dec():
lru = LRU()
inner_func = CountingCallable()
func = cached(lru)(inner_func)

other_inner_func = CountingCallable()
other_func = cached(lru)(other_inner_func)

assert inner_func.call_count == 0
func('a')
assert inner_func.call_count == 1
func('a')

other_func('a')
assert other_inner_func.call_count == 0
return


Expand Down Expand Up @@ -201,7 +220,7 @@ def hand(self, *a, **kw):
def hook(self, *a, **kw):
self.hook_count += 1

@cachedmethod('h_cache', selfish=False)
@cachedmethod('h_cache', scoped=False)
def door(self, *a, **kw):
self.door_count += 1

Expand Down Expand Up @@ -233,7 +252,7 @@ def door(self, *a, **kw):
car_two = Car(cache=lru)
assert car_two.door_count == 0
car_two.door('bob')
assert car.door_count == 0
assert car_two.door_count == 0

# try unbound for kicks
Car.door(Car(), 'bob')
Expand Down

0 comments on commit b1cd936

Please sign in to comment.