From 81d3b5412b43e4a104d0118ad4147402d787220e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Sep 2010 02:39:26 +0000 Subject: Features -------- - A ``request.matched_route`` attribute is now added to the request when a route has matched. Its value is the "route" object that matched (see the ``IRoute`` interface within ``repoze.bfg.interfaces`` API documentation for the API of a route object). - The ``exception`` attribute of the request is now set slightly earlier and in a slightly different set of scenarios, for benefit of "finished callbacks" and "response callbacks". In previous versions, the ``exception`` attribute of the request was not set at all if an exception view was not found. In this version, the ``request.exception`` attribute is set immediately when an exception is caught by the router, even if an exception view could not be found. Backwards Incompatibilities --------------------------- - The router no longer sets the value ``wsgiorg.routing_args`` into the environ when a route matches. The value used to be something like ``((), matchdict)``. This functionality was only ever obliquely referred to in change logs; it was never documented as an API. - The ``exception`` attribute of the request now defaults to ``None``. In prior versions, the ``request.exception`` attribute did not exist if an exception was not raised by user code during request processing; it only began existence once an exception view was found. Deprecations ------------ - References to the WSGI environment values ``bfg.routes.matchdict`` and ``bfg.routes.route`` were removed from documentation. These will stick around internally for several more releases, but it is ``request.matchdict`` and ``request.matched_route`` are now the "official" way to obtain the matchdict and the route object which resulted in the match. Documentation ------------- - Added two sections to the "Hooks" chapter of the documentation: "Using Response Callbacks" and "Using Finished Callbacks". - Added documentation of the ``request.exception`` attribute to the ``repoze.bfg.request.Request`` API documentation. - Added glossary entries for "response callback" and "finished callback". - The "Request Processing" narrative chapter has been updated to note finished and response callback steps. --- repoze/bfg/request.py | 19 ++++++++++++++++--- repoze/bfg/router.py | 30 ++++++++++++++++++++---------- repoze/bfg/tests/test_request.py | 4 ++++ repoze/bfg/tests/test_router.py | 27 +++++++++++++++++---------- 4 files changed, 57 insertions(+), 23 deletions(-) (limited to 'repoze') diff --git a/repoze/bfg/request.py b/repoze/bfg/request.py index 6e5417d9a..84a5660ef 100644 --- a/repoze/bfg/request.py +++ b/repoze/bfg/request.py @@ -40,6 +40,7 @@ class Request(WebobRequest): implements(IRequest) response_callbacks = () finished_callbacks = () + exception = None default_charset = 'utf-8' def add_response_callback(self, callback): @@ -72,7 +73,13 @@ class Request(WebobRequest): Errors raised by callbacks are not handled specially. They will be propagated to the caller of the :mod:`repoze.bfg` - router application. """ + router application. + + .. note: ``add_response_callback`` is new in :mod:`repoze.bfg` + 1.3. + + See also: :ref:`using_response_callbacks`. + """ callbacks = self.response_callbacks if not callbacks: @@ -101,7 +108,7 @@ class Request(WebobRequest): def commit_callback(request): '''commit or abort the transaction associated with request''' - if hasattr(request, 'exception'): + if request.exception is not None: transaction.abort() else: transaction.commit() @@ -126,7 +133,13 @@ class Request(WebobRequest): Errors raised by finished callbacks are not handled specially. They will be propagated to the caller of the :mod:`repoze.bfg` - router application. """ + router application. + + .. note: ``add_finished_callback`` is new in :mod:`repoze.bfg` + 1.3. + + See also: :ref:`using_finished_callbacks`. + """ callbacks = self.finished_callbacks if not callbacks: diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index 6532beec4..e6de4fdd7 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -57,9 +57,9 @@ class Router(object): has_listeners = registry.has_listeners logger = self.logger manager = self.threadlocal_manager - threadlocals = {'registry':registry, 'request':None} - manager.push(threadlocals) request = None + threadlocals = {'registry':registry, 'request':request} + manager.push(threadlocals) try: # create the request @@ -72,16 +72,19 @@ class Router(object): request_iface = IRequest try: - # find the root + # find the root object root_factory = self.root_factory if self.routes_mapper is not None: info = self.routes_mapper(request) match, route = info['match'], info['route'] if route is not None: - environ['wsgiorg.routing_args'] = ((), match) - environ['bfg.routes.route'] = route + # TODO: kill off bfg.routes.* environ keys + # when traverser requires request arg, and + # cant cope with environ anymore (likely 1.4+) + environ['bfg.routes.route'] = route environ['bfg.routes.matchdict'] = match attrs['matchdict'] = match + attrs['matched_route'] = route request_iface = registry.queryUtility( IRouteRequest, name=route.name, @@ -91,7 +94,7 @@ class Router(object): root = root_factory(request) attrs['root'] = root - # find a view callable + # find a context traverser = adapters.queryAdapter(root, ITraverser) if traverser is None: traverser = ModelGraphTraverser(root) @@ -102,6 +105,8 @@ class Router(object): tdict['virtual_root_path']) attrs.update(tdict) has_listeners and registry.notify(ContextFound(request)) + + # find a view callable context_iface = providedBy(context) view_callable = adapters.lookup( (IViewClassifier, request_iface, context_iface), @@ -121,27 +126,31 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info + # repoze.bfg.message should die environ['repoze.bfg.message'] = msg raise NotFound(msg) else: response = view_callable(context, request) - # handle exceptions raised during root finding and view lookup + # handle exceptions raised during root finding and view execution except Exception, why: + attrs['exception'] = why + for_ = (IExceptionViewClassifier, request_iface.combined, providedBy(why)) view_callable = adapters.lookup(for_, IView, default=None) + if view_callable is None: raise - try: + # r.b.message should be deprecated + try: msg = why[0] - except Exception: + except: msg = '' environ['repoze.bfg.message'] = msg - attrs['exception'] = why response = view_callable(why, request) # process the response @@ -164,6 +173,7 @@ class Router(object): return app_iter finally: + # post-response cleanup try: if request is not None and request.finished_callbacks: request._process_finished_callbacks() diff --git a/repoze/bfg/tests/test_request.py b/repoze/bfg/tests/test_request.py index d8a3c09fb..ae52f8420 100644 --- a/repoze/bfg/tests/test_request.py +++ b/repoze/bfg/tests/test_request.py @@ -23,6 +23,10 @@ class TestRequest(unittest.TestCase): r = self._makeOne({'PATH_INFO':'/'}) self.assertEqual(r.charset, 'utf-8') + def test_exception_defaults_to_None(self): + r = self._makeOne({'PATH_INFO':'/'}) + self.assertEqual(r.exception, None) + def test_params_decoded_from_utf_8_by_default(self): environ = { 'PATH_INFO':'/', diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py index 199602a96..fade0679b 100644 --- a/repoze/bfg/tests/test_router.py +++ b/repoze/bfg/tests/test_router.py @@ -518,12 +518,11 @@ class TestRouter(unittest.TestCase): self.assertEqual(request.subpath, []) self.assertEqual(request.context, context) self.assertEqual(request.root, root) - routing_args = environ['wsgiorg.routing_args'][1] - self.assertEqual(routing_args['action'], 'action1') - self.assertEqual(routing_args['article'], 'article1') - self.assertEqual(environ['bfg.routes.matchdict'], routing_args) + matchdict = {'action':'action1', 'article':'article1'} + self.assertEqual(environ['bfg.routes.matchdict'], matchdict) self.assertEqual(environ['bfg.routes.route'].name, 'foo') - self.assertEqual(request.matchdict, routing_args) + self.assertEqual(request.matchdict, matchdict) + self.assertEqual(request.matched_route.name, 'foo') def test_call_route_matches_doesnt_overwrite_subscriber_iface(self): from repoze.bfg.interfaces import INewRequest @@ -559,12 +558,11 @@ class TestRouter(unittest.TestCase): self.assertEqual(request.subpath, []) self.assertEqual(request.context, context) self.assertEqual(request.root, root) - routing_args = environ['wsgiorg.routing_args'][1] - self.assertEqual(routing_args['action'], 'action1') - self.assertEqual(routing_args['article'], 'article1') - self.assertEqual(environ['bfg.routes.matchdict'], routing_args) + matchdict = {'action':'action1', 'article':'article1'} + self.assertEqual(environ['bfg.routes.matchdict'], matchdict) self.assertEqual(environ['bfg.routes.route'].name, 'foo') - self.assertEqual(request.matchdict, routing_args) + self.assertEqual(request.matchdict, matchdict) + self.assertEqual(request.matched_route.name, 'foo') self.failUnless(IFoo.providedBy(request)) def test_root_factory_raises_notfound(self): @@ -634,6 +632,12 @@ class TestRouter(unittest.TestCase): pass from repoze.bfg.interfaces import IRequest from repoze.bfg.interfaces import IViewClassifier + from repoze.bfg.interfaces import IRequestFactory + def rfactory(environ): + return request + self.registry.registerUtility(rfactory, IRequestFactory) + from repoze.bfg.request import Request + request = Request.blank('/') context = DummyContext() directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) @@ -644,6 +648,9 @@ class TestRouter(unittest.TestCase): router = self._makeOne() start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) + # ``exception`` must be attached to request even if a suitable + # exception view cannot be found + self.assertEqual(request.exception.__class__, RuntimeError) def test_call_view_raises_exception_view(self): from repoze.bfg.interfaces import IViewClassifier -- cgit v1.2.3