From 0d49632e7f9fd60c2f02f09a34b922b567186d4d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 00:34:18 -0400 Subject: some tests fail; simpler registration of handlers --- pyramid/config.py | 220 ++++++++++++++++++++----------------------- pyramid/interfaces.py | 15 +-- pyramid/router.py | 21 ++--- pyramid/tests/test_config.py | 86 ++++++++--------- pyramid/tests/test_router.py | 94 +++++++++--------- 5 files changed, 203 insertions(+), 233 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index e9f2dd2a5..5288d1d48 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -38,8 +38,7 @@ 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 IRequestHandlerManager from pyramid.interfaces import IResponse from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest @@ -720,8 +719,12 @@ class Configurator(object): if settings: includes = settings.pop('pyramid.include', '') includes = [x.strip() for x in includes.splitlines()] + expl_handler_factories = settings.pop('pyramid.request_handlers','') + expl_handler_factories = [x.strip() for x in + expl_handler_factories.splitlines()] else: includes = [] + expl_handler_factories = [] registry = self.registry self._fix_registry() self._set_settings(settings) @@ -731,6 +734,11 @@ class Configurator(object): # cope with WebOb exc objects not decoratored with IExceptionResponse from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) + # add a handler manager + handler_manager = RequestHandlerManager() + registry.registerUtility(handler_manager, IRequestHandlerManager) + self._add_request_handler('pyramid.router.exc_view_handler_factory', + explicit=False) if debug_logger is None: debug_logger = logging.getLogger(self.package_name) @@ -777,6 +785,8 @@ class Configurator(object): if default_view_mapper is not None: self.set_view_mapper(default_view_mapper) self.commit() + for factory in expl_handler_factories: + self._add_request_handler(factory, explicit=True) for inc in includes: self.include(inc) @@ -898,15 +908,17 @@ class Configurator(object): return self._derive_view(view, attr=attr, renderer=renderer) @action_method - def add_request_handler(self, handler_factory, before=None, - after=None, name=None): + def add_request_handler(self, handler_factory): """ Add a request handler factory. A request handler factory is used to - wrap the Pyramid router's main request handling function. This is - a feature that may be used by framework extensions, to provide, for + wrap the Pyramid router's main request handling function. This is a + feature that may be 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. + the WSGI server. Request handlers behave a bit like :mod:`WSGI` + 'middleware' but they have the benefit of running in a context in + which they have access tot he Pyramid :term:`application registry` as + well as the Pyramid rendering machinery. A request handler factory (passed as ``handler_factory``) must be a callable (or a :term:`dotted Python name` to such a callable) which @@ -919,15 +931,19 @@ class Configurator(object): 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: + Here's an example creating a handler factory and registering the + handler factory: .. code-block:: python import time + from pyramid.settings import asbool + import logging + + log = logging.getLogger(__name__) def timing_handler_factory(handler, registry): - if registry.settings['do_timing']: + if asbool(registry.settings.get('do_timing')): # if timing support is enabled, return a wrapper def timing_handler(request): start = time.time() @@ -935,10 +951,12 @@ class Configurator(object): response = handler(request) finally: end = time.time() - print: 'The request took %s seconds' % (end - start) + log.debug('The request took %s seconds' % + (end - start)) return response return timing_handler - # if timing support is not enabled, return the original handler + # if timing support is not enabled, return the original + # handler return handler config.add_request_handler(timing_handler_factory) @@ -946,120 +964,61 @@ class Configurator(object): 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, request handlers will be chained together. The first - request handler factory in the chain 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 deploying user can specify request handler inclusion and ordering - in his Pyramid configuration using the ``pyramid.handlers`` - configuration value. However, he is not required to do so. Instead, - he can rely on hinting information provided by you (the framework - extender) which provides some clues about where in his request - handler pipeline a request handler should wind up. This is achieved - by using the ``before`` and/or ``after`` arguments to - ``add_request_handler``. - - The ``before`` and ``after`` arguments provide hints to Pyramid about - which position in the handler chain this handler must assume when the - deploying user has not defined a ``pyramid.handlers`` configuration - value. ``before`` and ``after`` are relative to request *ingress* - order. Therefore, ``before`` means 'closer to - request ingress' and ``after`` means 'closer to Pyramid's main request - handler'. - - ``before`` or ``after`` may be either the name of another handler, - the name ``main`` representing Pyramid's main request handler, the - name ``ingress`` representing the request ingress point, or a sequence - of handler names (the sequence can itself include ``main`` or - ``ingress``). For example, if you'd like to hint that this handler - should always come before the - ``pyramid.router.exception_view_handler_factory`` handler but before - the ``pyramid_retry.retry_handler_factory`` handler: - - .. code-block:: python - :linenos: - - config.add_request_handler( - 'mypkg.handler_factory', - before='pyramid.router.exception_view_handler_factory', - after='pyramid_retry.retry_handler_factory') - - Or you might rather hint that a handler should always come before the - ``main`` handler but after retry and transaction management handlers: - - .. code-block:: python - :linenos: - - config.add_handler('mypkg.handler', before='main', - after=('pyramid_retry.retry_handler_factory', - 'pyramid_tm.tm_handler_factory') - - Or you might hint that a handler should come after a browser - id-generating handler, but you don't want to hint that it comes - before anything: - - .. code-block:: python - :linenos: - - config.add_handler( - 'mypkg.handler', - after='pyramid_browserid.browserid_handler_factory') - - The names provided to ``before`` and ``after`` don't need to - represent handlers that actually exist in the current deployment. - For example, if you say - ``before='pyramid_retry.retry_handler_factory'`` and the - ``pyramid_retry.retry_handler_factory`` handler is not configured - into the current deployment at all, no error will be raised when the - add_request_handler statement is executed; instead, the hint will be - effectively ignored. - - It is an error to specify an ``after`` value of ``main`` or a - ``before`` value of ``ingress``. - - Pyramid does its best to configure a sensible handlers pipeline when - the user does not provide a ``pyramid.handlers`` configuration - setting in his Pyramid configuration, even in the face of conflicting - hints, by using a topological sort on all handlers configured into - the system using ``before`` and ``after`` hinting as input to the - sort. However, ``before`` and ``after`` hinting are ignored - completely when the deploying user has specified an explicit - ``pyramid.handlers`` configuration value; this value specifies an - unambigous inclusion and ordering for handlers in a given deployment; - when it is provided, the topological sort that uses the ``before`` - and ``after`` hint information is never performed. - - A user can get both the effective and hinted handler ordering by - using the ``paster phandlers`` command. - - The ``name`` argument to this function is optional. 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). By default, a name will - be the :term:`Python dotted name` of the object passed as - ``handler_factory``. + If more than one call to ``add_request_handler`` is made within a + single application configuration, request handlers will be chained + together. The first request handler factory added will be called + with the default Pyramid request handler as its ``handler`` argument, + the second handler factory added will be called with the result of + the first handler factory, and so on, ad infinitum. The Pyramid + router will use the outermost handler produced by this chain (the + very last handler added) as its handler function. + + By default, the ordering of the chain is controlled entirely by the + relative ordering of calls to ``add_request_handler``. However, the + deploying user can override request handler inclusion and ordering in + his Pyramid configuration using the ``pyramid.request_handlers`` + settings value. When used, this settings value should be a list of + Python dotted names which imply the ordering (and inclusion) of + handler factories in the request handler chain. + + Pyramid will prevent the same request handler factory from being + added to the request handling chain more than once using + configuration conflict detection. If you wish to add the same + handler factory more than once in a configuration, you should either: + a) use a handler that is an *instance* rather than a function or + class, or b) use a function or class with the same logic as the other + it conflicts with but with a different ``__name__`` attribute. + + A user can get a representation of both the implicit request handler + ordering (the ordering specified by calls to ``add_request_handler``) + and the explicit request handler ordering (specified by the + ``pyramid.request_handlers`` setting) orderings using the ``paster + phandlers`` command. Handler factories which are functions or + classes will show up as a standard Python dotted name in the + ``phandlers`` output. Handler factories which are *instances* will + show their module and class name; the Python object id of the + instance will be appended. .. note:: This feature is new as of Pyramid 1.1.1. """ + return self._add_request_handler(handler_factory, explicit=False) + + def _add_request_handler(self, handler_factory, explicit): handler_factory = self.maybe_dotted(handler_factory) - if name is None: - name = '.'.join( - (handler_factory.__module__, handler_factory.__name__)) + if hasattr(handler_factory, '__name__'): + # function or class + name = '.'.join((handler_factory.__module__, + handler_factory.__name__)) + else: + # instance + name = '.'.join(handler_factory.__module__, + handler_factory.__class__.__name__, + str(id(handler_factory))) def register(): registry = self.registry - registry.registerUtility(handler_factory, IRequestHandlerFactory, - name=name) - existing_names = registry.queryUtility(IRequestHandlerFactories, - default=[]) - existing_names.append(name) - registry.registerUtility(existing_names, IRequestHandlerFactories) - self.action(('requesthandler', name), register) + handler_manager = registry.getUtility(IRequestHandlerManager) + handler_manager.add(name, handler_factory, explicit) + self.action(('request_handler', name), register) @action_method def add_subscriber(self, subscriber, iface=None): @@ -3495,3 +3454,24 @@ def isexception(o): ) global_registries = WeakOrderedSet() + +class RequestHandlerManager(object): + implements(IRequestHandlerManager) + def __init__(self): + self.explicit = [] + self.implicit = [] + + def add(self, name, factory, explicit=False): + if explicit: + self.explicit.append((name, factory)) + else: + self.implicit.append((name, factory)) + + def __call__(self, handler, registry): + handler_factories = self.implicit + if self.explicit: + handler_factories = self.explicit + for name, factory in handler_factories[::-1]: + handler = factory(handler, registry) + return handler + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 41e896adf..0c4f4bc76 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -446,23 +446,16 @@ class IMultiDict(Interface): # docs-only interface class IRequest(Interface): """ Request type interface attached to all request objects """ -class IRequestHandlerFactories(Interface): +class IRequestHandlerManager(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.""" + """ Must return a tuple of IReqest, 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 diff --git a/pyramid/router.py b/pyramid/router.py index 5cc144996..b068458be 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -14,8 +14,7 @@ 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.interfaces import IRequestHandlerManager from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -40,17 +39,13 @@ 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 + handler_manager = q(IRequestHandlerManager) + if handler_manager is None: + self.handle_request = exc_view_handler_factory(self.handle_request, + registry) else: - self.handle_request = exception_view_handler_factory( - self.handle_request, registry) + self.handle_request = handler_manager(self.handle_request, registry) + self.root_policy = self.root_factory # b/w compat self.registry = registry settings = registry.settings @@ -195,7 +190,7 @@ class Router(object): finally: manager.pop() -def exception_view_handler_factory(handler, registry): +def exc_view_handler_factory(handler, registry): has_listeners = registry.has_listeners adapters = registry.adapters notify = registry.notify diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 250c53b9a..fbdf9e0b5 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -612,48 +612,48 @@ pyramid.tests.test_config.dummy_include2""", settings = reg.getUtility(ISettings) self.assertEqual(settings['a'], 1) - def test_add_request_handlers_names_distinct(self): - from pyramid.interfaces import IRequestHandlerFactories - from pyramid.interfaces import IRequestHandlerFactory - def factory1(handler, registry): return handler - def factory2(handler, registry): return handler - config = self._makeOne() - config.add_request_handler(factory1, 'name1') - config.add_request_handler(factory2, 'name2') - config.commit() - names = config.registry.queryUtility(IRequestHandlerFactories) - self.assertEqual(names, ['name1', 'name2']) - f1 = config.registry.getUtility(IRequestHandlerFactory, name='name1') - f2 = config.registry.getUtility(IRequestHandlerFactory, name='name2') - self.assertEqual(f1, factory1) - self.assertEqual(f2, factory2) - - def test_add_request_handlers_dottednames(self): - import pyramid.tests - from pyramid.interfaces import IRequestHandlerFactories - from pyramid.interfaces import IRequestHandlerFactory - config = self._makeOne() - config.add_request_handler('pyramid.tests', 'name1') - config.commit() - names = config.registry.queryUtility(IRequestHandlerFactories) - self.assertEqual(names, ['name1']) - f1 = config.registry.getUtility(IRequestHandlerFactory, name='name1') - self.assertEqual(f1, pyramid.tests) - - def test_add_request_handlers_names_overlap(self): - from pyramid.interfaces import IRequestHandlerFactories - from pyramid.interfaces import IRequestHandlerFactory - def factory1(handler, registry): return handler - def factory2(handler, registry): return handler - def factory3(handler, registry): return handler - config = self._makeOne(autocommit=True) - config.add_request_handler(factory1, 'name1') - config.add_request_handler(factory2, 'name2') - config.add_request_handler(factory3, 'name1') - names = config.registry.queryUtility(IRequestHandlerFactories) - self.assertEqual(names, ['name1', 'name2', 'name1']) - f3 = config.registry.getUtility(IRequestHandlerFactory, name='name1') - self.assertEqual(f3, factory3) + ## def test_add_request_handlers_names_distinct(self): + ## from pyramid.interfaces import IRequestHandlerFactories + ## from pyramid.interfaces import IRequestHandlerFactory + ## def factory1(handler, registry): return handler + ## def factory2(handler, registry): return handler + ## config = self._makeOne() + ## config.add_request_handler(factory1, 'name1') + ## config.add_request_handler(factory2, 'name2') + ## config.commit() + ## names = config.registry.queryUtility(IRequestHandlerFactories) + ## self.assertEqual(names, ['name1', 'name2']) + ## f1 = config.registry.getUtility(IRequestHandlerFactory, name='name1') + ## f2 = config.registry.getUtility(IRequestHandlerFactory, name='name2') + ## self.assertEqual(f1, factory1) + ## self.assertEqual(f2, factory2) + + ## def test_add_request_handlers_dottednames(self): + ## import pyramid.tests + ## from pyramid.interfaces import IRequestHandlerFactories + ## from pyramid.interfaces import IRequestHandlerFactory + ## config = self._makeOne() + ## config.add_request_handler('pyramid.tests', 'name1') + ## config.commit() + ## names = config.registry.queryUtility(IRequestHandlerFactories) + ## self.assertEqual(names, ['name1']) + ## f1 = config.registry.getUtility(IRequestHandlerFactory, name='name1') + ## self.assertEqual(f1, pyramid.tests) + + ## def test_add_request_handlers_names_overlap(self): + ## from pyramid.interfaces import IRequestHandlerFactories + ## from pyramid.interfaces import IRequestHandlerFactory + ## def factory1(handler, registry): return handler + ## def factory2(handler, registry): return handler + ## def factory3(handler, registry): return handler + ## config = self._makeOne(autocommit=True) + ## config.add_request_handler(factory1, 'name1') + ## config.add_request_handler(factory2, 'name2') + ## config.add_request_handler(factory3, 'name1') + ## names = config.registry.queryUtility(IRequestHandlerFactories) + ## self.assertEqual(names, ['name1', 'name2', 'name1']) + ## f3 = config.registry.getUtility(IRequestHandlerFactory, name='name1') + ## self.assertEqual(f3, factory3) def test_add_subscriber_defaults(self): from zope.interface import implements @@ -5645,6 +5645,8 @@ class DummyRegistry(object): self.adapters.append((arg, kw)) def queryAdapter(self, *arg, **kw): return self.adaptation + def getUtility(self, *arg, **kw): + return self.utilities[-1] def parse_httpdate(s): import datetime diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index b943f1ee6..633fe63f5 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -134,53 +134,53 @@ class TestRouter(unittest.TestCase): router = self._makeOne() self.assertEqual(router.request_factory, DummyRequestFactory) - def test_request_handler_factories(self): - from pyramid.interfaces import IRequestHandlerFactory - from pyramid.interfaces import IRequestHandlerFactories - L = [] - def handler_factory1(handler, registry): - L.append((handler, registry)) - def wrapper(request): - request.environ['handled'].append('one') - return handler(request) - wrapper.name = 'one' - wrapper.child = handler - return wrapper - def handler_factory2(handler, registry): - L.append((handler, registry)) - def wrapper(request): - request.environ['handled'] = ['two'] - return handler(request) - wrapper.name = 'two' - wrapper.child = handler - return wrapper - self.registry.registerUtility(['one', 'two'], IRequestHandlerFactories) - self.registry.registerUtility(handler_factory1, - IRequestHandlerFactory, name='one') - self.registry.registerUtility(handler_factory2, - IRequestHandlerFactory, name='two') - router = self._makeOne() - self.assertEqual(router.handle_request.name, 'two') - self.assertEqual(router.handle_request.child.name, 'one') - self.assertEqual(router.handle_request.child.child.__name__, - 'handle_request') - from pyramid.response import Response - from pyramid.interfaces import IViewClassifier - from pyramid.interfaces import IResponse - context = DummyContext() - self._registerTraverserFactory(context) - environ = self._makeEnviron() - view = DummyView('abc') - self._registerView(self.config.derive_view(view), '', - IViewClassifier, None, None) - start_response = DummyStartResponse() - def make_response(s): - return Response(s) - router.registry.registerAdapter(make_response, (str,), IResponse) - app_iter = router(environ, start_response) - self.assertEqual(app_iter, ['abc']) - self.assertEqual(start_response.status, '200 OK') - self.assertEqual(environ['handled'], ['two', 'one']) + ## def test_request_handler_factories(self): + ## from pyramid.interfaces import IRequestHandlerFactory + ## from pyramid.interfaces import IRequestHandlerFactories + ## L = [] + ## def handler_factory1(handler, registry): + ## L.append((handler, registry)) + ## def wrapper(request): + ## request.environ['handled'].append('one') + ## return handler(request) + ## wrapper.name = 'one' + ## wrapper.child = handler + ## return wrapper + ## def handler_factory2(handler, registry): + ## L.append((handler, registry)) + ## def wrapper(request): + ## request.environ['handled'] = ['two'] + ## return handler(request) + ## wrapper.name = 'two' + ## wrapper.child = handler + ## return wrapper + ## self.registry.registerUtility(['one', 'two'], IRequestHandlerFactories) + ## self.registry.registerUtility(handler_factory1, + ## IRequestHandlerFactory, name='one') + ## self.registry.registerUtility(handler_factory2, + ## IRequestHandlerFactory, name='two') + ## router = self._makeOne() + ## self.assertEqual(router.handle_request.name, 'two') + ## self.assertEqual(router.handle_request.child.name, 'one') + ## self.assertEqual(router.handle_request.child.child.__name__, + ## 'handle_request') + ## from pyramid.response import Response + ## from pyramid.interfaces import IViewClassifier + ## from pyramid.interfaces import IResponse + ## context = DummyContext() + ## self._registerTraverserFactory(context) + ## environ = self._makeEnviron() + ## view = DummyView('abc') + ## self._registerView(self.config.derive_view(view), '', + ## IViewClassifier, None, None) + ## start_response = DummyStartResponse() + ## def make_response(s): + ## return Response(s) + ## router.registry.registerAdapter(make_response, (str,), IResponse) + ## app_iter = router(environ, start_response) + ## self.assertEqual(app_iter, ['abc']) + ## self.assertEqual(start_response.status, '200 OK') + ## self.assertEqual(environ['handled'], ['two', 'one']) def test_call_traverser_default(self): from pyramid.httpexceptions import HTTPNotFound -- cgit v1.2.3