diff options
author | Adrien Di Mascio <Adrien.DiMascio@logilab.fr> | 2011-10-10 15:14:47 +0200 |
---|---|---|
committer | Adrien Di Mascio <Adrien.DiMascio@logilab.fr> | 2011-10-10 15:14:47 +0200 |
commit | dca0bc8ab45d0fba854e3e0cb103ee47aa1baef0 (patch) | |
tree | 3bae703bc59900c658515fd255dd235aba037e37 | |
parent | a1af8ab0eed9033e955bf69564e0c82fbb3cb30b (diff) | |
download | logilab-common-dca0bc8ab45d0fba854e3e0cb103ee47aa1baef0.tar.gz |
[decorators] provide a @cachedproperty decorator
-rw-r--r-- | ChangeLog | 1 | ||||
-rw-r--r-- | decorators.py | 39 | ||||
-rw-r--r-- | test/unittest_decorators.py | 38 |
3 files changed, 77 insertions, 1 deletions
@@ -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() |