Michael Merickel
2011-12-30 577db92fe4626e86e7c5cf4fe062205d6cf5d33b
Added the InstancePropertyMixin to the Request.

The new mixin allows adding properties to the request object which are
lazily evaluated.
4 files modified
212 ■■■■■ changed files
pyramid/request.py 3 ●●●● patch | view | raw | blame | history
pyramid/tests/test_request.py 18 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_util.py 108 ●●●●● patch | view | raw | blame | history
pyramid/util.py 83 ●●●●● patch | view | raw | blame | history
pyramid/request.py
@@ -26,6 +26,7 @@
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 @@
@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
pyramid/tests/test_request.py
@@ -267,6 +267,24 @@
        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()
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
pyramid/util.py
@@ -14,6 +14,89 @@
    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.