summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2020-01-09 22:53:46 -0600
committerMichael Merickel <michael@merickel.org>2020-01-09 22:53:46 -0600
commit68c1929bbfec92f3fff1985226d88f41b02e5a4f (patch)
tree99d1d6c463da8e36d0d7a9ddd60938eff1930f2d /src
parent9ba492843ef8685b5493516a9c740b6aa5a9e55a (diff)
parent912bccb8b715b0249c2c23736c467eaee14a4e3b (diff)
downloadpyramid-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.py6
-rw-r--r--src/pyramid/request.py134
-rw-r--r--src/pyramid/scripting.py8
-rw-r--r--src/pyramid/util.py57
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