diff options
| author | Michael Merickel <michael@merickel.org> | 2020-01-09 12:20:22 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-01-09 12:20:22 -0600 |
| commit | 912bccb8b715b0249c2c23736c467eaee14a4e3b (patch) | |
| tree | 8c8b05a388418b54d43589c042777f6bd25f7a5a /src | |
| parent | 5702c3c3a4357a6071c9ba624a89655209548336 (diff) | |
| parent | f04c06c1de47373e51f6fb1b5dc1330b3df58299 (diff) | |
| download | pyramid-912bccb8b715b0249c2c23736c467eaee14a4e3b.tar.gz pyramid-912bccb8b715b0249c2c23736c467eaee14a4e3b.tar.bz2 pyramid-912bccb8b715b0249c2c23736c467eaee14a4e3b.zip | |
Merge pull request #3561 from mmerickel/request-local-cache
Request local cache
Diffstat (limited to 'src')
| -rw-r--r-- | src/pyramid/paster.py | 6 | ||||
| -rw-r--r-- | src/pyramid/request.py | 134 | ||||
| -rw-r--r-- | src/pyramid/scripting.py | 8 |
3 files changed, 148 insertions, 0 deletions
diff --git a/src/pyramid/paster.py b/src/pyramid/paster.py index 22c09e41a..00c1a8915 100644 --- a/src/pyramid/paster.py +++ b/src/pyramid/paster.py @@ -107,6 +107,12 @@ def bootstrap(config_uri, request=None, options=None): Added the ability to use the return value as a context manager. + .. versionchanged:: 2.0 + + Request finished callbacks added via + :meth:`pyramid.request.Request.add_finished_callback` will be invoked + by the ``closer``. + """ app = get_app(config_uri, options=options) env = prepare(request) diff --git a/src/pyramid/request.py b/src/pyramid/request.py index d65be2a2f..62bd22589 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,134 @@ 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. + + Wrapping Functions + + Instantiate and use it to decorate functions that accept a request + parameter. The result is cached and returned in subsequent invocations + of the function. + + .. code-block:: python + + @RequestLocalCache() + def get_user(request): + result = ... # do some expensive computations + return result + + value = get_user(request) + + # manipulate the cache directly + get_user.cache.clear(request) + + The cache instance is attached to the resulting function as the ``cache`` + attribute such that the function may be used to manipulate the cache. + + Wrapping Methods + + A method can be used as the creator function but it needs to be bound to + an instance such that it only accepts one argument - the request. An easy + way to do this is to bind the creator in the constructor and then use + :meth:`.get_or_create`: + + .. code-block:: python + + class SecurityPolicy: + def __init__(self): + self.identity_cache = RequestLocalCache(self.load_identity) + + def load_identity(self, request): + result = ... # do some expensive computations + return result + + def authenticated_identity(self, request): + return self.identity_cache.get_or_create(request) + + The cache maintains a weakref to each request and will release the cached + values when the request is garbage-collected. However, in most scenarios, + it will release resources earlier via + :meth:`pyramid.request.Request.add_finished_callback`. + + .. versionadded:: 2.0 + + """ + + NO_VALUE = Sentinel('NO_VALUE') + + def __init__(self, creator=None): + self._store = weakref.WeakKeyDictionary() + self._creator = creator + + def __call__(self, fn): + @functools.wraps(fn) + def wrapper(request): + return wrapper.cache.get_or_create(request, fn) + + wrapper.cache = self + self._creator = fn + return wrapper + + def get_or_create(self, request, creator=None): + """ + Return the value from the cache. Compute if necessary. + + If no value is cached then execute the creator, cache the result, + and return it. + + The creator may be passed in as an argument or bound to the cache + by decorating a function or supplied as a constructor argument. + + """ + result = self._store.get(request, self.NO_VALUE) + if result is self.NO_VALUE: + if creator is None: + creator = self._creator + if creator is None: + raise ValueError( + 'no creator function has been registered with the ' + 'cache or supplied to "get_or_create"' + ) + result = creator(request) + self.set(request, result) + return result + + 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. + + """ + already_set = request in self._store + self._store[request] = value + + # avoid registering the callback more than once + if not already_set: + request.add_finished_callback(self._store.pop) + + def clear(self, request): + """ + Delete the value from the cache. + + The cached value is returned or :attr:`.NO_VALUE`. + + """ + old_value = self.NO_VALUE + if request in self._store: + old_value = self._store[request] + + # keep a value in the store so that we don't register another + # finished callback when set is invoked + self._store[request] = self.NO_VALUE + return old_value diff --git a/src/pyramid/scripting.py b/src/pyramid/scripting.py index abcdd1030..bf23f1008 100644 --- a/src/pyramid/scripting.py +++ b/src/pyramid/scripting.py @@ -74,6 +74,12 @@ def prepare(request=None, registry=None): Added the ability to use the return value as a context manager. + .. versionchanged:: 2.0 + + Request finished callbacks added via + :meth:`pyramid.request.Request.add_finished_callback` will be invoked + by the ``closer``. + """ if registry is None: registry = getattr(request, 'registry', global_registries.last) @@ -94,6 +100,8 @@ def prepare(request=None, registry=None): apply_request_extensions(request) def closer(): + if request.finished_callbacks: + request._process_finished_callbacks() ctx.end() root_factory = registry.queryUtility( |
