summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Di Mascio <Adrien.DiMascio@logilab.fr>2011-10-10 15:14:47 +0200
committerAdrien Di Mascio <Adrien.DiMascio@logilab.fr>2011-10-10 15:14:47 +0200
commitdca0bc8ab45d0fba854e3e0cb103ee47aa1baef0 (patch)
tree3bae703bc59900c658515fd255dd235aba037e37
parenta1af8ab0eed9033e955bf69564e0c82fbb3cb30b (diff)
downloadlogilab-common-dca0bc8ab45d0fba854e3e0cb103ee47aa1baef0.tar.gz
[decorators] provide a @cachedproperty decorator
-rw-r--r--ChangeLog1
-rw-r--r--decorators.py39
-rw-r--r--test/unittest_decorators.py38
3 files changed, 77 insertions, 1 deletions
diff --git a/ChangeLog b/ChangeLog
index 5f17f3d..0a14f40 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,7 @@ ChangeLog for logilab.common
* daemon: remove unused(?) DaemonMixin class
* update compat module for callable() and method_type()
* decorators: fix monkeypatch py3k compat (closes #75290)
+* decorators: provide a @cachedproperty decorator
2011-09-08 -- 0.56.2
* daemon: call initgroups/setgid before setuid (closes #74173)
diff --git a/decorators.py b/decorators.py
index 91d7492..2bce3e8 100644
--- a/decorators.py
+++ b/decorators.py
@@ -116,6 +116,45 @@ def cached(callableobj=None, keyarg=None, **kwargs):
else:
return decorator(callableobj)
+
+class cachedproperty(object):
+ """ Provides a cached property equivalent to the stacking of
+ @cached and @property, but more efficient.
+
+ After first usage, the <property_name> becomes part of the object's
+ __dict__. Doing:
+
+ del obj.<property_name> empties the cache.
+
+ Idea taken from the pyramid_ framework and the mercurial_ project.
+
+ .. _pyramid: http://pypi.python.org/pypi/pyramid
+ .. _mercurial: http://pypi.python.org/pypi/Mercurial
+ """
+ __slots__ = ('wrapped',)
+
+ def __init__(self, wrapped):
+ try:
+ wrapped.__name__
+ except AttributeError:
+ raise TypeError('%s must have a __name__ attribute' %
+ wrapped)
+ self.wrapped = wrapped
+
+ @property
+ def __doc__(self):
+ doc = getattr(self.wrapped, '__doc__', None)
+ return ('<wrapped by the cachedproperty decorator>%s'
+ % ('\n%s' % doc if doc else ''))
+
+ def __get__(self, inst, objtype=None):
+ if inst is None:
+ return self
+ val = self.wrapped(inst)
+ setattr(inst, self.wrapped.__name__, val)
+ return val
+
+
def get_cache_impl(obj, funcname):
cls = obj.__class__
member = getattr(cls, funcname)
diff --git a/test/unittest_decorators.py b/test/unittest_decorators.py
index 355e934..49661a6 100644
--- a/test/unittest_decorators.py
+++ b/test/unittest_decorators.py
@@ -20,7 +20,8 @@
import types
from logilab.common.testlib import TestCase, unittest_main
-from logilab.common.decorators import monkeypatch, cached, clear_cache, copy_cache
+from logilab.common.decorators import (monkeypatch, cached, clear_cache,
+ copy_cache, cachedproperty)
class DecoratorsTC(TestCase):
@@ -161,5 +162,40 @@ class DecoratorsTC(TestCase):
copy_cache(foo2, 'foo', foo)
self.assertEqual(foo2._foo, {(1,): None})
+
+ def test_cachedproperty(self):
+ class Foo(object):
+ x = 0
+ @cachedproperty
+ def bar(self):
+ self.__class__.x += 1
+ return self.__class__.x
+ @cachedproperty
+ def quux(self):
+ """ some prop """
+ return 42
+
+ foo = Foo()
+ self.assertEqual(Foo.x, 0)
+ self.failIf('bar' in foo.__dict__)
+ self.assertEqual(foo.bar, 1)
+ self.failUnless('bar' in foo.__dict__)
+ self.assertEqual(foo.bar, 1)
+ self.assertEqual(foo.quux, 42)
+ self.assertEqual(Foo.bar.__doc__,
+ '<wrapped by the cachedproperty decorator>')
+ self.assertEqual(Foo.quux.__doc__,
+ '<wrapped by the cachedproperty decorator>\n some prop ')
+
+ foo2 = Foo()
+ self.assertEqual(foo2.bar, 2)
+ # make sure foo.foo is cached
+ self.assertEqual(foo.bar, 1)
+
+ class Kallable(object):
+ def __call__(self):
+ return 42
+ self.assertRaises(TypeError, cachedproperty, Kallable())
+
if __name__ == '__main__':
unittest_main()