summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt59
-rw-r--r--docs/api/config.rst2
-rw-r--r--pyramid/config.py81
-rw-r--r--pyramid/interfaces.py18
-rw-r--r--pyramid/request.py2
-rw-r--r--pyramid/router.py284
6 files changed, 318 insertions, 128 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 3c232d880..b1979347a 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -51,6 +51,65 @@ Features
argument; this chain will be returned to Pyramid as a single view
callable.
+- New configurator directive:
+ ``pyramid.config.Configurator.add_request_handler``. This directive adds
+ a request handler factory.
+
+ A request handler factory is used to wrap the Pyramid router's primary
+ request handling function. This is a feature usually only used by
+ framework extensions, to provide, for example, view timing support and as
+ a convenient place to hang bookkeeping code that examines exceptions
+ before they are returned to the server.
+
+ A request handler factory (passed as ``handler_factory``) must be a
+ callable which accepts two arguments: ``handler`` and ``registry``.
+ ``handler`` will be the request handler being wrapped. ``registry`` will
+ be the Pyramid application registry represented by this Configurator. A
+ request handler factory must return a request handler when it is called.
+
+ A request handler accepts a request object and returns a response object.
+
+ Here's an example of creating both a handler factory and a handler, and
+ registering the handler factory:
+
+ .. code-block:: python
+
+ import time
+
+ def timing_handler_factory(handler, registry):
+ if registry.settings['do_timing']:
+ # if timing support is enabled, return a wrapper
+ def timing_handler(request):
+ start = time.time()
+ try:
+ response = handler(request)
+ finally:
+ end = time.time()
+ print: 'The request took %s seconds' % (end - start)
+ return response
+ return timing_handler
+ # if timing support is not enabled, return the original handler
+ return handler
+
+ config.add_request_handler(timing_handler_factory, 'timing')
+
+ The ``request`` argument to the handler will be the request created by
+ Pyramid's router when it receives a WSGI request.
+
+ If more than one request handler factory is registered into a single
+ configuration, the request handlers will be chained together. The first
+ request handler factory added (in code execution order) will be called
+ with the default Pyramid request handler, the second handler factory added
+ will be called with the result of the first handler factory, ad
+ infinitum. The Pyramid router will use the outermost wrapper in this chain
+ (which is a bit like a WSGI middleware "pipeline") as its handler
+ function.
+
+ The ``name`` argument to this function is required. The name is used as a
+ key for conflict detection. No two request handler factories may share
+ the same name in the same configuration (unless
+ automatic_conflict_resolution is able to resolve the conflict or
+ this is an autocommitting configurator).
1.1 (2011-07-22)
================
diff --git a/docs/api/config.rst b/docs/api/config.rst
index 96e955388..21e2b828d 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -78,6 +78,8 @@
.. automethod:: set_view_mapper
+ .. automethod:: add_request_handler
+
.. automethod:: testing_securitypolicy
.. automethod:: testing_resources
diff --git a/pyramid/config.py b/pyramid/config.py
index cad853674..6c47e7871 100644
--- a/pyramid/config.py
+++ b/pyramid/config.py
@@ -37,6 +37,8 @@ from pyramid.interfaces import IRendererFactory
from pyramid.interfaces import IRendererGlobalsFactory
from pyramid.interfaces import IRequest
from pyramid.interfaces import IRequestFactory
+from pyramid.interfaces import IRequestHandlerFactory
+from pyramid.interfaces import IRequestHandlerFactories
from pyramid.interfaces import IResponse
from pyramid.interfaces import IRootFactory
from pyramid.interfaces import IRouteRequest
@@ -886,6 +888,85 @@ class Configurator(object):
return self._derive_view(view, attr=attr, renderer=renderer)
@action_method
+ def add_request_handler(self, handler_factory, name):
+ """
+ Add a request handler factory. A request handler factory is used to
+ wrap the Pyramid router's primary request handling function. This is
+ a feature usually only used by framework extensions, to provide, for
+ example, view timing support and as a convenient place to hang
+ bookkeeping code that examines exceptions before they are returned to
+ the server.
+
+ A request handler factory (passed as ``handler_factory``) must be a
+ callable which accepts two arguments: ``handler`` and ``registry``.
+ ``handler`` will be the request handler being wrapped. ``registry``
+ will be the Pyramid :term:`application registry` represented by this
+ Configurator. A request handler factory must return a request
+ handler when it is called.
+
+ A request handler accepts a :term:`request` object and returns a
+ :term:`response` object.
+
+ Here's an example of creating both a handler factory and a handler,
+ and registering the handler factory:
+
+ .. code-block:: python
+
+ import time
+
+ def timing_handler_factory(handler, registry):
+ if registry.settings['do_timing']:
+ # if timing support is enabled, return a wrapper
+ def timing_handler(request):
+ start = time.time()
+ try:
+ response = handler(request)
+ finally:
+ end = time.time()
+ print: 'The request took %s seconds' % (end - start)
+ return response
+ return timing_handler
+ # if timing support is not enabled, return the original handler
+ return handler
+
+ config.add_request_handler(timing_handler_factory, 'timing')
+
+ The ``request`` argument to the handler will be the request created
+ by Pyramid's router when it receives a WSGI request.
+
+ If more than one request handler factory is registered into a single
+ configuration, the request handlers will be chained together. The
+ first request handler factory added (in code execution order) will be
+ called with the default Pyramid request handler, the second handler
+ factory added will be called with the result of the first handler
+ factory, ad infinitum. The Pyramid router will use the outermost
+ wrapper in this chain (which is a bit like a WSGI middleware
+ "pipeline") as its handler function.
+
+ The ``name`` argument to this function is required. The name is used
+ as a key for conflict detection. No two request handler factories
+ may share the same name in the same configuration (unless
+ :ref:`automatic_conflict_resolution` is able to resolve the conflict
+ or this is an autocommitting configurator).
+
+ .. note:: This feature is new as of Pyramid 1.1.1.
+ """
+ def register():
+ registry = self.registry
+ existing_factory = registry.queryUtility(IRequestHandlerFactory,
+ name=name)
+ registry.registerUtility(handler_factory, IRequestHandlerFactory,
+ name=name)
+ existing_names = registry.queryUtility(IRequestHandlerFactories,
+ default=[])
+ if not existing_factory:
+ # don't replace a name if someone is trying to override
+ # through a commit
+ existing_names.append(name)
+ registry.registerUtility(existing_names, IRequestHandlerFactories)
+ self.action(('requesthandler', name), register)
+
+ @action_method
def add_subscriber(self, subscriber, iface=None):
"""Add an event :term:`subscriber` for the event stream
implied by the supplied ``iface`` interface. The
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index ec1d23acf..a06cb7e52 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -446,6 +446,24 @@ class IMultiDict(Interface): # docs-only interface
class IRequest(Interface):
""" Request type interface attached to all request objects """
+class IRequestHandlerFactories(Interface):
+ """ Marker interface for utility registration representing the ordered
+ set of a configuration's request handler factories"""
+
+class IRequestHandlerFactory(Interface):
+ """ A request handler factory can be used to augment Pyramid's default
+ mainloop request handling."""
+ def __call__(self, handler, registry):
+ """ Return an IRequestHandler; the ``handler`` argument passed will
+ be the previous request handler added, or the default request handler
+ if no request handlers have yet been added ."""
+
+class IRequestHandler(Interface):
+ """ """
+ def __call__(self, request):
+ """ Must return an IResponse or raise an exception. The ``request``
+ argument will be an instance of an object that provides IRequest."""
+
IRequest.combined = IRequest # for exception view lookups
class IRouteRequest(Interface):
diff --git a/pyramid/request.py b/pyramid/request.py
index f84365dc5..927319479 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -262,6 +262,8 @@ class Request(BaseRequest, DeprecatedRequestMethods):
directly former view wrapper factory as its ``view_callable``
argument; this chain will be returned to Pyramid as a single view
callable.
+
+ .. note:: This feature is new as of Pyramid 1.1.1.
"""
wrappers = self.view_wrappers
if not wrappers:
diff --git a/pyramid/router.py b/pyramid/router.py
index 0294d8d75..dcf257beb 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -12,6 +12,8 @@ from pyramid.interfaces import IRoutesMapper
from pyramid.interfaces import ITraverser
from pyramid.interfaces import IView
from pyramid.interfaces import IViewClassifier
+from pyramid.interfaces import IRequestHandlerFactory
+from pyramid.interfaces import IRequestHandlerFactories
from pyramid.events import ContextFound
from pyramid.events import NewRequest
@@ -36,6 +38,14 @@ class Router(object):
self.root_factory = q(IRootFactory, default=DefaultRootFactory)
self.routes_mapper = q(IRoutesMapper)
self.request_factory = q(IRequestFactory, default=Request)
+ handler_factory_names = q(IRequestHandlerFactories)
+ handler = self.handle_request
+ if handler_factory_names:
+ for name in handler_factory_names:
+ handler_factory = registry.getUtility(IRequestHandlerFactory,
+ name=name)
+ handler = handler_factory(handler, registry)
+ self.handle_request = handler
self.root_policy = self.root_factory # b/w compat
self.registry = registry
settings = registry.settings
@@ -44,6 +54,150 @@ class Router(object):
self.debug_notfound = settings['debug_notfound']
self.debug_routematch = settings['debug_routematch']
+ def handle_request(self, request):
+ attrs = request.__dict__
+ registry = attrs['registry']
+ request_iface = IRequest
+ context = None
+ routes_mapper = self.routes_mapper
+ debug_routematch = self.debug_routematch
+ adapters = registry.adapters
+ has_listeners = registry.has_listeners
+ notify = registry.notify
+ logger = self.logger
+ try: # matches except Exception (exception view execution)
+ has_listeners and notify(NewRequest(request))
+ # find the root object
+ root_factory = self.root_factory
+ if routes_mapper is not None:
+ info = routes_mapper(request)
+ match, route = info['match'], info['route']
+ if route is None:
+ if debug_routematch:
+ msg = ('no route matched for url %s' %
+ request.url)
+ logger and logger.debug(msg)
+ else:
+ # TODO: kill off bfg.routes.* environ keys
+ # when traverser requires request arg, and
+ # cant cope with environ anymore (they are
+ # docs-deprecated as of BFG 1.3)
+ environ = request.environ
+ environ['bfg.routes.route'] = route
+ environ['bfg.routes.matchdict'] = match
+ attrs['matchdict'] = match
+ attrs['matched_route'] = route
+
+ if debug_routematch:
+ msg = (
+ 'route matched for url %s; '
+ 'route_name: %r, '
+ 'path_info: %r, '
+ 'pattern: %r, '
+ 'matchdict: %r, '
+ 'predicates: %r' % (
+ request.url,
+ route.name,
+ request.path_info,
+ route.pattern, match,
+ route.predicates)
+ )
+ logger and logger.debug(msg)
+
+ request_iface = registry.queryUtility(
+ IRouteRequest,
+ name=route.name,
+ default=IRequest)
+
+ root_factory = route.factory or \
+ self.root_factory
+
+ root = root_factory(request)
+ attrs['root'] = root
+
+ # find a context
+ traverser = adapters.queryAdapter(root, ITraverser)
+ if traverser is None:
+ traverser = ResourceTreeTraverser(root)
+ tdict = traverser(request)
+
+ context, view_name, subpath, traversed, vroot, \
+ vroot_path = (
+ tdict['context'],
+ tdict['view_name'],
+ tdict['subpath'],
+ tdict['traversed'],
+ tdict['virtual_root'],
+ tdict['virtual_root_path']
+ )
+
+ attrs.update(tdict)
+ has_listeners and notify(ContextFound(request))
+
+ # find a view callable
+ context_iface = providedBy(context)
+ view_callable = adapters.lookup(
+ (IViewClassifier, request_iface, context_iface),
+ IView, name=view_name, default=None)
+
+ # invoke the view callable
+ if view_callable is None:
+ if self.debug_notfound:
+ msg = (
+ 'debug_notfound of url %s; path_info: %r, '
+ 'context: %r, view_name: %r, subpath: %r, '
+ 'traversed: %r, root: %r, vroot: %r, '
+ 'vroot_path: %r' % (
+ request.url, request.path_info, context,
+ view_name,
+ subpath, traversed, root, vroot,
+ vroot_path)
+ )
+ logger and logger.debug(msg)
+ else:
+ msg = request.path_info
+ raise HTTPNotFound(msg)
+ else:
+ # if there were any view wrappers for the current
+ # request, use them to wrap the view
+ if request.view_wrappers:
+ view_callable = request._wrap_view(
+ view_callable)
+
+ response = view_callable(context, request)
+
+ # handle exceptions raised during root finding and view-exec
+ except Exception, why:
+ # clear old generated request.response, if any; it may
+ # have been mutated by the view, and its state is not
+ # sane (e.g. caching headers)
+ if 'response' in attrs:
+ del attrs['response']
+
+ attrs['exception'] = why
+
+ for_ = (IExceptionViewClassifier,
+ request_iface.combined,
+ providedBy(why))
+ view_callable = adapters.lookup(for_, IView,
+ default=None)
+
+ if view_callable is None:
+ raise
+
+ if request.view_wrappers:
+ view_callable = request._wrap_view(view_callable,
+ exc=why)
+
+ response = view_callable(why, request)
+
+ has_listeners and notify(NewResponse(request, response))
+
+ if request.response_callbacks:
+ request._process_response_callbacks(response)
+
+ return response
+
def __call__(self, environ, start_response):
"""
Accept ``environ`` and ``start_response``; create a
@@ -53,13 +207,7 @@ class Router(object):
return an iterable.
"""
registry = self.registry
- adapters = registry.adapters
- has_listeners = registry.has_listeners
- notify = registry.notify
- logger = self.logger
manager = self.threadlocal_manager
- routes_mapper = self.routes_mapper
- debug_routematch = self.debug_routematch
request = None
threadlocals = {'registry':registry, 'request':request}
manager.push(threadlocals)
@@ -70,129 +218,9 @@ class Router(object):
# create the request
request = self.request_factory(environ)
- context = None
threadlocals['request'] = request
- attrs = request.__dict__
- attrs['registry'] = registry
- request_iface = IRequest
-
- try: # matches except Exception (exception view execution)
- has_listeners and notify(NewRequest(request))
- # find the root object
- root_factory = self.root_factory
- if routes_mapper is not None:
- info = routes_mapper(request)
- match, route = info['match'], info['route']
- if route is None:
- if debug_routematch:
- msg = ('no route matched for url %s' %
- request.url)
- logger and logger.debug(msg)
- else:
- # TODO: kill off bfg.routes.* environ keys when
- # traverser requires request arg, and cant cope
- # with environ anymore (they are docs-deprecated as
- # of BFG 1.3)
- environ['bfg.routes.route'] = route
- environ['bfg.routes.matchdict'] = match
- attrs['matchdict'] = match
- attrs['matched_route'] = route
-
- if debug_routematch:
- msg = (
- 'route matched for url %s; '
- 'route_name: %r, '
- 'path_info: %r, '
- 'pattern: %r, '
- 'matchdict: %r, '
- 'predicates: %r' % (
- request.url,
- route.name,
- request.path_info,
- route.pattern, match,
- route.predicates)
- )
- logger and logger.debug(msg)
-
- request_iface = registry.queryUtility(
- IRouteRequest,
- name=route.name,
- default=IRequest)
- root_factory = route.factory or self.root_factory
-
- root = root_factory(request)
- attrs['root'] = root
-
- # find a context
- traverser = adapters.queryAdapter(root, ITraverser)
- if traverser is None:
- traverser = ResourceTreeTraverser(root)
- tdict = traverser(request)
- context, view_name, subpath, traversed, vroot, vroot_path =(
- tdict['context'], tdict['view_name'], tdict['subpath'],
- tdict['traversed'], tdict['virtual_root'],
- tdict['virtual_root_path'])
- attrs.update(tdict)
- has_listeners and notify(ContextFound(request))
-
- # find a view callable
- context_iface = providedBy(context)
- view_callable = adapters.lookup(
- (IViewClassifier, request_iface, context_iface),
- IView, name=view_name, default=None)
-
- # invoke the view callable
- if view_callable is None:
- if self.debug_notfound:
- msg = (
- 'debug_notfound of url %s; path_info: %r, '
- 'context: %r, view_name: %r, subpath: %r, '
- 'traversed: %r, root: %r, vroot: %r, '
- 'vroot_path: %r' % (
- request.url, request.path_info, context,
- view_name,
- subpath, traversed, root, vroot, vroot_path)
- )
- logger and logger.debug(msg)
- else:
- msg = request.path_info
- raise HTTPNotFound(msg)
- else:
- # if there were any view wrappers for the current
- # request, use them to wrap the view
- if request.view_wrappers:
- view_callable = request._wrap_view(view_callable)
-
- response = view_callable(context, request)
-
- # handle exceptions raised during root finding and view-exec
- except Exception, why:
- # clear old generated request.response, if any; it may
- # have been mutated by the view, and its state is not
- # sane (e.g. caching headers)
- if 'response' in attrs:
- del attrs['response']
-
- attrs['exception'] = why
-
- for_ = (IExceptionViewClassifier,
- request_iface.combined,
- providedBy(why))
- view_callable = adapters.lookup(for_, IView, default=None)
-
- if view_callable is None:
- raise
-
- if request.view_wrappers:
- view_callable = request._wrap_view(view_callable,
- exc=why)
-
- response = view_callable(why, request)
-
- has_listeners and notify(NewResponse(request, response))
-
- if request.response_callbacks:
- request._process_response_callbacks(response)
+ request.registry = registry
+ response = self.handle_request(request)
finally:
if request is not None and request.finished_callbacks: