diff options
| author | Michael Merickel <michael@merickel.org> | 2020-01-09 22:53:46 -0600 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2020-01-09 22:53:46 -0600 |
| commit | 68c1929bbfec92f3fff1985226d88f41b02e5a4f (patch) | |
| tree | 99d1d6c463da8e36d0d7a9ddd60938eff1930f2d /src | |
| parent | 9ba492843ef8685b5493516a9c740b6aa5a9e55a (diff) | |
| parent | 912bccb8b715b0249c2c23736c467eaee14a4e3b (diff) | |
| download | pyramid-68c1929bbfec92f3fff1985226d88f41b02e5a4f.tar.gz pyramid-68c1929bbfec92f3fff1985226d88f41b02e5a4f.tar.bz2 pyramid-68c1929bbfec92f3fff1985226d88f41b02e5a4f.zip | |
Merge branch 'master' into security-docs
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 | ||||
| -rw-r--r-- | src/pyramid/util.py | 57 |
4 files changed, 188 insertions, 17 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( diff --git a/src/pyramid/util.py b/src/pyramid/util.py index e552b37de..504516631 100644 --- a/src/pyramid/util.py +++ b/src/pyramid/util.py @@ -73,6 +73,26 @@ def as_sorted_tuple(val): return val +class SettableProperty(object): + def __init__(self, wrapped): + self.wrapped = wrapped + functools.update_wrapper(self, wrapped) + + def __get__(self, obj, type=None): + if obj is None: # pragma: no cover + return self + value = obj.__dict__.get(self.wrapped.__name__, _marker) + if value is _marker: + value = self.wrapped(obj) + return value + + def __set__(self, obj, value): + obj.__dict__[self.wrapped.__name__] = value + + def __delete__(self, obj): + del obj.__dict__[self.wrapped.__name__] + + class InstancePropertyHelper(object): """A helper object for assigning properties and descriptors to instances. It is not normally possible to do this because descriptors must be @@ -94,26 +114,29 @@ class InstancePropertyHelper(object): (name, property) pair. """ - is_property = isinstance(callable, property) - if is_property: - fn = callable - if name is None: - raise ValueError('must specify "name" for a property') - if reify: - raise ValueError('cannot reify a property') - elif name is not None: - fn = lambda this: callable(this) - fn.__name__ = get_callable_name(name) - fn.__doc__ = callable.__doc__ - else: + if name is None: + if not hasattr(callable, '__name__'): + raise ValueError( + 'missing __name__, must specify "name" for property' + ) name = callable.__name__ + name = get_callable_name(name) + is_data_descriptor = hasattr(callable, '__set__') + if reify and is_data_descriptor: + raise ValueError('cannot reify a data descriptor') + if is_data_descriptor: fn = callable - if reify: - import pyramid.decorator # avoid circular import + else: + wrapped = lambda this: callable(this) + wrapped.__name__ = name + wrapped.__doc__ = callable.__doc__ + + if reify: + import pyramid.decorator # avoid circular import - fn = pyramid.decorator.reify(fn) - elif not is_property: - fn = property(fn) + fn = pyramid.decorator.reify(wrapped) + else: + fn = SettableProperty(wrapped) return name, fn |
