diff options
| author | Michael Merickel <michael@merickel.org> | 2011-12-30 02:08:42 -0600 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2011-12-30 02:08:42 -0600 |
| commit | d16bddbd55a05bd0bd3414b4de90de198844dc4f (patch) | |
| tree | 1d7af21918f74fe5bfb9a92dd4df3f4aea531579 | |
| parent | c338e2111472a2daa4b3b01e43c804f0057b92d8 (diff) | |
| parent | 79a11ce169305ed32c4f3fa887450320ed837e94 (diff) | |
| download | pyramid-d16bddbd55a05bd0bd3414b4de90de198844dc4f.tar.gz pyramid-d16bddbd55a05bd0bd3414b4de90de198844dc4f.tar.bz2 pyramid-d16bddbd55a05bd0bd3414b4de90de198844dc4f.zip | |
Merge branch 'feature.lazy-request-properties' into 1.3-branch
| -rw-r--r-- | docs/api/request.rst | 55 | ||||
| -rw-r--r-- | pyramid/request.py | 3 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 18 | ||||
| -rw-r--r-- | pyramid/tests/test_util.py | 108 | ||||
| -rw-r--r-- | pyramid/util.py | 83 |
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. |
