summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst10
-rw-r--r--src/pyramid/request.py111
-rw-r--r--tests/test_request.py100
3 files changed, 216 insertions, 5 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 70f5bbb64..e93f8a813 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -82,6 +82,16 @@ Features
invoked as part of ``pyramid.scripting.prepare`` and
``pyramid.paster.boostrap``.
+- Added ``pyramid.request.RequestLocalCache`` which can be used to create
+ simple objects that are shared across requests and can be used to store
+ per-request data. This is useful when the source of data is external to
+ the request itself. Often a reified property is used on a request via
+ ``pyramid.config.Configurator.add_request_method``, or
+ ``pyramid.decorator.reify``, and these work great when the data is
+ generated on-demand when accessing the request property. However, often
+ the case is that the data is generated when accessing some other system
+ and then we want to cache the data for the duration of the request.
+
Deprecations
------------
diff --git a/src/pyramid/request.py b/src/pyramid/request.py
index d65be2a2f..83134666d 100644
--- a/src/pyramid/request.py
+++ b/src/pyramid/request.py
@@ -1,4 +1,6 @@
from collections import deque
+import functools
+import weakref
from webob import BaseRequest
from zope.interface import implementer
from zope.interface.interface import InterfaceClass
@@ -17,6 +19,7 @@ from pyramid.url import URLMethodsMixin
from pyramid.util import (
InstancePropertyHelper,
InstancePropertyMixin,
+ Sentinel,
bytes_,
text_,
)
@@ -330,3 +333,111 @@ def apply_request_extensions(request, extensions=None):
InstancePropertyHelper.apply_properties(
request, extensions.descriptors
)
+
+
+class RequestLocalCache:
+ """
+ A store that caches values during for the lifecycle of a request.
+
+ Instantiate a cache object and use it to decorate methods or functions
+ that accept a request parameter. Any other arguments are not used as
+ cache keys so make sure they are constant across calls per-request.
+
+ Wrapping methods:
+
+ .. code-block:: python
+
+ class SecurityPolicy:
+ identity_cache = RequestLocalCache()
+
+ @identity_cache
+ def authenticated_identity(self, request):
+ result = ... # do some expensive computations
+ return result
+
+ Wrapping functions:
+
+ .. code-block:: python
+
+ user_cache = RequestLocalCache()
+
+ @user_cache
+ def get_user(request):
+ result = ... # do some expensive computations
+ return result
+
+ It's also possible to inspect and influence the cache during runtime using
+ :meth:`.get`, :meth:`.set` and :meth:`.clear`. Using these methods, the
+ cache can be used directly as well, without using it as a decorator.
+
+ The cache will clean release resources aggressively by utilizing
+ :meth:`pyramid.request.Request.add_finished_callback`, but it will also
+ maintain a weakref to the request and cleanup when it is garbage collected
+ if the callbacks are not invoked for some reason.
+
+ .. versionadded:: 2.0
+
+ """
+ NO_VALUE = Sentinel('NO_VALUE')
+
+ def __init__(self):
+ self.store = weakref.WeakKeyDictionary()
+
+ def auto_adapt(decorator):
+ class FunctionOrMethodAdapter:
+ def __init__(self, inst, func):
+ self.inst = inst
+ self.__wrapped__ = func
+
+ def __call__(self, *args, **kwargs):
+ return decorator(self.inst, self.__wrapped__)(*args, **kwargs)
+
+ def __get__(self, instance, owner):
+ return decorator(self.inst, self.__wrapped__.__get__(instance, owner))
+
+ def adapt(inst, fn):
+ return FunctionOrMethodAdapter(inst, fn)
+ return adapt
+
+ @auto_adapt
+ def __call__(self, fn):
+ """
+ Decorate a method or function.
+
+ """
+ @functools.wraps(fn)
+ def wrapper(request, *args, **kwargs):
+ result = self.get(request)
+ if result is self.NO_VALUE:
+ result = fn(request, *args, **kwargs)
+ self.set(request, result)
+ request.add_finished_callback(self.clear)
+ return result
+ return wrapper
+
+ del auto_adapt
+
+ def clear(self, request):
+ """
+ Delete the value from the cache.
+
+ The cached value is returned or :attr:`.NO_VALUE`.
+
+ """
+ return self.store.pop(request, self.NO_VALUE)
+
+ def get(self, request, default=NO_VALUE):
+ """
+ Return the value from the cache.
+
+ The cached value is returned or ``default``.
+
+ """
+ return self.store.get(request, default)
+
+ def set(self, request, value):
+ """
+ Update the cache with a new value.
+
+ """
+ self.store[request] = value
diff --git a/tests/test_request.py b/tests/test_request.py
index bbf6aa47c..3cff4bb53 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -535,10 +535,6 @@ class Test_apply_request_extensions(unittest.TestCase):
self.assertEqual(request.foo('abc'), 'abc')
-class Dummy(object):
- pass
-
-
class Test_subclassing_Request(unittest.TestCase):
def test_subclass(self):
from pyramid.interfaces import IRequest
@@ -598,14 +594,108 @@ class Test_subclassing_Request(unittest.TestCase):
self.assertTrue(IRequest.implementedBy(RequestSub))
+class TestRequestLocalCache(unittest.TestCase):
+ def _makeOne(self):
+ from pyramid.request import RequestLocalCache
+
+ return RequestLocalCache()
+
+ def test_it_works_with_functions(self):
+ cache = self._makeOne()
+ a = [0]
+
+ @cache
+ def foo(request):
+ a[0] += 1
+ return a[0]
+
+ req1 = DummyRequest()
+ req2 = DummyRequest()
+ self.assertEqual(foo(req1), 1)
+ self.assertEqual(foo(req2), 2)
+ self.assertEqual(foo(req1), 1)
+ self.assertEqual(foo(req2), 2)
+ self.assertEqual(len(req1.finished_callbacks), 1)
+ self.assertEqual(len(req2.finished_callbacks), 1)
+
+ def test_it_works_with_methods(self):
+ cache = self._makeOne()
+ a = [0]
+
+ class DummyPolicy:
+ @cache
+ def foo(self, request):
+ a[0] += 1
+ return a[0]
+
+ policy = DummyPolicy()
+ req1 = DummyRequest()
+ req2 = DummyRequest()
+ self.assertEqual(policy.foo(req1), 1)
+ self.assertEqual(policy.foo(req2), 2)
+ self.assertEqual(policy.foo(req1), 1)
+ self.assertEqual(policy.foo(req2), 2)
+ self.assertEqual(len(req1.finished_callbacks), 1)
+ self.assertEqual(len(req2.finished_callbacks), 1)
+
+ def test_clear_works(self):
+ cache = self._makeOne()
+ a = [0]
+
+ @cache
+ def foo(request):
+ a[0] += 1
+ return a[0]
+
+ req = DummyRequest()
+ self.assertEqual(foo(req), 1)
+ self.assertEqual(len(req.finished_callbacks), 1)
+ cache.clear(req)
+ self.assertEqual(foo(req), 2)
+ self.assertEqual(len(req.finished_callbacks), 2)
+
+ def test_set_overrides_current_value(self):
+ cache = self._makeOne()
+ a = [0]
+
+ @cache
+ def foo(request):
+ a[0] += 1
+ return a[0]
+
+ req = DummyRequest()
+ self.assertEqual(foo(req), 1)
+ self.assertEqual(len(req.finished_callbacks), 1)
+ cache.set(req, 8)
+ self.assertEqual(foo(req), 8)
+ self.assertEqual(len(req.finished_callbacks), 1)
+ self.assertEqual(cache.get(req), 8)
+
+ def test_get_works(self):
+ cache = self._makeOne()
+ req = DummyRequest()
+ self.assertIs(cache.get(req), cache.NO_VALUE)
+ cache.set(req, 2)
+ self.assertIs(cache.get(req), 2)
+
+
+class Dummy(object):
+ pass
+
+
class DummyRequest(object):
def __init__(self, environ=None):
if environ is None:
environ = {}
self.environ = environ
+ self.response_callbacks = []
+ self.finished_callbacks = []
def add_response_callback(self, callback):
- self.response_callbacks = [callback]
+ self.response_callbacks.append(callback)
+
+ def add_finished_callback(self, callback):
+ self.finished_callbacks.append(callback)
def get_response(self, app):
return app