summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api/request.rst1
-rw-r--r--pyramid/config/factories.py4
-rw-r--r--pyramid/interfaces.py3
-rw-r--r--pyramid/request.py26
-rw-r--r--pyramid/router.py3
-rw-r--r--pyramid/scripting.py8
-rw-r--r--pyramid/tests/test_request.py45
-rw-r--r--pyramid/tests/test_router.py8
-rw-r--r--pyramid/tests/test_scripting.py16
-rw-r--r--pyramid/tests/test_util.py236
-rw-r--r--pyramid/util.py75
11 files changed, 321 insertions, 104 deletions
diff --git a/docs/api/request.rst b/docs/api/request.rst
index dd68fa09c..b325ad076 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -369,3 +369,4 @@
that used as ``request.GET``, ``request.POST``, and ``request.params``),
see :class:`pyramid.interfaces.IMultiDict`.
+.. autofunction:: apply_request_extensions(request)
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index 10678df55..f0b6252ae 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -14,8 +14,8 @@ from pyramid.traversal import DefaultRootFactory
from pyramid.util import (
action_method,
- InstancePropertyMixin,
get_callable_name,
+ InstancePropertyHelper,
)
@@ -174,7 +174,7 @@ class FactoriesConfiguratorMixin(object):
property = property or reify
if property:
- name, callable = InstancePropertyMixin._make_property(
+ name, callable = InstancePropertyHelper.make_property(
callable, name=name, reify=reify)
elif name is None:
name = callable.__name__
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 0f1b4efc3..d7422bdde 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -591,8 +591,7 @@ class IResponseFactory(Interface):
class IRequestFactory(Interface):
""" A utility which generates a request """
def __call__(environ):
- """ Return an object implementing IRequest, e.g. an instance
- of ``pyramid.request.Request``"""
+ """ Return an instance of ``pyramid.request.Request``"""
def blank(path):
""" Return an empty request object (see
diff --git a/pyramid/request.py b/pyramid/request.py
index b2e2efe05..3cbe5d9e3 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -8,6 +8,7 @@ from webob import BaseRequest
from pyramid.interfaces import (
IRequest,
+ IRequestExtensions,
IResponse,
ISessionFactory,
)
@@ -16,6 +17,7 @@ from pyramid.compat import (
text_,
bytes_,
native_,
+ iteritems_,
)
from pyramid.decorator import reify
@@ -26,7 +28,10 @@ from pyramid.security import (
AuthorizationAPIMixin,
)
from pyramid.url import URLMethodsMixin
-from pyramid.util import InstancePropertyMixin
+from pyramid.util import (
+ InstancePropertyHelper,
+ InstancePropertyMixin,
+)
class TemplateContext(object):
pass
@@ -307,3 +312,22 @@ def call_app_with_subpath_as_path_info(request, app):
new_request.environ['PATH_INFO'] = new_path_info
return new_request.get_response(app)
+
+def apply_request_extensions(request, extensions=None):
+ """Apply request extensions (methods and properties) to an instance of
+ :class:`pyramid.interfaces.IRequest`. This method is dependent on the
+ ``request`` containing a properly initialized registry.
+
+ After invoking this method, the ``request`` should have the methods
+ and properties that were defined using
+ :meth:`pyramid.config.Configurator.add_request_method`.
+ """
+ if extensions is None:
+ extensions = request.registry.queryUtility(IRequestExtensions)
+ if extensions is not None:
+ for name, fn in iteritems_(extensions.methods):
+ method = fn.__get__(request, request.__class__)
+ setattr(request, name, method)
+
+ InstancePropertyHelper.apply_properties(
+ request, extensions.descriptors)
diff --git a/pyramid/router.py b/pyramid/router.py
index ba4f85b18..0b1ecade7 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -27,6 +27,7 @@ from pyramid.events import (
from pyramid.exceptions import PredicateMismatch
from pyramid.httpexceptions import HTTPNotFound
from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager
from pyramid.traversal import (
@@ -213,7 +214,7 @@ class Router(object):
try:
extensions = self.request_extensions
if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request, extensions=extensions)
response = handle_request(request)
if request.response_callbacks:
diff --git a/pyramid/scripting.py b/pyramid/scripting.py
index fdb4aa430..d9587338f 100644
--- a/pyramid/scripting.py
+++ b/pyramid/scripting.py
@@ -1,12 +1,12 @@
from pyramid.config import global_registries
from pyramid.exceptions import ConfigurationError
-from pyramid.request import Request
from pyramid.interfaces import (
- IRequestExtensions,
IRequestFactory,
IRootFactory,
)
+from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager as threadlocal_manager
from pyramid.traversal import DefaultRootFactory
@@ -77,9 +77,7 @@ def prepare(request=None, registry=None):
request.registry = registry
threadlocals = {'registry':registry, 'request':request}
threadlocal_manager.push(threadlocals)
- extensions = registry.queryUtility(IRequestExtensions)
- if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request)
def closer():
threadlocal_manager.pop()
root_factory = registry.queryUtility(IRootFactory,
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index 48af98f59..f142e4536 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -435,7 +435,50 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase):
self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded)
self.assertEqual(request.environ['PATH_INFO'], '/' + encoded)
-class DummyRequest:
+class Test_apply_request_extensions(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, request, extensions=None):
+ from pyramid.request import apply_request_extensions
+ return apply_request_extensions(request, extensions=extensions)
+
+ def test_it_with_registry(self):
+ from pyramid.interfaces import IRequestExtensions
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self.config.registry.registerUtility(extensions, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+ self._callFUT(request)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+ def test_it_override_extensions(self):
+ from pyramid.interfaces import IRequestExtensions
+ ignore = Dummy()
+ ignore.methods = {'x': lambda x, y, z: 'asdf'}
+ ignore.descriptors = {'bar': property(lambda x: 'asdf')}
+ self.config.registry.registerUtility(ignore, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self._callFUT(request, extensions=extensions)
+ self.assertRaises(AttributeError, lambda: request.x)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+class Dummy(object):
+ pass
+
+class DummyRequest(object):
def __init__(self, environ=None):
if environ is None:
environ = {}
diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py
index 30ebd5918..b57c248d5 100644
--- a/pyramid/tests/test_router.py
+++ b/pyramid/tests/test_router.py
@@ -317,6 +317,7 @@ class TestRouter(unittest.TestCase):
from pyramid.interfaces import IRequestExtensions
from pyramid.interfaces import IRequest
from pyramid.request import Request
+ from pyramid.util import InstancePropertyHelper
context = DummyContext()
self._registerTraverserFactory(context)
class Extensions(object):
@@ -324,11 +325,12 @@ class TestRouter(unittest.TestCase):
self.methods = {}
self.descriptors = {}
extensions = Extensions()
- L = []
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, name='foo')
+ extensions.descriptors[name] = fn
request = Request.blank('/')
request.request_iface = IRequest
request.registry = self.registry
- request._set_extensions = lambda *x: L.extend(x)
def request_factory(environ):
return request
self.registry.registerUtility(extensions, IRequestExtensions)
@@ -342,7 +344,7 @@ class TestRouter(unittest.TestCase):
router.request_factory = request_factory
start_response = DummyStartResponse()
router(environ, start_response)
- self.assertEqual(L, [extensions])
+ self.assertEqual(view.request.foo, 'bar')
def test_call_view_registered_nonspecific_default_path(self):
from pyramid.interfaces import IViewClassifier
diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py
index a36d1ed71..1e952062b 100644
--- a/pyramid/tests/test_scripting.py
+++ b/pyramid/tests/test_scripting.py
@@ -122,11 +122,15 @@ class Test_prepare(unittest.TestCase):
self.assertEqual(request.context, context)
def test_it_with_extensions(self):
- exts = Dummy()
+ from pyramid.util import InstancePropertyHelper
+ exts = DummyExtensions()
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, 'foo')
+ exts.descriptors[name] = fn
request = DummyRequest({})
registry = request.registry = self._makeRegistry([exts, DummyFactory])
info = self._callFUT(request=request, registry=registry)
- self.assertEqual(request.extensions, exts)
+ self.assertEqual(request.foo, 'bar')
root, closer = info['root'], info['closer']
closer()
@@ -199,11 +203,13 @@ class DummyThreadLocalManager:
def pop(self):
self.popped.append(True)
-class DummyRequest:
+class DummyRequest(object):
matchdict = None
matched_route = None
def __init__(self, environ):
self.environ = environ
- def _set_extensions(self, exts):
- self.extensions = exts
+class DummyExtensions:
+ def __init__(self):
+ self.descriptors = {}
+ self.methods = {}
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index 371cd8703..459c729a0 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -2,6 +2,188 @@ import unittest
from pyramid.compat import PY3
+class Test_InstancePropertyHelper(unittest.TestCase):
+ def _makeOne(self):
+ cls = self._getTargetClass()
+ return cls()
+
+ def _getTargetClass(self):
+ from pyramid.util import InstancePropertyHelper
+ return InstancePropertyHelper
+
+ def test_callable(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, 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 = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, 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 = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, 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 = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ helper.set_property(foo, 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 = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property, foo, property(worker))
+
+ def test_property_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, 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 = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property,
+ foo, property(worker), name='x', reify=True)
+
+ def test_override_property(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ def doit():
+ foo.x = 1
+ self.assertRaises(AttributeError, doit)
+
+ def test_override_reify(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, 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 = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x')
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, 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 = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, lambda _: 2, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+
+ def test_make_property(self):
+ from pyramid.decorator import reify
+ helper = self._getTargetClass()
+ name, fn = helper.make_property(lambda x: 1, name='x', reify=True)
+ self.assertEqual(name, 'x')
+ self.assertTrue(isinstance(fn, reify))
+
+ def test_apply_properties_with_iterable(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x = helper.make_property(lambda _: 1, name='x', reify=True)
+ y = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, [x, y])
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_apply_properties_with_dict(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True)
+ y_name, y_fn = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn})
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_make_property_unicode(self):
+ from pyramid.compat import text_
+ from pyramid.exceptions import ConfigurationError
+
+ cls = self._getTargetClass()
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ def make_bad_name():
+ cls.make_property(lambda x: 1, name=name, reify=True)
+
+ self.assertRaises(ConfigurationError, make_bad_name)
+
+ def test_add_property(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: obj.bar, name='x', reify=True)
+ helper.add_property(lambda obj: obj.bar, name='y')
+ self.assertEqual(len(helper.properties), 2)
+ foo = Dummy()
+ helper.apply(foo)
+ foo.bar = 1
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 1)
+ foo.bar = 2
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 2)
+
+ def test_apply_multiple_times(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: 1, name='x')
+ foo, bar = Dummy(), Dummy()
+ helper.apply(foo)
+ self.assertEqual(foo.x, 1)
+ helper.add_property(lambda obj: 2, name='x')
+ helper.apply(bar)
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(bar.x, 2)
+
class Test_InstancePropertyMixin(unittest.TestCase):
def _makeOne(self):
cls = self._getTargetClass()
@@ -111,58 +293,6 @@ class Test_InstancePropertyMixin(unittest.TestCase):
foo.set_property(lambda _: 2, name='x', reify=True)
self.assertEqual(1, foo.x)
- def test__make_property(self):
- from pyramid.decorator import reify
- cls = self._getTargetClass()
- name, fn = cls._make_property(lambda x: 1, name='x', reify=True)
- self.assertEqual(name, 'x')
- self.assertTrue(isinstance(fn, reify))
-
- def test__set_properties_with_iterable(self):
- foo = self._makeOne()
- x = foo._make_property(lambda _: 1, name='x', reify=True)
- y = foo._make_property(lambda _: 2, name='y')
- foo._set_properties([x, y])
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__make_property_unicode(self):
- from pyramid.compat import text_
- from pyramid.exceptions import ConfigurationError
-
- cls = self._getTargetClass()
- if PY3: # pragma: nocover
- name = b'La Pe\xc3\xb1a'
- else: # pragma: nocover
- name = text_(b'La Pe\xc3\xb1a', 'utf-8')
-
- def make_bad_name():
- cls._make_property(lambda x: 1, name=name, reify=True)
-
- self.assertRaises(ConfigurationError, make_bad_name)
-
- def test__set_properties_with_dict(self):
- foo = self._makeOne()
- x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True)
- y_name, y_fn = foo._make_property(lambda _: 2, name='y')
- foo._set_properties({x_name: x_fn, y_name: y_fn})
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__set_extensions(self):
- inst = self._makeOne()
- def foo(self, result):
- return result
- n, bar = inst._make_property(lambda _: 'bar', name='bar')
- class Extensions(object):
- def __init__(self):
- self.methods = {'foo':foo}
- self.descriptors = {'bar':bar}
- extensions = Extensions()
- inst._set_extensions(extensions)
- self.assertEqual(inst.bar, 'bar')
- self.assertEqual(inst.foo('abc'), 'abc')
-
class Test_WeakOrderedSet(unittest.TestCase):
def _makeOne(self):
from pyramid.config import WeakOrderedSet
@@ -646,7 +776,7 @@ class TestCallableName(unittest.TestCase):
else: # pragma: nocover
name = text_(b'hello world', 'utf-8')
- self.assertEquals(get_callable_name(name), 'hello world')
+ self.assertEqual(get_callable_name(name), 'hello world')
def test_invalid_ascii(self):
from pyramid.util import get_callable_name
diff --git a/pyramid/util.py b/pyramid/util.py
index 7e8535aaf..63d113361 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -34,14 +34,21 @@ class DottedNameResolver(_DottedNameResolver):
_marker = object()
-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.
+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
+ defined on the class itself.
+
+ This class is optimized for adding multiple properties at once to an
+ instance. This is done by calling :meth:`.add_property` once
+ per-property and then invoking :meth:`.apply` on target objects.
+
"""
+ def __init__(self):
+ self.properties = {}
@classmethod
- def _make_property(cls, callable, name=None, reify=False):
+ def make_property(cls, callable, name=None, reify=False):
""" Convert a callable into one suitable for adding to the
instance. This will return a 2-tuple containing the computed
(name, property) pair.
@@ -69,25 +76,12 @@ class InstancePropertyMixin(object):
return name, fn
- def _set_properties(self, properties):
- """ Create several properties on the instance at once.
-
- This is a more efficient version of
- :meth:`pyramid.util.InstancePropertyMixin.set_property` which
- can accept multiple ``(name, property)`` pairs generated via
- :meth:`pyramid.util.InstancePropertyMixin._make_property`.
-
- ``properties`` is a sequence of two-tuples *or* a data structure
- with an ``.items()`` method which returns a sequence of two-tuples
- (presumably a dictionary). It will be used to add several
- properties to the instance in a manner that is more efficient
- than simply calling ``set_property`` repeatedly.
- """
+ @classmethod
+ def apply_properties(cls, target, properties):
attrs = dict(properties)
-
if attrs:
- parent = self.__class__
- cls = type(parent.__name__, (parent, object), attrs)
+ parent = target.__class__
+ newcls = type(parent.__name__, (parent, object), attrs)
# We assign __provides__, __implemented__ and __providedBy__ below
# to prevent a memory leak that results from from the usage of this
# instance's eventual use in an adapter lookup. Adapter lookup
@@ -106,15 +100,34 @@ class InstancePropertyMixin(object):
# attached to it
val = getattr(parent, name, _marker)
if val is not _marker:
- setattr(cls, name, val)
- self.__class__ = cls
+ setattr(newcls, name, val)
+ target.__class__ = newcls
+
+ @classmethod
+ def set_property(cls, target, callable, name=None, reify=False):
+ """A helper method to apply a single property to an instance."""
+ prop = cls.make_property(callable, name=name, reify=reify)
+ cls.apply_properties(target, [prop])
+
+ def add_property(self, callable, name=None, reify=False):
+ """Add a new property configuration.
+
+ This should be used in combination with :meth:`.apply` as a
+ more efficient version of :meth:`.set_property`.
+ """
+ name, fn = self.make_property(callable, name=name, reify=reify)
+ self.properties[name] = fn
- def _set_extensions(self, extensions):
- for name, fn in iteritems_(extensions.methods):
- method = fn.__get__(self, self.__class__)
- setattr(self, name, method)
+ def apply(self, target):
+ """ Apply all configured properties to the ``target`` instance."""
+ if self.properties:
+ self.apply_properties(target, self.properties)
- self._set_properties(extensions.descriptors)
+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, callable, name=None, reify=False):
""" Add a callable or a property descriptor to the instance.
@@ -168,8 +181,8 @@ class InstancePropertyMixin(object):
>>> foo.y # notice y keeps the original value
1
"""
- prop = self._make_property(callable, name=name, reify=reify)
- self._set_properties([prop])
+ InstancePropertyHelper.set_property(
+ self, callable, name=name, reify=reify)
class WeakOrderedSet(object):
""" Maintain a set of items.