From d15920e8e4ebf90e0213ad79017f621ea0e9781e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 2 Aug 2011 21:10:32 -0400 Subject: first cut --- pyramid/router.py | 286 +++++++++++++++++++++++++++--------------------------- 1 file changed, 143 insertions(+), 143 deletions(-) diff --git a/pyramid/router.py b/pyramid/router.py index ddec23cdb..73e97a94b 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -57,143 +57,160 @@ class Router(object): self.debug_routematch = settings['debug_routematch'] def handle_request(self, request): + exc = None 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: + + try: # matches finally: manager.pop() + manager = self.threadlocal_manager + threadlocals = { 'registry':registry, 'request':request} + manager.push(threadlocals) + 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 = ( - '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) + '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) - - 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: - 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 there were any view wrappers for the current + # request, use them to wrap the view - response = view_callable(context, request) + 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'] + # handle exceptions raised during root finding and view-exec + except Exception, exc: + # WARNING: do not assign the result of sys.exc_info() to a + # local var here, doing so will cause a leak + attrs['exc_info'] = sys.exc_info() + attrs['exception'] = exc - attrs['exception'] = why - attrs['exc_info'] = sys.exc_info() + # 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'] - for_ = (IExceptionViewClassifier, - request_iface.combined, - providedBy(why)) - view_callable = adapters.lookup(for_, IView, - default=None) + for_ = (IExceptionViewClassifier, + request_iface.combined, + providedBy(exc)) + view_callable = adapters.lookup(for_, IView, + default=None) - if view_callable is None: - raise + if view_callable is None: + raise - response = view_callable(why, request) + response = view_callable(exc, request) - has_listeners and notify(NewResponse(request, response)) + has_listeners and notify(NewResponse(request, response)) - if request.response_callbacks: - request._process_response_callbacks(response) + if request.response_callbacks: + request._process_response_callbacks(response) - return response + return request, response, attrs.get('exc_info') + + finally: + manager.pop() + if request is not None: + try: + if request.finished_callbacks: + request._process_finished_callbacks() + finally: + request.exc_info = None def __call__(self, environ, start_response): """ @@ -203,25 +220,8 @@ class Router(object): within the application registry; call ``start_response`` and return an iterable. """ - registry = self.registry - manager = self.threadlocal_manager - request = None - threadlocals = {'registry':registry, 'request':request} - manager.push(threadlocals) - - try: - try: - request = self.request_factory(environ) - threadlocals['request'] = request - request.registry = registry - response = self.handle_request(request) - finally: - if request is not None: - if request.finished_callbacks: - request._process_finished_callbacks() - request.exc_info = None # avoid leak - - return response(request.environ, start_response) - finally: - manager.pop() + request = self.request_factory(environ) + request.registry = self.registry + request, response = self.handle_request(request)[:2] + return response(request.environ, start_response) -- cgit v1.2.3 From 3c4555ea118884bf0fc3636b5c6e3f851c52ce41 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 5 Aug 2011 05:57:58 -0400 Subject: refactor --- pyramid/router.py | 272 +++++++++++++++++++++++++++--------------------------- 1 file changed, 138 insertions(+), 134 deletions(-) diff --git a/pyramid/router.py b/pyramid/router.py index 73e97a94b..5cc144996 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -48,6 +48,9 @@ class Router(object): name=name) handler = handler_factory(handler, registry) self.handle_request = handler + else: + self.handle_request = exception_view_handler_factory( + self.handle_request, registry) self.root_policy = self.root_factory # b/w compat self.registry = registry settings = registry.settings @@ -57,15 +60,11 @@ class Router(object): self.debug_routematch = settings['debug_routematch'] def handle_request(self, request): - exc = None attrs = request.__dict__ registry = attrs['registry'] - try: # matches finally: manager.pop() - manager = self.threadlocal_manager - threadlocals = { 'registry':registry, 'request':request} - manager.push(threadlocals) - request_iface = IRequest + try: # matches finally: if request is not None + request.request_iface = IRequest context = None routes_mapper = self.routes_mapper debug_routematch = self.debug_routematch @@ -74,143 +73,107 @@ class Router(object): 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: + 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 = ( - '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) + '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) - 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 - - response = view_callable(context, request) - - # handle exceptions raised during root finding and view-exec - except Exception, exc: - # WARNING: do not assign the result of sys.exc_info() to a - # local var here, doing so will cause a leak - attrs['exc_info'] = sys.exc_info() - attrs['exception'] = exc - # 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'] - - for_ = (IExceptionViewClassifier, - request_iface.combined, - providedBy(exc)) - view_callable = adapters.lookup(for_, IView, - default=None) - - if view_callable is None: - raise - - response = view_callable(exc, request) + request.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.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: + response = view_callable(context, request) has_listeners and notify(NewResponse(request, response)) if request.response_callbacks: request._process_response_callbacks(response) - return request, response, attrs.get('exc_info') + return request, response finally: - manager.pop() - if request is not None: - try: - if request.finished_callbacks: - request._process_finished_callbacks() - finally: - request.exc_info = None + if request is not None and request.finished_callbacks: + request._process_finished_callbacks() def __call__(self, environ, start_response): """ @@ -220,8 +183,49 @@ class Router(object): within the application registry; call ``start_response`` and return an iterable. """ + registry = self.registry request = self.request_factory(environ) - request.registry = self.registry - request, response = self.handle_request(request)[:2] - return response(request.environ, start_response) + threadlocals = {'registry':registry, 'request':request} + manager = self.threadlocal_manager + manager.push(threadlocals) + try: + request.registry = registry + request, response = self.handle_request(request) + return response(request.environ, start_response) + finally: + manager.pop() + +def exception_view_handler_factory(handler, registry): + has_listeners = registry.has_listeners + adapters = registry.adapters + notify = registry.notify + + def exception_view_handler(request): + attrs = request.__dict__ + try: + request, response = handler(request) + except Exception, exc: + # WARNING: do not assign the result of sys.exc_info() to a + # local var here, doing so will cause a leak + attrs['exc_info'] = sys.exc_info() + attrs['exception'] = exc + # 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'] + request_iface = attrs['request_iface'] + provides = providedBy(exc) + for_ = (IExceptionViewClassifier, request_iface.combined, provides) + view_callable = adapters.lookup(for_, IView, default=None) + if view_callable is None: + raise + response = view_callable(exc, request) + has_listeners and notify(NewResponse(request, response)) + finally: + attrs['exc_info'] = None + attrs['exception'] = None + return request, response + + return exception_view_handler -- cgit v1.2.3 From 654a67df97623eb1890999584f0cc0299fa1944c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 5 Aug 2011 08:48:20 -0400 Subject: add toposort and tests --- pyramid/tests/test_util.py | 66 +++++++++++++++++++++++++++++++++++++++ pyramid/util.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 247b61dad..8b6f47478 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -246,5 +246,71 @@ class Test_WeakOrderedSet(unittest.TestCase): self.assertEqual(list(wos), []) self.assertEqual(wos.last, None) +class Test_topological_sort(unittest.TestCase): + def _callFUT(self, items, partial_order, ignore_missing_partials=True): + from pyramid.util import topological_sort + return topological_sort(items, partial_order, ignore_missing_partials) + + def test_no_items_no_order(self): + result = self._callFUT([], []) + self.assertEqual(result, []) + + def test_no_order(self): + result = self._callFUT(['a', 'b'], []) + self.assertEqual(result, ['a', 'b']) + + def test_partial_order(self): + result = self._callFUT(['a', 'b', 'c'], [('b', 'c')]) + self.assertEqual(result, ['a', 'b', 'c']) + + def test_partial_order2(self): + result = self._callFUT(['a', 'b', 'c'], [('a', 'b'), ('b', 'c')]) + self.assertEqual(result, ['a', 'b', 'c']) + + def test_partial_order3(self): + result = self._callFUT(['a', 'b', 'c'], [('a', 'c'), ('b', 'a')]) + self.assertEqual(result, ['b', 'a', 'c']) + + def test_partial_order4(self): + result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c')]) + self.assertEqual(result, ['a', 'b', 'd', 'c']) + + def test_partial_order_missing_partial_a(self): + result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c'), ('f', 'c')]) + self.assertEqual(result, ['a', 'b', 'd', 'f', 'c']) + + def test_partial_order_missing_partial_b(self): + result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c'), ('c', 'f')]) + self.assertEqual(result, ['a', 'b', 'd', 'c', 'f']) + + def test_cycle_direct(self): + from pyramid.util import CyclicDependencyError + self.assertRaises( + CyclicDependencyError, + self._callFUT, + ['a', 'b', 'c', 'd'], [('c', 'd'), ('d', 'c')]) + + def test_cycle_indirect(self): + from pyramid.util import CyclicDependencyError + self.assertRaises( + CyclicDependencyError, + self._callFUT, + ['a', 'b', 'c', 'd', 'e'], + [('c', 'd'), ('d', 'e'), ('e', 'c')]) + +class TestCyclicDependencyError(unittest.TestCase): + def _makeOne(self, cycles): + from pyramid.util import CyclicDependencyError + return CyclicDependencyError(cycles) + + def test___str__(self): + exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) + result = str(exc) + self.assertEqual(result, + "'a' depends on ['c', 'd']; 'c' depends on ['a']") + +def reraise(exc): + raise exc + class Dummy(object): pass diff --git a/pyramid/util.py b/pyramid/util.py index 7fd1b0dc6..fa885b81e 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -206,3 +206,80 @@ class WeakOrderedSet(object): if self._order: oid = self._order[-1] return self._items[oid]() + +def topological_sort(items, partial_order, ignore_missing_partials=True): + """ + Stolen from http://www.bitinformation.com/art/python_topsort.html + (modified to sort initial roots in items order, and to ignore missing + partials). + + Given the example list of items ['item2', 'item3', 'item1', + 'item4'] and a 'partial order' list in the form [(item1, item2), + (item2, item3)], where the example tuples indicate that 'item1' + should precede 'item2' and 'item2' should precede 'item3', return + the sorted list of items ['item1', 'item2', 'item3', 'item4']. + Note that since 'item4' is not mentioned in the partial ordering + list, it will be at an arbitrary position in the returned list. + """ + def add_node(graph, node, roots): + if not graph.has_key(node): + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(graph, fromnode, tonode, roots): + graph[fromnode].append(tonode) + graph[tonode][0] = graph[tonode][0] + 1 + if tonode in roots: + roots.remove(tonode) + + graph = {} + roots = [] + + for v in items: + add_node(graph, v, roots) + + for a, b in partial_order: + if ignore_missing_partials: + # don't fail if a value is present in the partial_order + # list but missing in items. In this mode, we fake up a + # value instead of raising a KeyError when trying to use + # add_arc. The result will contain the faked item. + if not graph.has_key(a): + add_node(graph, a, roots) + elif not graph.has_key(b): + add_node(graph, b, roots) + add_arc(graph, a, b, roots) + + sorted = [] + + while roots: + root = roots.pop(0) + sorted.append(root) + for child in graph[root][1:]: + graph[child][0] = graph[child][0] - 1 + if graph[child][0] == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + return sorted + +class CyclicDependencyError(ConfigurationError): + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r depends on %r' % (dependent, dependees)) + msg = '; '.join(L) + return msg -- cgit v1.2.3 From 2df6bee0cf734187b70f3362affc63d04809c581 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 5 Aug 2011 08:50:25 -0400 Subject: remove unused code --- pyramid/tests/test_util.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 8b6f47478..2dd2dbd7f 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -309,8 +309,5 @@ class TestCyclicDependencyError(unittest.TestCase): self.assertEqual(result, "'a' depends on ['c', 'd']; 'c' depends on ['a']") -def reraise(exc): - raise exc - class Dummy(object): pass -- cgit v1.2.3 From e25fae8ebdc5b77ec1578186d11653430835f76e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 5 Aug 2011 11:57:26 -0400 Subject: add docs, return only items in the list --- pyramid/config.py | 116 +++++++++++++++++++++++++++++++++++++++------ pyramid/tests/test_util.py | 4 +- pyramid/util.py | 2 +- 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 159422c22..e9f2dd2a5 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -898,17 +898,18 @@ class Configurator(object): return self._derive_view(view, attr=attr, renderer=renderer) @action_method - def add_request_handler(self, handler_factory, name): + def add_request_handler(self, handler_factory, before=None, + after=None, name=None): """ Add a request handler factory. A request handler factory is used to - wrap the Pyramid router's primary request handling function. This is + 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. A request handler factory (passed as ``handler_factory``) must be a - callable (or a :term:`dotted Python name` to a callable) which + callable (or a :term:`dotted Python name` to such 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 @@ -940,29 +941,116 @@ class Configurator(object): # if timing support is not enabled, return the original handler return handler - config.add_request_handler(timing_handler_factory, 'timing') + config.add_request_handler(timing_handler_factory) 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 + 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). + or this is an autocommitting configurator). By default, a name will + be the :term:`Python dotted name` of the object passed as + ``handler_factory``. .. note:: This feature is new as of Pyramid 1.1.1. """ handler_factory = self.maybe_dotted(handler_factory) + if name is None: + name = '.'.join( + (handler_factory.__module__, handler_factory.__name__)) def register(): registry = self.registry registry.registerUtility(handler_factory, IRequestHandlerFactory, diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 2dd2dbd7f..62477ed9f 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -277,11 +277,11 @@ class Test_topological_sort(unittest.TestCase): def test_partial_order_missing_partial_a(self): result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c'), ('f', 'c')]) - self.assertEqual(result, ['a', 'b', 'd', 'f', 'c']) + self.assertEqual(result, ['a', 'b', 'd', 'c']) def test_partial_order_missing_partial_b(self): result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c'), ('c', 'f')]) - self.assertEqual(result, ['a', 'b', 'd', 'c', 'f']) + self.assertEqual(result, ['a', 'b', 'd', 'c']) def test_cycle_direct(self): from pyramid.util import CyclicDependencyError diff --git a/pyramid/util.py b/pyramid/util.py index fa885b81e..5de8aa37a 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -268,7 +268,7 @@ def topological_sort(items, partial_order, ignore_missing_partials=True): cycledeps[k] = v[1:] raise CyclicDependencyError(cycledeps) - return sorted + return [ x for x in sorted if x in items ] class CyclicDependencyError(ConfigurationError): def __init__(self, cycles): -- cgit v1.2.3 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 From 28b40471d5a3558ee0dfe1d445c5b8bec0c1cf0b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 16:31:22 -0400 Subject: make tests pass; remove toposort stuff --- pyramid/config.py | 14 +++---- pyramid/interfaces.py | 2 +- pyramid/router.py | 6 +-- pyramid/tests/test_config.py | 4 +- pyramid/tests/test_router.py | 93 ++++++++++++++++++++++---------------------- pyramid/tests/test_util.py | 63 ------------------------------ pyramid/util.py | 76 ------------------------------------ 7 files changed, 59 insertions(+), 199 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 5288d1d48..914bfbd53 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -38,7 +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 IRequestHandlerManager +from pyramid.interfaces import IRequestHandlers from pyramid.interfaces import IResponse from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest @@ -735,8 +735,8 @@ class Configurator(object): from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) # add a handler manager - handler_manager = RequestHandlerManager() - registry.registerUtility(handler_manager, IRequestHandlerManager) + handler_manager = RequestHandlers() + registry.registerUtility(handler_manager, IRequestHandlers) self._add_request_handler('pyramid.router.exc_view_handler_factory', explicit=False) @@ -1016,7 +1016,7 @@ class Configurator(object): str(id(handler_factory))) def register(): registry = self.registry - handler_manager = registry.getUtility(IRequestHandlerManager) + handler_manager = registry.getUtility(IRequestHandlers) handler_manager.add(name, handler_factory, explicit) self.action(('request_handler', name), register) @@ -3455,8 +3455,8 @@ def isexception(o): global_registries = WeakOrderedSet() -class RequestHandlerManager(object): - implements(IRequestHandlerManager) +class RequestHandlers(object): + implements(IRequestHandlers) def __init__(self): self.explicit = [] self.implicit = [] @@ -3471,7 +3471,7 @@ class RequestHandlerManager(object): handler_factories = self.implicit if self.explicit: handler_factories = self.explicit - for name, factory in handler_factories[::-1]: + for name, factory in handler_factories: handler = factory(handler, registry) return handler diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 0c4f4bc76..c4bacd46d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -446,7 +446,7 @@ class IMultiDict(Interface): # docs-only interface class IRequest(Interface): """ Request type interface attached to all request objects """ -class IRequestHandlerManager(Interface): +class IRequestHandlers(Interface): """ Marker interface for utility registration representing the ordered set of a configuration's request handler factories""" diff --git a/pyramid/router.py b/pyramid/router.py index b068458be..d8f5a08da 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -14,7 +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 IRequestHandlerManager +from pyramid.interfaces import IRequestHandlers from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -39,7 +39,7 @@ class Router(object): self.root_factory = q(IRootFactory, default=DefaultRootFactory) self.routes_mapper = q(IRoutesMapper) self.request_factory = q(IRequestFactory, default=Request) - handler_manager = q(IRequestHandlerManager) + handler_manager = q(IRequestHandlers) if handler_manager is None: self.handle_request = exc_view_handler_factory(self.handle_request, registry) @@ -219,7 +219,7 @@ def exc_view_handler_factory(handler, registry): has_listeners and notify(NewResponse(request, response)) finally: attrs['exc_info'] = None - attrs['exception'] = None + return request, response return exception_view_handler diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index fbdf9e0b5..ebc3f85cc 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -336,6 +336,7 @@ class ConfiguratorTests(unittest.TestCase): reg = DummyRegistry() config = self._makeOne(reg) config.add_view = lambda *arg, **kw: False + config._add_request_handler = lambda *arg, **kw: False config.setup_registry() self.assertEqual(reg.has_listeners, True) @@ -347,6 +348,7 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(reg) views = [] config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config._add_request_handler = lambda *arg, **kw: False config.setup_registry() self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) @@ -5645,8 +5647,6 @@ 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 633fe63f5..6921de179 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -134,53 +134,52 @@ 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 IRequestHandlers + from pyramid.config import RequestHandlers + handler_manager = RequestHandlers() + self.registry.registerUtility(handler_manager, IRequestHandlers) + 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 + handler_manager.add('one', handler_factory1) + handler_manager.add('two', handler_factory2) + 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 diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 62477ed9f..247b61dad 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -246,68 +246,5 @@ class Test_WeakOrderedSet(unittest.TestCase): self.assertEqual(list(wos), []) self.assertEqual(wos.last, None) -class Test_topological_sort(unittest.TestCase): - def _callFUT(self, items, partial_order, ignore_missing_partials=True): - from pyramid.util import topological_sort - return topological_sort(items, partial_order, ignore_missing_partials) - - def test_no_items_no_order(self): - result = self._callFUT([], []) - self.assertEqual(result, []) - - def test_no_order(self): - result = self._callFUT(['a', 'b'], []) - self.assertEqual(result, ['a', 'b']) - - def test_partial_order(self): - result = self._callFUT(['a', 'b', 'c'], [('b', 'c')]) - self.assertEqual(result, ['a', 'b', 'c']) - - def test_partial_order2(self): - result = self._callFUT(['a', 'b', 'c'], [('a', 'b'), ('b', 'c')]) - self.assertEqual(result, ['a', 'b', 'c']) - - def test_partial_order3(self): - result = self._callFUT(['a', 'b', 'c'], [('a', 'c'), ('b', 'a')]) - self.assertEqual(result, ['b', 'a', 'c']) - - def test_partial_order4(self): - result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c')]) - self.assertEqual(result, ['a', 'b', 'd', 'c']) - - def test_partial_order_missing_partial_a(self): - result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c'), ('f', 'c')]) - self.assertEqual(result, ['a', 'b', 'd', 'c']) - - def test_partial_order_missing_partial_b(self): - result = self._callFUT(['a', 'b', 'c', 'd'], [('d', 'c'), ('c', 'f')]) - self.assertEqual(result, ['a', 'b', 'd', 'c']) - - def test_cycle_direct(self): - from pyramid.util import CyclicDependencyError - self.assertRaises( - CyclicDependencyError, - self._callFUT, - ['a', 'b', 'c', 'd'], [('c', 'd'), ('d', 'c')]) - - def test_cycle_indirect(self): - from pyramid.util import CyclicDependencyError - self.assertRaises( - CyclicDependencyError, - self._callFUT, - ['a', 'b', 'c', 'd', 'e'], - [('c', 'd'), ('d', 'e'), ('e', 'c')]) - -class TestCyclicDependencyError(unittest.TestCase): - def _makeOne(self, cycles): - from pyramid.util import CyclicDependencyError - return CyclicDependencyError(cycles) - - def test___str__(self): - exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) - result = str(exc) - self.assertEqual(result, - "'a' depends on ['c', 'd']; 'c' depends on ['a']") - class Dummy(object): pass diff --git a/pyramid/util.py b/pyramid/util.py index 5de8aa37a..c0e7640c4 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -207,79 +207,3 @@ class WeakOrderedSet(object): oid = self._order[-1] return self._items[oid]() -def topological_sort(items, partial_order, ignore_missing_partials=True): - """ - Stolen from http://www.bitinformation.com/art/python_topsort.html - (modified to sort initial roots in items order, and to ignore missing - partials). - - Given the example list of items ['item2', 'item3', 'item1', - 'item4'] and a 'partial order' list in the form [(item1, item2), - (item2, item3)], where the example tuples indicate that 'item1' - should precede 'item2' and 'item2' should precede 'item3', return - the sorted list of items ['item1', 'item2', 'item3', 'item4']. - Note that since 'item4' is not mentioned in the partial ordering - list, it will be at an arbitrary position in the returned list. - """ - def add_node(graph, node, roots): - if not graph.has_key(node): - roots.append(node) - graph[node] = [0] # 0 = number of arcs coming into this node - - def add_arc(graph, fromnode, tonode, roots): - graph[fromnode].append(tonode) - graph[tonode][0] = graph[tonode][0] + 1 - if tonode in roots: - roots.remove(tonode) - - graph = {} - roots = [] - - for v in items: - add_node(graph, v, roots) - - for a, b in partial_order: - if ignore_missing_partials: - # don't fail if a value is present in the partial_order - # list but missing in items. In this mode, we fake up a - # value instead of raising a KeyError when trying to use - # add_arc. The result will contain the faked item. - if not graph.has_key(a): - add_node(graph, a, roots) - elif not graph.has_key(b): - add_node(graph, b, roots) - add_arc(graph, a, b, roots) - - sorted = [] - - while roots: - root = roots.pop(0) - sorted.append(root) - for child in graph[root][1:]: - graph[child][0] = graph[child][0] - 1 - if graph[child][0] == 0: - roots.insert(0, child) - del graph[root] - - if graph: - # loop in input - cycledeps = {} - for k, v in graph.items(): - cycledeps[k] = v[1:] - raise CyclicDependencyError(cycledeps) - - return [ x for x in sorted if x in items ] - -class CyclicDependencyError(ConfigurationError): - def __init__(self, cycles): - self.cycles = cycles - - def __str__(self): - L = [] - cycles = self.cycles - for cycle in cycles: - dependent = cycle - dependees = cycles[cycle] - L.append('%r depends on %r' % (dependent, dependees)) - msg = '; '.join(L) - return msg -- cgit v1.2.3 From 4f321df3ae4f09445555b653ef421f6b6fd97069 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 16:35:26 -0400 Subject: dont pop pyramid.includes or pyramid.request_handlers from the settings --- pyramid/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 914bfbd53..6a7b4d328 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -717,9 +717,9 @@ class Configurator(object): other settings using the configurator's current registry, as per the descriptions in the Configurator constructor.""" if settings: - includes = settings.pop('pyramid.include', '') + includes = settings.get('pyramid.include', '') includes = [x.strip() for x in includes.splitlines()] - expl_handler_factories = settings.pop('pyramid.request_handlers','') + expl_handler_factories = settings.get('pyramid.request_handlers','') expl_handler_factories = [x.strip() for x in expl_handler_factories.splitlines()] else: -- cgit v1.2.3 From b4843bc087524320460fb3fca9d33688cafa0dbb Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 17:38:37 -0400 Subject: requesthandlers->tweens; add ptweens command --- pyramid/config.py | 183 +++++++++++++++++++++++-------------------- pyramid/interfaces.py | 4 +- pyramid/paster.py | 66 +++++++++++++++- pyramid/router.py | 14 ++-- pyramid/tests/test_config.py | 4 +- pyramid/tests/test_router.py | 24 +++--- setup.py | 1 + 7 files changed, 185 insertions(+), 111 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 6a7b4d328..259be7688 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -38,7 +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 IRequestHandlers +from pyramid.interfaces import ITweens from pyramid.interfaces import IResponse from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest @@ -719,12 +719,11 @@ class Configurator(object): if settings: includes = settings.get('pyramid.include', '') includes = [x.strip() for x in includes.splitlines()] - expl_handler_factories = settings.get('pyramid.request_handlers','') - expl_handler_factories = [x.strip() for x in - expl_handler_factories.splitlines()] + expl_tweens = settings.get('pyramid.tweens','') + expl_tweens = [x.strip() for x in expl_tweens.splitlines()] else: includes = [] - expl_handler_factories = [] + expl_tweens = [] registry = self.registry self._fix_registry() self._set_settings(settings) @@ -735,10 +734,10 @@ class Configurator(object): from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) # add a handler manager - handler_manager = RequestHandlers() - registry.registerUtility(handler_manager, IRequestHandlers) - self._add_request_handler('pyramid.router.exc_view_handler_factory', - explicit=False) + tweens = Tweens() + registry.registerUtility(tweens, ITweens) + self._add_tween('pyramid.router.exc_view_tween_factory', + explicit=False) if debug_logger is None: debug_logger = logging.getLogger(self.package_name) @@ -785,8 +784,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 factory in expl_tweens: + self._add_tween(factory, explicit=True) for inc in includes: self.include(inc) @@ -908,31 +907,32 @@ class Configurator(object): return self._derive_view(view, attr=attr, renderer=renderer) @action_method - def add_request_handler(self, handler_factory): + def add_tween(self, tween_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 - example, view timing support and as a convenient place to hang + Add a 'tween factory'. A 'tween' (think: 'between') is a bit of code + that sits between the Pyramid router's main request handling function + and the upstream WSGI component that uses :app:`Pyramid` as its + 'app'. This is a feature that may be used by framework extensions, + to provide, for example, Pyramid-specific view timing support bookkeeping code that examines exceptions before they are returned to - the WSGI server. Request handlers behave a bit like :mod:`WSGI` + the upstream WSGI application. Tweens 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 + which they have access to the 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 - 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 tween factory (passed as ``tween_factory``) must be a callable (or + a :term:`dotted Python name` to such a callable) which accepts two + arguments: ``handler`` and ``registry``. ``handler`` will be the + either the main Pyramid request handling function or another tween + (if more than one tween is configured into the request handling + chain). ``registry`` will be the Pyramid :term:`application + registry` represented by this Configurator. A tween factory must + return a tween when it is called. - A request handler accepts a :term:`request` object and returns a - :term:`response` object. + A tween accepts a :term:`request` object and returns a two-tuple + consisting of a :term:`request` object and a :term:`response` object. - Here's an example creating a handler factory and registering the - handler factory: + Here's an example creating a tween factory and registering it: .. code-block:: python @@ -942,83 +942,92 @@ class Configurator(object): log = logging.getLogger(__name__) - def timing_handler_factory(handler, registry): + def timing_tween_factory(handler, registry): if asbool(registry.settings.get('do_timing')): # if timing support is enabled, return a wrapper - def timing_handler(request): + def timing_tween(request): start = time.time() try: - response = handler(request) + request, response = handler(request) finally: end = time.time() log.debug('The request took %s seconds' % (end - start)) - return response - return timing_handler + return request, response + return timing_tween # if timing support is not enabled, return the original # handler return handler - config.add_request_handler(timing_handler_factory) + config.add_tween(timing_tween_factory) - The ``request`` argument to the handler will be the request created - by Pyramid's router when it receives a WSGI request. + The ``request`` argument to a tween will be the request created by + Pyramid's router when it receives a WSGI request. - 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. + If more than one call to ``add_tween`` is made within a single + application configuration, the added tweens will be chained together. + The first tween factory added will be called with the default Pyramid + request handler as its ``handler`` argument, the second tween factory + added will be called with the result of the first tween factory as + its ``handler`` argument, and so on, ad infinitum. The Pyramid router + will use the outermost tween produced by this chain (the very last + tween added) as its request 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. + relative ordering of calls to ``add_tween``. However, the deploying + user can override tween inclusion and ordering in his Pyramid + configuration using the ``pyramid.tweens`` settings value. When + used, this settings value should be a list of Python dotted names + which imply the ordering (and inclusion) of tween factories in the + tween chain. + + Pyramid will prevent the same tween factory from being added to the + tween chain more than once using configuration conflict detection. + If you wish to add the same tween factory more than once in a + configuration, you should either: a) use a tween factory that is an + *instance* rather than a function or class, or b) use a function or + class as a tween factory with the same logic as the other tween + factory it conflicts with but with a different ``__name__`` + attribute. + + A user can get a representation of both the implicit tween ordering + (the ordering specified by calls to ``add_tween``) and the explicit + request handler ordering (specified by the ``pyramid.tweens`` + setting) orderings using the ``paster ptweens`` command. Handler + factories which are functions or classes will show up as a standard + Python dotted name in the ``ptweens`` output. Tween 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) + return self._add_tween(tween_factory, explicit=False) - def _add_request_handler(self, handler_factory, explicit): - handler_factory = self.maybe_dotted(handler_factory) - if hasattr(handler_factory, '__name__'): + # XXX temporary bw compat for debugtoolbar + def add_request_handler(self, factory, name): + return self._add_tween(factory, explicit=False) + + def _add_tween(self, tween_factory, explicit): + tween_factory = self.maybe_dotted(tween_factory) + if (hasattr(tween_factory, '__name__') and + hasattr(tween_factory, '__module__')): # function or class - name = '.'.join((handler_factory.__module__, - handler_factory.__name__)) - else: + name = '.'.join([tween_factory.__module__, + tween_factory.__name__]) + elif hasattr(tween_factory, '__module__'): # instance - name = '.'.join(handler_factory.__module__, - handler_factory.__class__.__name__, - str(id(handler_factory))) + name = '.'.join([tween_factory.__module__, + tween_factory.__class__.__name__, + str(id(tween_factory))]) + else: + raise ConfigurationError( + 'A tween factory must be a class, an instance, or a function; ' + '%s is not a suitable tween factory' % tween_factory) def register(): registry = self.registry - handler_manager = registry.getUtility(IRequestHandlers) - handler_manager.add(name, handler_factory, explicit) - self.action(('request_handler', name), register) + handler_manager = registry.getUtility(ITweens) + handler_manager.add(name, tween_factory, explicit) + self.action(('tween', name), register) @action_method def add_subscriber(self, subscriber, iface=None): @@ -3455,8 +3464,8 @@ def isexception(o): global_registries = WeakOrderedSet() -class RequestHandlers(object): - implements(IRequestHandlers) +class Tweens(object): + implements(ITweens) def __init__(self): self.explicit = [] self.implicit = [] @@ -3468,10 +3477,10 @@ class RequestHandlers(object): self.implicit.append((name, factory)) def __call__(self, handler, registry): - handler_factories = self.implicit + factories = self.implicit if self.explicit: - handler_factories = self.explicit - for name, factory in handler_factories: + factories = self.explicit + for name, factory in factories: handler = factory(handler, registry) return handler diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index c4bacd46d..d97632018 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -446,9 +446,9 @@ class IMultiDict(Interface): # docs-only interface class IRequest(Interface): """ Request type interface attached to all request objects """ -class IRequestHandlers(Interface): +class ITweens(Interface): """ Marker interface for utility registration representing the ordered - set of a configuration's request handler factories""" + set of a configuration's tween factories""" class IRequestHandler(Interface): """ """ diff --git a/pyramid/paster.py b/pyramid/paster.py index 3143fa91e..54f5a51a6 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -9,8 +9,8 @@ from paste.deploy import loadapp from paste.script.command import Command from pyramid.interfaces import IMultiView +from pyramid.interfaces import ITweens -from pyramid.scripting import get_root from pyramid.scripting import prepare from pyramid.util import DottedNameResolver @@ -534,3 +534,67 @@ class PViewsCommand(PCommand): self.out(" Not found.") self.out('') + +class PTweensCommand(PCommand): + """Print all implicit and explicit :term:`tween` objects used by a + Pyramid application. The handler output includes whether the system is + using an explicit tweens ordering (will be true when the + ``pyramid.tweens`` setting is used) or an implicit tweens ordering (will + be true when the ``pyramid.tweens`` setting is *not* used). + + This command accepts one positional argument: + + ``config_uri`` -- specifies the PasteDeploy config file to use for the + interactive shell. The format is ``inifile#name``. If the name is left + off, ``main`` will be assumed. + + Example:: + + $ paster ptweens myapp.ini#main + + """ + summary = "Print all tweens related to a Pyramid application" + min_args = 1 + max_args = 1 + stdout = sys.stdout + + parser = Command.standard_parser(simulate=True) + + def _get_tweens(self, registry): + from pyramid.config import Configurator + config = Configurator(registry = registry) + return config.registry.queryUtility(ITweens) + + def out(self, msg): # pragma: no cover + print msg + + def command(self): + config_uri = self.args[0] + env = self.bootstrap[0](config_uri) + registry = env['registry'] + tweens = self._get_tweens(registry) + if tweens is not None: + ordering = [] + if tweens.explicit: + self.out('"pyramid.tweens" config value set ' + '(explicitly ordered tweens used)') + self.out('') + ordering.append((tweens.explicit, + 'Explicit Tween Chain (used)')) + ordering.append((tweens.implicit, + 'Implicit Tween Chain (not used)')) + else: + self.out('"pyramid.tweens" config value NOT set ' + '(implicitly ordered tweens used)') + self.out('') + ordering.append((tweens.implicit, '')) + for L, title in ordering: + if title: + self.out(title) + self.out('') + fmt = '%-8s %-30s' + self.out(fmt % ('Position', 'Name')) + self.out(fmt % ('-'*len('Position'), '-'*len('Name'))) + for pos, (name, item) in enumerate(L): + self.out(fmt % (pos, name)) + self.out('') diff --git a/pyramid/router.py b/pyramid/router.py index d8f5a08da..8cac733fb 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -14,7 +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 IRequestHandlers +from pyramid.interfaces import ITweens from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -39,12 +39,12 @@ class Router(object): self.root_factory = q(IRootFactory, default=DefaultRootFactory) self.routes_mapper = q(IRoutesMapper) self.request_factory = q(IRequestFactory, default=Request) - handler_manager = q(IRequestHandlers) - if handler_manager is None: - self.handle_request = exc_view_handler_factory(self.handle_request, - registry) + tweens = q(ITweens) + if tweens is None: + self.handle_request = exc_view_tween_factory(self.handle_request, + registry) else: - self.handle_request = handler_manager(self.handle_request, registry) + self.handle_request = tweens(self.handle_request, registry) self.root_policy = self.root_factory # b/w compat self.registry = registry @@ -190,7 +190,7 @@ class Router(object): finally: manager.pop() -def exc_view_handler_factory(handler, registry): +def exc_view_tween_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 ebc3f85cc..ef2bb24d7 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -336,7 +336,7 @@ class ConfiguratorTests(unittest.TestCase): reg = DummyRegistry() config = self._makeOne(reg) config.add_view = lambda *arg, **kw: False - config._add_request_handler = lambda *arg, **kw: False + config._add_tween = lambda *arg, **kw: False config.setup_registry() self.assertEqual(reg.has_listeners, True) @@ -348,7 +348,7 @@ class ConfiguratorTests(unittest.TestCase): config = self._makeOne(reg) views = [] config.add_view = lambda *arg, **kw: views.append((arg, kw)) - config._add_request_handler = lambda *arg, **kw: False + config._add_tween = lambda *arg, **kw: False config.setup_registry() self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 6921de179..6b0354468 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -134,13 +134,16 @@ class TestRouter(unittest.TestCase): router = self._makeOne() self.assertEqual(router.request_factory, DummyRequestFactory) - def test_request_handler_factories(self): - from pyramid.interfaces import IRequestHandlers - from pyramid.config import RequestHandlers - handler_manager = RequestHandlers() - self.registry.registerUtility(handler_manager, IRequestHandlers) + def test_tween_factories(self): + from pyramid.interfaces import ITweens + from pyramid.config import Tweens + from pyramid.response import Response + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponse + tweens = Tweens() + self.registry.registerUtility(tweens, ITweens) L = [] - def handler_factory1(handler, registry): + def tween_factory1(handler, registry): L.append((handler, registry)) def wrapper(request): request.environ['handled'].append('one') @@ -148,7 +151,7 @@ class TestRouter(unittest.TestCase): wrapper.name = 'one' wrapper.child = handler return wrapper - def handler_factory2(handler, registry): + def tween_factory2(handler, registry): L.append((handler, registry)) def wrapper(request): request.environ['handled'] = ['two'] @@ -156,16 +159,13 @@ class TestRouter(unittest.TestCase): wrapper.name = 'two' wrapper.child = handler return wrapper - handler_manager.add('one', handler_factory1) - handler_manager.add('two', handler_factory2) + tweens.add('one', tween_factory1) + tweens.add('two', tween_factory2) 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() diff --git a/setup.py b/setup.py index 109be6951..5e362ea86 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ setup(name='pyramid', pshell=pyramid.paster:PShellCommand proutes=pyramid.paster:PRoutesCommand pviews=pyramid.paster:PViewsCommand + ptweens=pyramid.paster:PTweensCommand [console_scripts] bfg2pyramid = pyramid.fixers.fix_bfg_imports:main """ -- cgit v1.2.3 From dc7122f8bb5033d0cd0e95f5adfa66c4499f140e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 17:41:30 -0400 Subject: add glossary entry for tween --- docs/glossary.rst | 11 +++++++++++ pyramid/config.py | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index c6705fdc5..ccb62bbc8 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -917,3 +917,14 @@ Glossary PyPy is an "alternative implementation of the Python language":http://pypy.org/ + tween + A bit of code that sits between the Pyramid router's main request + handling function and the upstream WSGI component that uses + :app:`Pyramid` as its 'app'. A tween may be used by Pyramid framework + extensions, to provide, for example, Pyramid-specific view timing + support bookkeeping code that examines exceptions before they are + returned to the upstream WSGI application. Tweens behave a bit like + :mod:`WSGI` 'middleware' but they have the benefit of running in a + context in which they have access to the Pyramid :term:`application + registry` as well as the Pyramid rendering machinery. + diff --git a/pyramid/config.py b/pyramid/config.py index 259be7688..b8382e6a7 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -909,16 +909,16 @@ class Configurator(object): @action_method def add_tween(self, tween_factory): """ - Add a 'tween factory'. A 'tween' (think: 'between') is a bit of code - that sits between the Pyramid router's main request handling function - and the upstream WSGI component that uses :app:`Pyramid` as its - 'app'. This is a feature that may be used by framework extensions, - to provide, for example, Pyramid-specific view timing support - bookkeeping code that examines exceptions before they are returned to - the upstream WSGI application. Tweens behave a bit like :mod:`WSGI` - 'middleware' but they have the benefit of running in a context in - which they have access to the Pyramid :term:`application registry` as - well as the Pyramid rendering machinery. + Add a 'tween factory'. A :term`tween` (think: 'between') is a bit of + code that sits between the Pyramid router's main request handling + function and the upstream WSGI component that uses :app:`Pyramid` as + its 'app'. This is a feature that may be used by Pyramid framework + extensions, to provide, for example, Pyramid-specific view timing + support bookkeeping code that examines exceptions before they are + returned to the upstream WSGI application. Tweens behave a bit like + :mod:`WSGI` 'middleware' but they have the benefit of running in a + context in which they have access to the Pyramid :term:`application + registry` as well as the Pyramid rendering machinery. A tween factory (passed as ``tween_factory``) must be a callable (or a :term:`dotted Python name` to such a callable) which accepts two -- cgit v1.2.3 From dccaa2ff3dec57a1166ad10b545336b227a1f9d7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 17:43:42 -0400 Subject: add glossary entry for tween --- pyramid/tests/test_config.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index ef2bb24d7..3e4895277 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -614,21 +614,16 @@ 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_tweens_names_distinct(self): + from pyramid.interfaces import ITweens + def factory1(handler, registry): return handler + def factory2(handler, registry): return handler + config = self._makeOne() + config.add_tween(factory1) + config.add_tween(factory2) + config.commit() + tweens = config.registry.queryUtility(ITweens) + self.assertEqual(tweens.implicit, None) ## def test_add_request_handlers_dottednames(self): ## import pyramid.tests -- cgit v1.2.3 From 46adcd6bd42318bea3c84bc8d86e3395d6d026d9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 17:52:57 -0400 Subject: tests for add_tween --- pyramid/config.py | 3 +-- pyramid/router.py | 6 ++--- pyramid/tests/test_config.py | 58 +++++++++++++++++++++++--------------------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index b8382e6a7..33e9db6a1 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -736,8 +736,7 @@ class Configurator(object): # add a handler manager tweens = Tweens() registry.registerUtility(tweens, ITweens) - self._add_tween('pyramid.router.exc_view_tween_factory', - explicit=False) + self._add_tween('pyramid.router.excview_tween_factory', explicit=False) if debug_logger is None: debug_logger = logging.getLogger(self.package_name) diff --git a/pyramid/router.py b/pyramid/router.py index 8cac733fb..dcf828377 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -41,8 +41,8 @@ class Router(object): self.request_factory = q(IRequestFactory, default=Request) tweens = q(ITweens) if tweens is None: - self.handle_request = exc_view_tween_factory(self.handle_request, - registry) + self.handle_request = excview_tween_factory(self.handle_request, + registry) else: self.handle_request = tweens(self.handle_request, registry) @@ -190,7 +190,7 @@ class Router(object): finally: manager.pop() -def exc_view_tween_factory(handler, registry): +def excview_tween_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 3e4895277..7bf0e632f 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -616,6 +616,7 @@ pyramid.tests.test_config.dummy_include2""", def test_add_tweens_names_distinct(self): from pyramid.interfaces import ITweens + from pyramid.router import excview_tween_factory def factory1(handler, registry): return handler def factory2(handler, registry): return handler config = self._makeOne() @@ -623,34 +624,33 @@ pyramid.tests.test_config.dummy_include2""", config.add_tween(factory2) config.commit() tweens = config.registry.queryUtility(ITweens) - self.assertEqual(tweens.implicit, None) - - ## 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) + self.assertEqual( + tweens.implicit, + [('pyramid.router.excview_tween_factory', excview_tween_factory), + ('pyramid.tests.test_config.factory1', factory1), + ('pyramid.tests.test_config.factory2', factory2)]) + + def test_add_tween_dottedname(self): + from pyramid.interfaces import ITweens + from pyramid.router import excview_tween_factory + config = self._makeOne() + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + config.commit() + tweens = config.registry.queryUtility(ITweens) + self.assertEqual( + tweens.implicit, + [ + ('pyramid.router.excview_tween_factory', excview_tween_factory), + ('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory) + ]) + + def test_add_tweens_conflict(self): + from zope.configuration.config import ConfigurationConflictError + config = self._makeOne() + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + self.assertRaises(ConfigurationConflictError, config.commit) def test_add_subscriber_defaults(self): from zope.interface import implements @@ -5660,3 +5660,5 @@ from pyramid.interfaces import IResponse class DummyResponse(object): implements(IResponse) +def dummy_tween_factory(handler, registry): + pass -- cgit v1.2.3 From feabd37caa3f860b550da904165ccb17252f2bf7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 18:00:57 -0400 Subject: add setup_registry test --- pyramid/config.py | 15 ++++++++------- pyramid/tests/test_config.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 33e9db6a1..9136c0880 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -719,11 +719,11 @@ class Configurator(object): if settings: includes = settings.get('pyramid.include', '') includes = [x.strip() for x in includes.splitlines()] - expl_tweens = settings.get('pyramid.tweens','') - expl_tweens = [x.strip() for x in expl_tweens.splitlines()] + tweens = settings.get('pyramid.tweens','') + tweens = [x.strip() for x in tweens.splitlines()] else: includes = [] - expl_tweens = [] + tweens = [] registry = self.registry self._fix_registry() self._set_settings(settings) @@ -734,10 +734,13 @@ class Configurator(object): from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) # add a handler manager - tweens = Tweens() - registry.registerUtility(tweens, ITweens) + tweenreg = Tweens() + registry.registerUtility(tweenreg, ITweens) self._add_tween('pyramid.router.excview_tween_factory', explicit=False) + for factory in tweens: + self._add_tween(factory, explicit=True) + if debug_logger is None: debug_logger = logging.getLogger(self.package_name) elif isinstance(debug_logger, basestring): @@ -783,8 +786,6 @@ class Configurator(object): if default_view_mapper is not None: self.set_view_mapper(default_view_mapper) self.commit() - for factory in expl_tweens: - self._add_tween(factory, explicit=True) for inc in includes: self.include(inc) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 7bf0e632f..0761498fb 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -583,6 +583,20 @@ pyramid.tests.test_config.dummy_include2""", self.assert_(reg.included) self.assert_(reg.also_included) + def test_setup_registry_tweens(self): + from pyramid.interfaces import ITweens + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg) + settings = { + 'pyramid.tweens': 'pyramid.tests.test_config.dummy_tween_factory' + } + config.setup_registry(settings=settings) + tweens = config.registry.getUtility(ITweens) + self.assertEqual(tweens.explicit, + [('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory)]) + def test_get_settings_nosettings(self): from pyramid.registry import Registry reg = Registry() -- cgit v1.2.3 From 3e15222b7d92e55f1c53ee252d3bebf25944e216 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 18:13:52 -0400 Subject: test Tweens --- pyramid/config.py | 9 +++--- pyramid/tests/test_config.py | 65 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 9136c0880..db5e5f945 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -1003,10 +1003,6 @@ class Configurator(object): """ return self._add_tween(tween_factory, explicit=False) - # XXX temporary bw compat for debugtoolbar - def add_request_handler(self, factory, name): - return self._add_tween(factory, explicit=False) - def _add_tween(self, tween_factory, explicit): tween_factory = self.maybe_dotted(tween_factory) if (hasattr(tween_factory, '__name__') and @@ -1029,6 +1025,11 @@ class Configurator(object): handler_manager.add(name, tween_factory, explicit) self.action(('tween', name), register) + # XXX temporary bw compat for debugtoolbar + @action_method + def add_request_handler(self, factory, name): # pragma: no cover + return self._add_tween(factory, explicit=False) + @action_method def add_subscriber(self, subscriber, iface=None): """Add an event :term:`subscriber` for the event stream diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 0761498fb..8b11bb22a 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -659,6 +659,29 @@ pyramid.tests.test_config.dummy_include2""", dummy_tween_factory) ]) + def test_add_tween_instance(self): + from pyramid.interfaces import ITweens + from pyramid.router import excview_tween_factory + class ATween(object): pass + atween = ATween() + config = self._makeOne() + config.add_tween(atween) + config.commit() + tweens = config.registry.queryUtility(ITweens) + self.assertEqual(len(tweens.implicit), 2) + self.assertEqual( + tweens.implicit[0], + ('pyramid.router.excview_tween_factory', excview_tween_factory)) + self.assertTrue( + tweens.implicit[1][0].startswith('pyramid.tests.test_config.ATween.')) + self.assertEqual(tweens.implicit[1][1], atween) + + def test_add_tween_unsuitable(self): + from pyramid.exceptions import ConfigurationError + import pyramid.tests + config = self._makeOne() + self.assertRaises(ConfigurationError, config.add_tween, pyramid.tests) + def test_add_tweens_conflict(self): from zope.configuration.config import ConfigurationConflictError config = self._makeOne() @@ -5488,6 +5511,45 @@ class Test_isexception(unittest.TestCase): pass self.assertEqual(self._callFUT(ISubException), True) +class TestTweens(unittest.TestCase): + def _makeOne(self): + from pyramid.config import Tweens + return Tweens() + + def test_add_explicit(self): + tweens = self._makeOne() + tweens.add('name', 'factory', explicit=True) + self.assertEqual(tweens.explicit, [('name', 'factory')]) + tweens.add('name2', 'factory2', explicit=True) + self.assertEqual(tweens.explicit, [('name', 'factory'), + ('name2', 'factory2')]) + + def test_add_implicit(self): + tweens = self._makeOne() + tweens.add('name', 'factory', explicit=False) + self.assertEqual(tweens.implicit, [('name', 'factory')]) + tweens.add('name2', 'factory2', explicit=False) + self.assertEqual(tweens.implicit, [('name', 'factory'), + ('name2', 'factory2')]) + + def test___call___explicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.explicit = [('name', factory1), ('name', factory2)] + self.assertEqual(tweens(None, None), '123') + + def test___call___implicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.implicit = [('name', factory1), ('name', factory2)] + self.assertEqual(tweens(None, None), '123') + class DummyRequest: subpath = () matchdict = None @@ -5674,5 +5736,4 @@ from pyramid.interfaces import IResponse class DummyResponse(object): implements(IResponse) -def dummy_tween_factory(handler, registry): - pass +def dummy_tween_factory(handler, registry): pass -- cgit v1.2.3 From 19466d028c1139445eb249c0363cafef5f870bdf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 18:15:24 -0400 Subject: remove temp bw compat; the return signature of tweens has changed anyway --- pyramid/config.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index db5e5f945..c1adb51c9 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -1025,11 +1025,6 @@ class Configurator(object): handler_manager.add(name, tween_factory, explicit) self.action(('tween', name), register) - # XXX temporary bw compat for debugtoolbar - @action_method - def add_request_handler(self, factory, name): # pragma: no cover - return self._add_tween(factory, explicit=False) - @action_method def add_subscriber(self, subscriber, iface=None): """Add an event :term:`subscriber` for the event stream -- cgit v1.2.3 From 1dc57d53c54338df467e6c1e8201589eab73022f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 18:21:11 -0400 Subject: docs --- pyramid/config.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index c1adb51c9..918e3bce4 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -909,14 +909,14 @@ class Configurator(object): @action_method def add_tween(self, tween_factory): """ - Add a 'tween factory'. A :term`tween` (think: 'between') is a bit of - code that sits between the Pyramid router's main request handling + Add a 'tween factory'. A :term:`tween` (think: 'between') is a bit + of code that sits between the Pyramid router's main request handling function and the upstream WSGI component that uses :app:`Pyramid` as its 'app'. This is a feature that may be used by Pyramid framework extensions, to provide, for example, Pyramid-specific view timing support bookkeeping code that examines exceptions before they are returned to the upstream WSGI application. Tweens behave a bit like - :mod:`WSGI` 'middleware' but they have the benefit of running in a + :term:`WSGI` 'middleware' but they have the benefit of running in a context in which they have access to the Pyramid :term:`application registry` as well as the Pyramid rendering machinery. @@ -929,8 +929,9 @@ class Configurator(object): registry` represented by this Configurator. A tween factory must return a tween when it is called. - A tween accepts a :term:`request` object and returns a two-tuple - consisting of a :term:`request` object and a :term:`response` object. + A tween is a callable which accepts a :term:`request` object and + returns a two-tuple consisting of a :term:`request` object and a + :term:`response` object. Here's an example creating a tween factory and registering it: @@ -970,25 +971,33 @@ class Configurator(object): request handler as its ``handler`` argument, the second tween factory added will be called with the result of the first tween factory as its ``handler`` argument, and so on, ad infinitum. The Pyramid router - will use the outermost tween produced by this chain (the very last - tween added) as its request handler function. + will use the outermost tween produced by this chain (the tween + generated by the very last tween factory added) as its request + handler function. By default, the ordering of the chain is controlled entirely by the relative ordering of calls to ``add_tween``. However, the deploying user can override tween inclusion and ordering in his Pyramid configuration using the ``pyramid.tweens`` settings value. When - used, this settings value should be a list of Python dotted names - which imply the ordering (and inclusion) of tween factories in the - tween chain. + used, this settings value will be a list of Python dotted names which + imply the ordering (and inclusion) of tween factories in the tween + chain. + + Note that Pyramid's own exception view handling logic is implemented + as a tween factory: ``pyramid.router.excview_tween_factory``. If + Pyramid exception view handling is desired, and explicit tween + factories are specified via ``pyramid.tweens``, this function must be + added to the ``pyramid.tweens`` setting. If it is not present, + Pyramid will not perform exception view handling. Pyramid will prevent the same tween factory from being added to the tween chain more than once using configuration conflict detection. If you wish to add the same tween factory more than once in a configuration, you should either: a) use a tween factory that is an - *instance* rather than a function or class, or b) use a function or + instance rather than a function or class, b) use a function or class as a tween factory with the same logic as the other tween factory it conflicts with but with a different ``__name__`` - attribute. + attribute or c) call config.commit() between calls to ``add_tween``. A user can get a representation of both the implicit tween ordering (the ordering specified by calls to ``add_tween``) and the explicit -- cgit v1.2.3 From 1311321d454ded6226b09f929ebc9e4aa2c12771 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 18:58:26 -0400 Subject: add tweens module, add docs for ptweens and tweens to hooks --- docs/api.rst | 1 + docs/api/config.rst | 2 +- docs/api/tweens.rst | 8 +++ docs/narr/commandline.rst | 75 ++++++++++++++++++++++++- docs/narr/hooks.rst | 128 +++++++++++++++++++++++++++++++++++++++++++ pyramid/config.py | 90 +----------------------------- pyramid/router.py | 38 +------------ pyramid/tests/test_config.py | 12 ++-- pyramid/tweens.py | 45 +++++++++++++++ 9 files changed, 265 insertions(+), 134 deletions(-) create mode 100644 docs/api/tweens.rst create mode 100644 pyramid/tweens.py diff --git a/docs/api.rst b/docs/api.rst index a7e1566d3..6ff6e9fb1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -32,6 +32,7 @@ documentation is organized alphabetically by module name. api/testing api/threadlocal api/traversal + api/tweens api/url api/view api/wsgi diff --git a/docs/api/config.rst b/docs/api/config.rst index 21e2b828d..1a9bb6ba4 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -78,7 +78,7 @@ .. automethod:: set_view_mapper - .. automethod:: add_request_handler + .. automethod:: add_tween .. automethod:: testing_securitypolicy diff --git a/docs/api/tweens.rst b/docs/api/tweens.rst new file mode 100644 index 000000000..5fc15cb00 --- /dev/null +++ b/docs/api/tweens.rst @@ -0,0 +1,8 @@ +.. _tweens_module: + +:mod:`pyramid.tweens` +--------------------- + +.. automodule:: pyramid.tweens + + .. autofunction:: excview_tween_factory diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 509af7dd3..6f969196f 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -297,8 +297,79 @@ application, nothing will be printed to the console when ``paster proutes`` is executed. .. index:: - single: scripting - single: bootstrap + pair: tweens; printing + single: paster ptweens + single: ptweens + +.. _displaying_tweens: + +Displaying "Tweens" +------------------- + +A user can get a representation of both the implicit :term:`tween` ordering +(the ordering specified by calls to +:meth:`pyramid.config.Configurator.add_tween`) and the explicit tween +ordering (specified by the ``pyramid.tweens`` configuration setting) +orderings using the ``paster ptweens`` command. Handler factories which are +functions or classes will show up as a standard Python dotted name in the +``paster ptweens`` output. Tween factories which are *instances* will show +their module and class name; the Python object id of the instance will be +appended. + +For example, here's the ``paster pwteens`` command run against a system +configured without any explicit tweens: + +.. code-block:: text + :linenos: + + [chrism@thinko starter]$ ../bin/paster ptweens development.ini + "pyramid.tweens" config value NOT set (implicitly ordered tweens used) + + Position Name + -------- ---- + 0 pyramid.router.excview_tween_factory + +Here's the ``paster pwteens`` command run against a system configured *with* +explicit tweens defined in its ``development.ini`` file: + +.. code-block:: text + :linenos: + + [chrism@thinko starter]$ ../bin/paster ptweens development.ini + "pyramid.tweens" config value set (explicitly ordered tweens used) + + Explicit Tween Chain (used) + + Position Name + -------- ---- + 0 pyramid.tweens.excview_tween_factory + 1 starter.tween_factory1 + 2 starter.tween_factory2 + + Implicit Tween Chain (not used) + + Position Name + -------- ---- + 0 pyramid.tweens.excview_tween_factory + +Here's the application configuration section of the ``development.ini`` used +by the above ``paster ptweens`` command which reprorts that the explicit +tween chain is used: + +.. code-block:: text + :linenos: + + [app:starter] + use = egg:starter + pyramid.reload_templates = true + pyramid.debug_authorization = false + pyramid.debug_notfound = false + pyramid.debug_routematch = false + pyramid.debug_templates = true + pyramid.default_locale_name = en + pyramid.tweens = pyramid.tweens.excview_tween_factory + starter.tween_factory1 + starter.tween_factory2 .. _writing_a_script: diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 4f493c854..7290876e6 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -828,3 +828,131 @@ performed, enabling you to set up the utility in advance: For full details, please read the `Venusian documentation `_. +.. _registering_tweens: + +Registering "Tweens" +-------------------- + +.. note:: Tweens are a feature which were added in Pyramid 1.1.1. They are + not available in any previous version. + +A :term:`tween` (think: "between") is a bit of code that sits between the +Pyramid router's main request handling function and the upstream WSGI +component that uses :app:`Pyramid` as its "app". This is a feature that may +be used by Pyramid framework extensions, to provide, for example, +Pyramid-specific view timing support bookkeeping code that examines +exceptions before they are returned to the upstream WSGI application. Tweens +behave a bit like :term:`WSGI` middleware but they have the benefit of +running in a context in which they have access to the Pyramid +:term:`application registry` as well as the Pyramid rendering machinery. + +To make use of tweens, you must construct a "tween factory". A tween factory +must be a callable (or a :term:`dotted Python name` to such a callable) which +accepts two arguments: ``handler`` and ``registry``. ``handler`` will be the +either the main Pyramid request handling function or another tween (if more +than one tween is configured into the request handling chain). ``registry`` +will be the Pyramid :term:`application registry` represented by this +Configurator. A tween factory must return a tween when it is called. + +A tween is a callable which accepts a :term:`request` object and returns a +two-tuple consisting of a :term:`request` object and a :term:`response` +object. + +Once you've created a tween factory, you can register it using the +:meth:`pyramid.config.Configurator.add_tween` method. + +Here's an example creating a tween factory and registering it: + +.. code-block:: python + :lineno: + + import time + from pyramid.settings import asbool + import logging + + log = logging.getLogger(__name__) + + def timing_tween_factory(handler, registry): + if asbool(registry.settings.get('do_timing')): + # if timing support is enabled, return a wrapper + def timing_tween(request): + start = time.time() + try: + request, response = handler(request) + finally: + end = time.time() + log.debug('The request took %s seconds' % + (end - start)) + return request, response + return timing_tween + # if timing support is not enabled, return the original + # handler + return handler + + from pyramid.config import Configurator + + config = Configurator() + config.add_tween(timing_tween_factory) + +The ``request`` argument to a tween will be the request created by Pyramid's +router when it receives a WSGI request. + +If more than one call to :meth:`pyramid.config.Configurator.add_tween` is +made within a single application configuration, the added tweens will be +chained together. The first tween factory added will be called with the +default Pyramid request handler as its ``handler`` argument, the second tween +factory added will be called with the result of the first tween factory as +its ``handler`` argument, and so on, ad infinitum. The Pyramid router will +use the outermost tween produced by this chain (the tween generated by the +very last tween factory added) as its request handler function. + +Pyramid will prevent the same tween factory from being added to the implicit +tween chain more than once using configuration conflict detection. If you +wish to add the same tween factory more than once in a configuration, you +should either: a) use a tween factory that is an instance rather than a +function or class, b) use a function or class as a tween factory with the +same logic as the other tween factory it conflicts with but with a different +``__name__`` attribute or c) call :meth:`pyramid.config.Configurator.commit` +between calls to :meth:`pyramid.config.Configurator.add_tween`. + +By default, the ordering of the chain is controlled entirely by the relative +ordering of calls to :meth:`pyramid.config.Configurator.add_tween`. However, +the deploying user can override tween inclusion and ordering entirely in his +Pyramid configuration using the ``pyramid.tweens`` settings value. When +used, this settings value will be a list of Python dotted names which imply +the ordering (and inclusion) of tween factories in the tween chain. For +example: + +.. code-block:: ini + :linenos: + + [app:main] + use = egg:MyApp + pyramid.reload_templates = true + pyramid.debug_authorization = false + pyramid.debug_notfound = false + pyramid.debug_routematch = false + pyramid.debug_templates = true + pyramid.tweens = pyramid.tweens.excview_tween_factory + myapp.my_cool_tween_factory + +In the above configuration, calls made during configuration to +:meth:`pyramid.config.Configurator.add_tween` are ignored, and the user is +telling the system to use the tween factories he has listed in the +``pyramid.tweens`` configuration setting (each is a:term:`Python dotted name` +which points to a tween factory). The *last* tween factory in the +``pyramid.tweens`` list will be used as the producer of the effective +:app:`Pyramid` request handling function; it will wrap the tween factory +declared directly "above" it, ad infinitum. + +.. note:: Pyramid's own :term:`exception view` handling logic is implemented + as a tween factory function: :func:`pyramid.tweens.excview_tween_factory`. + If Pyramid exception view handling is desired, and tween factories are + specified via the ``pyramid.tweens`` configuration setting, the + :func:`pyramid.tweens.excview_tween_factory` function must be added to the + ``pyramid.tweens`` configuration setting list explicitly. If it is not + present, Pyramid will not perform exception view handling. + +The ``paster ptweens`` command-line utility can be used to report the current +tween chain used by an application. See :ref:`displaying_tweens`. + diff --git a/pyramid/config.py b/pyramid/config.py index 918e3bce4..ebbea35ea 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -920,93 +920,7 @@ class Configurator(object): context in which they have access to the Pyramid :term:`application registry` as well as the Pyramid rendering machinery. - A tween factory (passed as ``tween_factory``) must be a callable (or - a :term:`dotted Python name` to such a callable) which accepts two - arguments: ``handler`` and ``registry``. ``handler`` will be the - either the main Pyramid request handling function or another tween - (if more than one tween is configured into the request handling - chain). ``registry`` will be the Pyramid :term:`application - registry` represented by this Configurator. A tween factory must - return a tween when it is called. - - A tween is a callable which accepts a :term:`request` object and - returns a two-tuple consisting of a :term:`request` object and a - :term:`response` object. - - Here's an example creating a tween factory and registering it: - - .. code-block:: python - - import time - from pyramid.settings import asbool - import logging - - log = logging.getLogger(__name__) - - def timing_tween_factory(handler, registry): - if asbool(registry.settings.get('do_timing')): - # if timing support is enabled, return a wrapper - def timing_tween(request): - start = time.time() - try: - request, response = handler(request) - finally: - end = time.time() - log.debug('The request took %s seconds' % - (end - start)) - return request, response - return timing_tween - # if timing support is not enabled, return the original - # handler - return handler - - config.add_tween(timing_tween_factory) - - The ``request`` argument to a tween will be the request created by - Pyramid's router when it receives a WSGI request. - - If more than one call to ``add_tween`` is made within a single - application configuration, the added tweens will be chained together. - The first tween factory added will be called with the default Pyramid - request handler as its ``handler`` argument, the second tween factory - added will be called with the result of the first tween factory as - its ``handler`` argument, and so on, ad infinitum. The Pyramid router - will use the outermost tween produced by this chain (the tween - generated by the very last tween factory added) as its request - handler function. - - By default, the ordering of the chain is controlled entirely by the - relative ordering of calls to ``add_tween``. However, the deploying - user can override tween inclusion and ordering in his Pyramid - configuration using the ``pyramid.tweens`` settings value. When - used, this settings value will be a list of Python dotted names which - imply the ordering (and inclusion) of tween factories in the tween - chain. - - Note that Pyramid's own exception view handling logic is implemented - as a tween factory: ``pyramid.router.excview_tween_factory``. If - Pyramid exception view handling is desired, and explicit tween - factories are specified via ``pyramid.tweens``, this function must be - added to the ``pyramid.tweens`` setting. If it is not present, - Pyramid will not perform exception view handling. - - Pyramid will prevent the same tween factory from being added to the - tween chain more than once using configuration conflict detection. - If you wish to add the same tween factory more than once in a - configuration, you should either: a) use a tween factory that is an - instance rather than a function or class, b) use a function or - class as a tween factory with the same logic as the other tween - factory it conflicts with but with a different ``__name__`` - attribute or c) call config.commit() between calls to ``add_tween``. - - A user can get a representation of both the implicit tween ordering - (the ordering specified by calls to ``add_tween``) and the explicit - request handler ordering (specified by the ``pyramid.tweens`` - setting) orderings using the ``paster ptweens`` command. Handler - factories which are functions or classes will show up as a standard - Python dotted name in the ``ptweens`` output. Tween factories - which are *instances* will show their module and class name; the - Python object id of the instance will be appended. + For more information, see :ref:`registering_tweens`. .. note:: This feature is new as of Pyramid 1.1.1. """ @@ -1032,7 +946,7 @@ class Configurator(object): registry = self.registry handler_manager = registry.getUtility(ITweens) handler_manager.add(name, tween_factory, explicit) - self.action(('tween', name), register) + self.action(('tween', name, explicit), register) @action_method def add_subscriber(self, subscriber, iface=None): diff --git a/pyramid/router.py b/pyramid/router.py index dcf828377..5faafb4bb 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,10 +1,7 @@ -import sys - from zope.interface import implements from zope.interface import providedBy from pyramid.interfaces import IDebugLogger -from pyramid.interfaces import IExceptionViewClassifier from pyramid.interfaces import IRequest from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest @@ -24,6 +21,7 @@ from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory from pyramid.traversal import ResourceTreeTraverser +from pyramid.tweens import excview_tween_factory class Router(object): implements(IRouter) @@ -190,37 +188,3 @@ class Router(object): finally: manager.pop() -def excview_tween_factory(handler, registry): - has_listeners = registry.has_listeners - adapters = registry.adapters - notify = registry.notify - - def exception_view_handler(request): - attrs = request.__dict__ - try: - request, response = handler(request) - except Exception, exc: - # WARNING: do not assign the result of sys.exc_info() to a - # local var here, doing so will cause a leak - attrs['exc_info'] = sys.exc_info() - attrs['exception'] = exc - # 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'] - request_iface = attrs['request_iface'] - provides = providedBy(exc) - for_ = (IExceptionViewClassifier, request_iface.combined, provides) - view_callable = adapters.lookup(for_, IView, default=None) - if view_callable is None: - raise - response = view_callable(exc, request) - has_listeners and notify(NewResponse(request, response)) - finally: - attrs['exc_info'] = None - - return request, response - - return exception_view_handler - diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 8b11bb22a..d73fd7f7d 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -630,7 +630,7 @@ pyramid.tests.test_config.dummy_include2""", def test_add_tweens_names_distinct(self): from pyramid.interfaces import ITweens - from pyramid.router import excview_tween_factory + from pyramid.tweens import excview_tween_factory def factory1(handler, registry): return handler def factory2(handler, registry): return handler config = self._makeOne() @@ -640,13 +640,13 @@ pyramid.tests.test_config.dummy_include2""", tweens = config.registry.queryUtility(ITweens) self.assertEqual( tweens.implicit, - [('pyramid.router.excview_tween_factory', excview_tween_factory), + [('pyramid.tweens.excview_tween_factory', excview_tween_factory), ('pyramid.tests.test_config.factory1', factory1), ('pyramid.tests.test_config.factory2', factory2)]) def test_add_tween_dottedname(self): from pyramid.interfaces import ITweens - from pyramid.router import excview_tween_factory + from pyramid.tweens import excview_tween_factory config = self._makeOne() config.add_tween('pyramid.tests.test_config.dummy_tween_factory') config.commit() @@ -654,14 +654,14 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual( tweens.implicit, [ - ('pyramid.router.excview_tween_factory', excview_tween_factory), + ('pyramid.tweens.excview_tween_factory', excview_tween_factory), ('pyramid.tests.test_config.dummy_tween_factory', dummy_tween_factory) ]) def test_add_tween_instance(self): from pyramid.interfaces import ITweens - from pyramid.router import excview_tween_factory + from pyramid.tweens import excview_tween_factory class ATween(object): pass atween = ATween() config = self._makeOne() @@ -671,7 +671,7 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual(len(tweens.implicit), 2) self.assertEqual( tweens.implicit[0], - ('pyramid.router.excview_tween_factory', excview_tween_factory)) + ('pyramid.tweens.excview_tween_factory', excview_tween_factory)) self.assertTrue( tweens.implicit[1][0].startswith('pyramid.tests.test_config.ATween.')) self.assertEqual(tweens.implicit[1][1], atween) diff --git a/pyramid/tweens.py b/pyramid/tweens.py new file mode 100644 index 000000000..209768198 --- /dev/null +++ b/pyramid/tweens.py @@ -0,0 +1,45 @@ +import sys +from pyramid.interfaces import IExceptionViewClassifier +from pyramid.interfaces import IView + +from pyramid.events import NewResponse +from zope.interface import providedBy + +def excview_tween_factory(handler, registry): + """ A :term:`tween` factory which produces a tween that catches an + exception raised by downstream tweens (or the main Pyramid request + handler) and, if possible, converts it into a Response using an + :term:`exception view`.""" + has_listeners = registry.has_listeners + adapters = registry.adapters + notify = registry.notify + + def excview_tween(request): + attrs = request.__dict__ + try: + request, response = handler(request) + except Exception, exc: + # WARNING: do not assign the result of sys.exc_info() to a + # local var here, doing so will cause a leak + attrs['exc_info'] = sys.exc_info() + attrs['exception'] = exc + # 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'] + request_iface = attrs['request_iface'] + provides = providedBy(exc) + for_ = (IExceptionViewClassifier, request_iface.combined, provides) + view_callable = adapters.lookup(for_, IView, default=None) + if view_callable is None: + raise + response = view_callable(exc, request) + has_listeners and notify(NewResponse(request, response)) + finally: + attrs['exc_info'] = None + + return request, response + + return excview_tween + -- cgit v1.2.3 From fc38f448d9b35d18f0b7d687efc659e1a2b8ec7c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 19:10:42 -0400 Subject: write tests for ptweens --- pyramid/tests/test_paster.py | 73 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 3cf249c5c..58ed73d2c 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -824,6 +824,79 @@ class TestBootstrap(unittest.TestCase): self.assertEqual(result['root'], self.root) self.assert_('closer' in result) +class TestPTweensCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.paster import PTweensCommand + return PTweensCommand + + def _makeOne(self): + cmd = self._getTargetClass()('ptweens') + cmd.bootstrap = (DummyBootstrap(),) + cmd.args = ('/foo/bar/myapp.ini#myapp',) + return cmd + + def test_command_no_tweens(self): + command = self._makeOne() + command._get_tweens = lambda *arg: None + L = [] + command.out = L.append + result = command.command() + self.assertEqual(result, None) + self.assertEqual(L, []) + + def test_command_implicit_tweens_only(self): + command = self._makeOne() + tweens = DummyTweens([('name', 'item')], None) + command._get_tweens = lambda *arg: tweens + L = [] + command.out = L.append + result = command.command() + self.assertEqual(result, None) + self.assertEqual( + L, + ['"pyramid.tweens" config value NOT set (implicitly ordered tweens used)', + '', + 'Position Name ', + '-------- ---- ', + '0 name ', + '']) + + def test_command_implicit_and_explicit_tweens(self): + command = self._makeOne() + tweens = DummyTweens([('name', 'item')], [('name2', 'item2')]) + command._get_tweens = lambda *arg: tweens + L = [] + command.out = L.append + result = command.command() + self.assertEqual(result, None) + self.assertEqual( + L, + ['"pyramid.tweens" config value set (explicitly ordered tweens used)', + '', + 'Explicit Tween Chain (used)', + '', + 'Position Name ', + '-------- ---- ', + '0 name2 ', + '', + 'Implicit Tween Chain (not used)', + '', + 'Position Name ', + '-------- ---- ', + '0 name ', + '' + ]) + + def test__get_tweens(self): + command = self._makeOne() + registry = DummyRegistry() + self.assertEqual(command._get_tweens(registry), None) + +class DummyTweens(object): + def __init__(self, implicit, explicit): + self.implicit = implicit + self.explicit = explicit + class Dummy: pass -- cgit v1.2.3 From 6b2a6210dcda54d81e6e827aed74379f579a2810 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 19:41:40 -0400 Subject: improve changes docs --- CHANGES.txt | 86 +++++++++++++++++++++---------------------------------------- 1 file changed, 29 insertions(+), 57 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c94ecb800..b6f33715e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,63 +12,19 @@ Features ``rendering_val``. This can be used to introspect the value returned by a view in a BeforeRender subscriber. -- 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 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. - - 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). +- New configurator directive: ``pyramid.config.Configurator.add_tween``. + This directive adds a "tween". A "tween" is used to wrap the Pyramid + router's primary request handling function. This is a feature may be used + by Pyramid framework extensions, to provide, for example, view timing + support and as a convenient place to hang bookkeeping code. + + Tweens are further described in the narrative docs section in the Hooks + chapter, named "Registering Tweens". + +- New paster command ``paster ptweens``, which prints the current "tween" + configuration for an application. See the section entitled "Displaying + Tweens" in the Command-Line Pyramid chapter of the narrative documentation + for more info. - The Pyramid debug logger now uses the standard logging configuration (usually set up by Paste as part of startup). This means that output from @@ -80,6 +36,12 @@ Features will be ``None`` until an exception is caught by the Pyramid router, after which it will be the result of ``sys.exc_info()``. +Internal +-------- + +- The Pyramid "exception view" machinery is now implemented as a "tween" + (``pyramid.tweens.excview_tween_factory``). + Deprecations ------------ @@ -97,6 +59,16 @@ Backwards Incompatibilities that string is considered to be the name of a global Python logger rather than a dotted name to an instance of a logger. +Documentation +------------- + +- Added a new module to the API docs: ``pyramid.tweens``. + +- Added a "Registering Tweens" section to the "Hooks" narrative chapter. + +- Added a "Displaying Tweens" section to the "Command-Line Pyramid" narrative + chapter. + 1.1 (2011-07-22) ================ -- cgit v1.2.3 From 479db008942048e29041e7225b8f60b17e843f07 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 20:00:07 -0400 Subject: cope with the realities of trying to initialize a utility in setup_registry; it's not always called --- pyramid/config.py | 60 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index ebbea35ea..126111269 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -79,6 +79,7 @@ from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory from pyramid.traversal import find_interface from pyramid.traversal import traversal_path +from pyramid.tweens import excview_tween_factory from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver from pyramid.util import WeakOrderedSet @@ -716,14 +717,13 @@ class Configurator(object): policies, renderers, a debug logger, a locale negotiator, and various other settings using the configurator's current registry, as per the descriptions in the Configurator constructor.""" + tweens = [] + includes = [] if settings: - includes = settings.get('pyramid.include', '') - includes = [x.strip() for x in includes.splitlines()] - tweens = settings.get('pyramid.tweens','') - tweens = [x.strip() for x in tweens.splitlines()] - else: - includes = [] - tweens = [] + includes = [x.strip() for x in + settings.get('pyramid.include', '').splitlines()] + tweens = [x.strip() for x in + settings.get('pyramid.tweens','').splitlines()] registry = self.registry self._fix_registry() self._set_settings(settings) @@ -734,10 +734,6 @@ class Configurator(object): from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) # add a handler manager - tweenreg = Tweens() - registry.registerUtility(tweenreg, ITweens) - self._add_tween('pyramid.router.excview_tween_factory', explicit=False) - for factory in tweens: self._add_tween(factory, explicit=True) @@ -928,24 +924,18 @@ class Configurator(object): def _add_tween(self, tween_factory, explicit): tween_factory = self.maybe_dotted(tween_factory) - if (hasattr(tween_factory, '__name__') and - hasattr(tween_factory, '__module__')): - # function or class - name = '.'.join([tween_factory.__module__, - tween_factory.__name__]) - elif hasattr(tween_factory, '__module__'): - # instance - name = '.'.join([tween_factory.__module__, - tween_factory.__class__.__name__, - str(id(tween_factory))]) - else: - raise ConfigurationError( - 'A tween factory must be a class, an instance, or a function; ' - '%s is not a suitable tween factory' % tween_factory) + name = tween_factory_name(tween_factory) def register(): registry = self.registry - handler_manager = registry.getUtility(ITweens) - handler_manager.add(name, tween_factory, explicit) + tweens = registry.queryUtility(ITweens) + if tweens is None: + tweens = Tweens() + registry.registerUtility(tweens, ITweens) + tweens.add( + tween_factory_name(excview_tween_factory), + excview_tween_factory, + explicit=False) + tweens.add(name, tween_factory, explicit) self.action(('tween', name, explicit), register) @action_method @@ -3403,3 +3393,19 @@ class Tweens(object): handler = factory(handler, registry) return handler +def tween_factory_name(factory): + if (hasattr(factory, '__name__') and + hasattr(factory, '__module__')): + # function or class + name = '.'.join([factory.__module__, + factory.__name__]) + elif hasattr(factory, '__module__'): + # instance + name = '.'.join([factory.__module__, + factory.__class__.__name__, + str(id(factory))]) + else: + raise ConfigurationError( + 'A tween factory must be a class, an instance, or a function; ' + '%s is not a suitable tween factory' % factory) + return name -- cgit v1.2.3 From 8d1533d95ceebbef641a220236b77d9b3931cc31 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 20:22:43 -0400 Subject: change return value of tween to just response --- docs/narr/hooks.rst | 7 +++---- pyramid/router.py | 4 ++-- pyramid/tweens.py | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 7290876e6..c6438dfdf 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -855,8 +855,7 @@ will be the Pyramid :term:`application registry` represented by this Configurator. A tween factory must return a tween when it is called. A tween is a callable which accepts a :term:`request` object and returns a -two-tuple consisting of a :term:`request` object and a :term:`response` -object. +two-tuple a :term:`response` object. Once you've created a tween factory, you can register it using the :meth:`pyramid.config.Configurator.add_tween` method. @@ -878,12 +877,12 @@ Here's an example creating a tween factory and registering it: def timing_tween(request): start = time.time() try: - request, response = handler(request) + response = handler(request) finally: end = time.time() log.debug('The request took %s seconds' % (end - start)) - return request, response + return response return timing_tween # if timing support is not enabled, return the original # handler diff --git a/pyramid/router.py b/pyramid/router.py index 5faafb4bb..79fd7992b 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -162,7 +162,7 @@ class Router(object): if request.response_callbacks: request._process_response_callbacks(response) - return request, response + return response finally: if request is not None and request.finished_callbacks: @@ -183,7 +183,7 @@ class Router(object): manager.push(threadlocals) try: request.registry = registry - request, response = self.handle_request(request) + response = self.handle_request(request) return response(request.environ, start_response) finally: manager.pop() diff --git a/pyramid/tweens.py b/pyramid/tweens.py index 209768198..f7673a738 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -17,7 +17,7 @@ def excview_tween_factory(handler, registry): def excview_tween(request): attrs = request.__dict__ try: - request, response = handler(request) + response = handler(request) except Exception, exc: # WARNING: do not assign the result of sys.exc_info() to a # local var here, doing so will cause a leak @@ -39,7 +39,7 @@ def excview_tween_factory(handler, registry): finally: attrs['exc_info'] = None - return request, response + return response return excview_tween -- cgit v1.2.3 From ca55f9754876c572fe638553aebc90120f12d6fe Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 20:29:57 -0400 Subject: readd bw compat for toolbar (temporary) --- pyramid/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyramid/config.py b/pyramid/config.py index 126111269..a12df8ef7 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -937,6 +937,11 @@ class Configurator(object): explicit=False) tweens.add(name, tween_factory, explicit) self.action(('tween', name, explicit), register) + + @action_method + def add_request_handler(self, factory, name): # pragma: no cover + # XXX bw compat for debugtoolbar + return self._add_tween(factory, explicit=False) @action_method def add_subscriber(self, subscriber, iface=None): -- cgit v1.2.3 From 18a99ae15e7f36cd21da6e2d2e70d61d1733bf30 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 6 Aug 2011 20:35:34 -0400 Subject: docs rendering --- docs/narr/hooks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index c6438dfdf..889e8d6d8 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -863,7 +863,7 @@ Once you've created a tween factory, you can register it using the Here's an example creating a tween factory and registering it: .. code-block:: python - :lineno: + :linenos: import time from pyramid.settings import asbool -- cgit v1.2.3