diff options
| -rw-r--r-- | CHANGES.txt | 59 | ||||
| -rw-r--r-- | docs/api/config.rst | 2 | ||||
| -rw-r--r-- | pyramid/config.py | 81 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 18 | ||||
| -rw-r--r-- | pyramid/request.py | 2 | ||||
| -rw-r--r-- | pyramid/router.py | 284 |
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: |
