diff options
| -rw-r--r-- | docs/api/request.rst | 1 | ||||
| -rw-r--r-- | pyramid/config/factories.py | 4 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 3 | ||||
| -rw-r--r-- | pyramid/request.py | 26 | ||||
| -rw-r--r-- | pyramid/router.py | 3 | ||||
| -rw-r--r-- | pyramid/scripting.py | 8 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 45 | ||||
| -rw-r--r-- | pyramid/tests/test_router.py | 8 | ||||
| -rw-r--r-- | pyramid/tests/test_scripting.py | 16 | ||||
| -rw-r--r-- | pyramid/tests/test_util.py | 236 | ||||
| -rw-r--r-- | pyramid/util.py | 75 |
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. |
