summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api/request.rst55
-rw-r--r--pyramid/request.py3
-rw-r--r--pyramid/tests/test_request.py18
-rw-r--r--pyramid/tests/test_util.py108
-rw-r--r--pyramid/util.py83
5 files changed, 266 insertions, 1 deletions
diff --git a/docs/api/request.rst b/docs/api/request.rst
index 642e6c84f..9596e5621 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -8,6 +8,10 @@
.. autoclass:: Request
:members:
:inherited-members:
+ :exclude-members: add_response_callback, add_finished_callback,
+ route_url, route_path, current_route_url,
+ current_route_path, static_url, static_path,
+ model_url, resource_url, set_property
.. attribute:: context
@@ -204,6 +208,57 @@
body associated with this request, this property will raise an
exception. See also :ref:`request_json_body`.
+ .. method:: set_property(func, name=None, reify=False)
+
+ .. versionadded:: 1.3
+
+ Add a callable or a property descriptor to the request instance.
+
+ Properties, unlike attributes, are lazily evaluated by executing
+ an underlying callable when accessed. They can be useful for
+ adding features to an object without any cost if those features
+ go unused.
+
+ A property may also be reified via the
+ :class:`pyramid.decorator.reify` decorator by setting
+ ``reify=True``, allowing the result of the evaluation to be
+ cached. Thus the value of the property is only computed once for
+ the lifetime of the object.
+
+ ``func`` can either be a callable that accepts the request as
+ its single positional parameter, or it can be a property
+ descriptor.
+
+ If the ``func`` is a property descriptor a ``ValueError`` will
+ be raised if ``name`` is ``None`` or ``reify`` is ``True``.
+
+ If ``name`` is None, the name of the property will be computed
+ from the name of the ``func``.
+
+ .. code-block:: python
+ :linenos:
+
+ def _connect(request):
+ conn = request.registry.dbsession()
+ def cleanup(_):
+ conn.close()
+ request.add_finished_callback(cleanup)
+ return conn
+
+ @subscriber(NewRequest)
+ def new_request(event):
+ request = event.request
+ request.set_property(_connect, 'db', reify=True)
+
+ The subscriber doesn't actually connect to the database, it just
+ provides the API which, when accessed via ``request.db``, will
+ create the connection. Thanks to reify, only one connection is
+ made per-request even if ``request.db`` is accessed many times.
+
+ This pattern provides a way to augment the ``request`` object
+ without having to subclass it, which can be useful for extension
+ authors.
+
.. note::
For information about the API of a :term:`multidict` structure (such as
diff --git a/pyramid/request.py b/pyramid/request.py
index 4005213fa..c15ed7d8e 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -26,6 +26,7 @@ from pyramid.compat import (
from pyramid.decorator import reify
from pyramid.response import Response
from pyramid.url import URLMethodsMixin
+from pyramid.util import InstancePropertyMixin
class TemplateContext(object):
pass
@@ -301,7 +302,7 @@ class CallbackMethodsMixin(object):
@implementer(IRequest)
class Request(BaseRequest, DeprecatedRequestMethodsMixin, URLMethodsMixin,
- CallbackMethodsMixin):
+ CallbackMethodsMixin, InstancePropertyMixin):
"""
A subclass of the :term:`WebOb` Request class. An instance of
this class is created by the :term:`router` and is provided to a
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index 546f670c0..10cda96d8 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -267,6 +267,24 @@ class TestRequest(unittest.TestCase):
request = self._makeOne({'REQUEST_METHOD':'GET'})
self.assertRaises(ValueError, getattr, request, 'json_body')
+ def test_set_property(self):
+ request = self._makeOne({})
+ opts = [2, 1]
+ def connect(obj):
+ return opts.pop()
+ request.set_property(connect, name='db')
+ self.assertEqual(1, request.db)
+ self.assertEqual(2, request.db)
+
+ def test_set_property_reify(self):
+ request = self._makeOne({})
+ opts = [2, 1]
+ def connect(obj):
+ return opts.pop()
+ request.set_property(connect, name='db', reify=True)
+ self.assertEqual(1, request.db)
+ self.assertEqual(1, request.db)
+
class TestRequestDeprecatedMethods(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index b9a9d1960..824ee329f 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -1,6 +1,114 @@
import unittest
from pyramid.compat import PY3
+class Test_InstancePropertyMixin(unittest.TestCase):
+ def _makeOne(self):
+ cls = self._targetClass()
+ class Foo(cls):
+ pass
+ return Foo()
+
+ def _targetClass(self):
+ from pyramid.util import InstancePropertyMixin
+ return InstancePropertyMixin
+
+ def test_callable(self):
+ def worker(obj):
+ return obj.bar
+ foo = self._makeOne()
+ foo.set_property(worker)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(2, foo.worker)
+
+ def test_callable_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = self._makeOne()
+ foo.set_property(worker, name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_callable_with_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = self._makeOne()
+ foo.set_property(worker, reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(1, foo.worker)
+
+ def test_callable_with_name_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = self._makeOne()
+ foo.set_property(worker, name='x')
+ foo.set_property(worker, name='y', reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.y)
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+ self.assertEqual(1, foo.y)
+
+ def test_property_without_name(self):
+ def worker(obj): pass
+ foo = self._makeOne()
+ self.assertRaises(ValueError, foo.set_property, property(worker))
+
+ def test_property_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = self._makeOne()
+ foo.set_property(property(worker), name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_property_with_reify(self):
+ def worker(obj): pass
+ foo = self._makeOne()
+ self.assertRaises(ValueError, foo.set_property,
+ property(worker), name='x', reify=True)
+
+ def test_override_property(self):
+ def worker(obj): pass
+ foo = self._makeOne()
+ foo.set_property(worker, name='x')
+ def doit():
+ foo.x = 1
+ self.assertRaises(AttributeError, doit)
+
+ def test_override_reify(self):
+ def worker(obj): pass
+ foo = self._makeOne()
+ foo.set_property(worker, name='x', reify=True)
+ foo.x = 1
+ self.assertEqual(1, foo.x)
+ foo.x = 2
+ self.assertEqual(2, foo.x)
+
+ def test_reset_property(self):
+ foo = self._makeOne()
+ foo.set_property(lambda _: 1, name='x')
+ self.assertEqual(1, foo.x)
+ foo.set_property(lambda _: 2, name='x')
+ self.assertEqual(2, foo.x)
+
+ def test_reset_reify(self):
+ """ This is questionable behavior, but may as well get notified
+ if it changes."""
+ foo = self._makeOne()
+ foo.set_property(lambda _: 1, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+ foo.set_property(lambda _: 2, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+
class Test_WeakOrderedSet(unittest.TestCase):
def _makeOne(self):
from pyramid.config import WeakOrderedSet
diff --git a/pyramid/util.py b/pyramid/util.py
index 76968bbbd..852689c4d 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -14,6 +14,89 @@ class DottedNameResolver(_DottedNameResolver):
def __init__(self, package=None): # default to package = None for bw compat
return _DottedNameResolver.__init__(self, package)
+class InstancePropertyMixin(object):
+ """ Mixin that will allow an instance to add properties at
+ run-time as if they had been defined via @property or @reify
+ on the class itself.
+ """
+
+ def set_property(self, func, name=None, reify=False):
+ """ Add a callable or a property descriptor to the instance.
+
+ Properties, unlike attributes, are lazily evaluated by executing
+ an underlying callable when accessed. They can be useful for
+ adding features to an object without any cost if those features
+ go unused.
+
+ A property may also be reified via the
+ :class:`pyramid.decorator.reify` decorator by setting
+ ``reify=True``, allowing the result of the evaluation to be
+ cached. Thus the value of the property is only computed once for
+ the lifetime of the object.
+
+ ``func`` can either be a callable that accepts the instance as
+ its single positional parameter, or it can be a property
+ descriptor.
+
+ If the ``func`` is a property descriptor, the ``name`` parameter
+ must be supplied or a ``ValueError`` will be raised. Also note
+ that a property descriptor cannot be reified, so ``reify`` must
+ be ``False``.
+
+ If ``name`` is None, the name of the property will be computed
+ from the name of the ``func``.
+
+ .. code-block:: python
+ :linenos:
+
+ class Foo(InstancePropertyMixin):
+ _x = 1
+
+ def _get_x(self):
+ return _x
+
+ def _set_x(self, value):
+ self._x = value
+
+ foo = Foo()
+ foo.set_property(property(_get_x, _set_x), name='x')
+ foo.set_property(_get_x, name='y', reify=True)
+
+ >>> foo.x
+ 1
+ >>> foo.y
+ 1
+ >>> foo.x = 5
+ >>> foo.x
+ 5
+ >>> foo.y # notice y keeps the original value
+ 1
+ """
+
+ is_property = isinstance(func, property)
+ if is_property:
+ fn = func
+ 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: func(this)
+ fn.__name__ = name
+ fn.__doc__ = func.__doc__
+ else:
+ name = func.__name__
+ fn = func
+ if reify:
+ import pyramid.decorator
+ fn = pyramid.decorator.reify(fn)
+ elif not is_property:
+ fn = property(fn)
+ attrs = { name: fn }
+ parent = self.__class__
+ cls = type(parent.__name__, (parent, object), attrs)
+ self.__class__ = cls
+
class WeakOrderedSet(object):
""" Maintain a set of items.