diff options
| -rw-r--r-- | CHANGES.txt | 19 | ||||
| -rw-r--r-- | docs/glossary.rst | 19 | ||||
| -rw-r--r-- | docs/narr/views.rst | 396 | ||||
| -rw-r--r-- | docs/whatsnew-1.2.rst | 15 | ||||
| -rw-r--r-- | repoze/bfg/router.py | 116 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_router.py | 43 |
6 files changed, 365 insertions, 243 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index f9ee7d81a..024ed8929 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,20 @@ Next release ============ +Features +-------- + +- When the ``repoze.bfg.exceptions.NotFound`` or + ``repoze.bfg.exceptions.Forbidden`` error is raised from within a + custom root factory or the ``factory`` of a route, the appropriate + response is now sent to the requesting user agent (the result of the + notfound view or the forbidden view, respectively). When these + errors are raised from within a root factory, the ``context`` passed + to the notfound or forbidden view will be ``None``. Also, the + request will not be decorated with ``view_name``, ``subpath``, + ``context``, etc. as would normally be the case if traversal had + been allowed to take place. + Internals --------- @@ -22,6 +36,11 @@ Documentation - Added "Thread Locals" narrative chapter to documentation, and added a API chapter documenting the ``repoze.bfg.threadlocals`` module. +- Added a "Special Exceptions" section to the "Views" narrative + documentation chapter explaining the effect of raising + ``repoze.bfg.exceptions.NotFound`` and + ``repoze.bfg.exceptions.Forbidden`` from within view code. + Dependencies ------------ diff --git a/docs/glossary.rst b/docs/glossary.rst index 491ad6d48..0fa827188 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -490,3 +490,22 @@ Glossary The configuration mode in which you use Python to call methods on a :term:`Configurator` in order to add each :term:`configuration declaration` required by your application. + Not Found View + The :term:`view callable` invoked by :mod:`repoze.bfg` when the + developer explicitly raises a + ``repoze.bfg.exceptions.NotFound`` exception from within + :term:`view` code or :term:`root factory` code, or when the + current request doesn't match any :term:`view configuration`. + :mod:`repoze.bfg` provides a default implementation of a not + found view; it can be overridden. See + :ref:`changing_the_notfound_view`. + Forbidden View + The :term:`view callable` invoked by :mod:`repoze.bfg` when the + developer explicitly raises a + ``repoze.bfg.exceptions.Forbidden`` exception from within + :term:`view` code or :term:`root factory` code, or when the the + :term:`view configuration` and :term:`authorization policy` found + for a request disallows a particular view invocation. + :mod:`repoze.bfg` provides a default implementation of a + forbidden view; it can be overridden. See + :ref:`changing_the_forbidden_view`. diff --git a/docs/narr/views.rst b/docs/narr/views.rst index e4eba6a0b..01389cec0 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -1154,6 +1154,192 @@ Views with use a Chameleon renderer can vary response attributes by attaching properties to the request. See :ref:`response_request_attrs`. +.. _response_request_attrs: + +Varying Attributes of Rendered Responses +---------------------------------------- + +Before a response that is constructed as the result of the use of a +:term:`renderer` is returned to BFG, several attributes of the request +are examined which have the potential to influence response behavior. + +View callables that don't directly return a response should set these +values on the ``request`` object via ``setattr`` within the view +callable to influence automatically constructed response attributes. + +``response_content_type`` + + Defines the content-type of the resulting response, + e.g. ``text/xml``. + +``response_headerlist`` + + A sequence of tuples describing cookie values that should be set in + the response, e.g. ``[('Set-Cookie', 'abc=123'), ('X-My-Header', + 'foo')]``. + +``response_status`` + + A WSGI-style status code (e.g. ``200 OK``) describing the status of + the response. + +``response_charset`` + + The character set (e.g. ``UTF-8``) of the response. + +``response_cache_for`` + + A value in seconds which will influence ``Cache-Control`` and + ``Expires`` headers in the returned response. The same can also be + achieved by returning various values in the ``response_headerlist``, + this is purely a convenience. + +.. _adding_and_overriding_renderers: + +Adding and Overriding Renderers +------------------------------- + +Additional configuration declarations can be made which override an +existing :term:`renderer` or which add a new renderer. Adding or +overriding a renderer is accomplished via :term:`ZCML` or via +imperative configuration. + +For example, to add a renderer which renders views which have a +``renderer`` attribute that is a path that ends in ``.jinja2``: + +.. topic:: Via ZCML + + .. code-block:: xml + :linenos: + + <renderer + name=".jinja2" + factory="my.package.MyJinja2Renderer"/> + + The ``factory`` attribute is a dotted Python name that must point + to an implementation of a :term:`renderer`. + + The ``name`` attribute is the renderer name. + +.. topic:: Via Imperative Configuration + + .. code-block:: python + :linenos: + + from my.package import MyJinja2Renderer + config.add_renderer('.jinja2', MyJinja2Renderer) + + The first argument is the renderer name. + + The second argument is a reference to an to an implementation of a + :term:`renderer`. + +A renderer implementation is usually a class which has the following +interface: + +.. code-block:: python + :linenos: + + class RendererFactory: + def __init__(self, name): + """ Constructor: ``name`` may be a path """ + + def __call__(self, value, system): """ Call a the renderer + implementation with the value and the system value passed + in as arguments and return the result (a string or unicode + object). The value is the return value of a view. The + system value is a dictionary containing available system + values (e.g. ``view``, ``context``, and ``request``). """ + +There are essentially two different kinds of ``renderer`` +registrations: registrations that use a dot (``.``) in their ``name`` +argument and ones which do not. + +Renderer registrations that have a ``name`` attribute which starts +with a dot are meant to be *wildcard* registrations. When a ``view`` +configuration is encountered which has a ``name`` attribute that +contains a dot, at startup time, the path is split on its final dot, +and the second element of the split (the filename extension, +typically) is used to look up a renderer for the configured view. The +renderer's factory is still passed the entire ``name`` attribute value +(not just the extension). + +Renderer registrations that have ``name`` attribute which *does not* +start with a dot are meant to be absolute registrations. When a +``view`` configuration is encountered which has a ``name`` argument +that does not contain a dot, the full value of the ``name`` attribute +is used to look up the renderer for the configured view. + +Here's an example of a renderer registration in ZCML: + +.. code-block:: xml + :linenos: + + <renderer + name="amf" + factory="my.package.MyAMFRenderer"/> + +Adding the above ZCML to your application will allow you to use the +``my.package.MyAMFRenderer`` renderer implementation in ``view`` +configurations by referring to it as ``amf`` in the ``renderer`` +attribute: + +.. code-block:: python + :linenos: + + from repoze.bfg.view import bfg_view + + @bfg_view(renderer='amf') + def myview(request): + return {'Hello':'world'} + +By default, when a template extension is unrecognized, an error is +thrown at rendering time. You can associate more than one filename +extension with the same renderer implementation as necessary if you +need to use a different file extension for the same kinds of +templates. For example, to associate the ``.zpt`` extension with the +Chameleon page template renderer factory, use: + +.. code-block:: xml + :linenos: + + <renderer + name=".zpt" + factory="repoze.bfg.chameleon_zpt.renderer_factory"/> + +To override the default mapping in which files with a ``.pt`` +extension are rendered via a Chameleon ZPT page template renderer, use +a variation on the following in your application's ZCML: + +.. code-block:: xml + :linenos: + + <renderer + name=".pt" + factory="my.package.pt_renderer"/> + +To override the default mapping in which files with a ``.txt`` +extension are rendered via a Chameleon text template renderer, use a +variation on the following in your application's ZCML: + +.. code-block:: xml + :linenos: + + <renderer + name=".txt" + factory="my.package.text_renderer"/> + +To associate a *default* renderer with *all* view configurations (even +ones which do not possess a ``renderer`` attribute), use a variation +on the following (ie. omit the ``name`` attribute to the renderer +tag): + +.. code-block:: xml + :linenos: + + <renderer + factory="repoze.bfg.renderers.json_renderer_factory"/> + .. _view_security_section: View Security @@ -1426,6 +1612,31 @@ In this case, ``.models.Root`` refers to the class of which your ``/static/foo.js``. See :ref:`traversal_chapter` for information about "goggles" (``@@``). +Special Exceptions +------------------ + +Usually when a Python exception is raised within view code, +:mod:`repoze.bfg` allows the exception to propagate all the way out to +the :term:`WSGI` server which invoked the application. + +However, for convenience, two special exceptions exist which are +always handled by :mod:`repoze.bfg` itself. These are +``repoze.bfg.exceptions.NotFound`` and +``repoze.bfg.exceptions.Forbidden``. Both is an exception classe +which accept a single positional constructor argument: a message. + +If ``repoze.bfg.exceptions.NotFound`` is raised within view code, the +result of the :term:`Not Found View` will be returned to the user +agent which performed the request. + +If ``repoze.bfg.exceptions.Forbidden`` is raised within view code, the +result of the :term:`Forbidden View` will be returned to the user +agent which performed the request. + +In all cases, the message provided to the exception constructor is +made available to the view which :mod:`repoze.bfg` invokes as +``request.environ['repoze.bfg.message']``. + Using Views to Handle Form Submissions (Unicode and Character Set Issues) ------------------------------------------------------------------------- @@ -1553,188 +1764,3 @@ rendered in a request that has a ``;charset=utf-8`` stanza on its to Unicode objects implicitly in :mod:`repoze.bfg`'s default configuration. The keys are still strings. -.. _response_request_attrs: - -Varying Attributes of Rendered Responses ----------------------------------------- - -Before a response that is constructed as the result of the use of a -:term:`renderer` is returned to BFG, several attributes of the request -are examined which have the potential to influence response behavior. - -View callables that don't directly return a response should set these -values on the ``request`` object via ``setattr`` within the view -callable to influence automatically constructed response attributes. - -``response_content_type`` - - Defines the content-type of the resulting response, - e.g. ``text/xml``. - -``response_headerlist`` - - A sequence of tuples describing cookie values that should be set in - the response, e.g. ``[('Set-Cookie', 'abc=123'), ('X-My-Header', - 'foo')]``. - -``response_status`` - - A WSGI-style status code (e.g. ``200 OK``) describing the status of - the response. - -``response_charset`` - - The character set (e.g. ``UTF-8``) of the response. - -``response_cache_for`` - - A value in seconds which will influence ``Cache-Control`` and - ``Expires`` headers in the returned response. The same can also be - achieved by returning various values in the ``response_headerlist``, - this is purely a convenience. - -.. _adding_and_overriding_renderers: - -Adding and Overriding Renderers -------------------------------- - -Additional configuration declarations can be made which override an -existing :term:`renderer` or which add a new renderer. Adding or -overriding a renderer is accomplished via :term:`ZCML` or via -imperative configuration. - -For example, to add a renderer which renders views which have a -``renderer`` attribute that is a path that ends in ``.jinja2``: - -.. topic:: Via ZCML - - .. code-block:: xml - :linenos: - - <renderer - name=".jinja2" - factory="my.package.MyJinja2Renderer"/> - - The ``factory`` attribute is a dotted Python name that must point - to an implementation of a :term:`renderer`. - - The ``name`` attribute is the renderer name. - -.. topic:: Via Imperative Configuration - - .. code-block:: python - :linenos: - - from my.package import MyJinja2Renderer - config.add_renderer('.jinja2', MyJinja2Renderer) - - The first argument is the renderer name. - - The second argument is a reference to an to an implementation of a - :term:`renderer`. - -A renderer implementation is usually a class which has the following -interface: - -.. code-block:: python - :linenos: - - class RendererFactory: - def __init__(self, name): - """ Constructor: ``name`` may be a path """ - - def __call__(self, value, system): """ Call a the renderer - implementation with the value and the system value passed - in as arguments and return the result (a string or unicode - object). The value is the return value of a view. The - system value is a dictionary containing available system - values (e.g. ``view``, ``context``, and ``request``). """ - -There are essentially two different kinds of ``renderer`` -registrations: registrations that use a dot (``.``) in their ``name`` -argument and ones which do not. - -Renderer registrations that have a ``name`` attribute which starts -with a dot are meant to be *wildcard* registrations. When a ``view`` -configuration is encountered which has a ``name`` attribute that -contains a dot, at startup time, the path is split on its final dot, -and the second element of the split (the filename extension, -typically) is used to look up a renderer for the configured view. The -renderer's factory is still passed the entire ``name`` attribute value -(not just the extension). - -Renderer registrations that have ``name`` attribute which *does not* -start with a dot are meant to be absolute registrations. When a -``view`` configuration is encountered which has a ``name`` argument -that does not contain a dot, the full value of the ``name`` attribute -is used to look up the renderer for the configured view. - -Here's an example of a renderer registration in ZCML: - -.. code-block:: xml - :linenos: - - <renderer - name="amf" - factory="my.package.MyAMFRenderer"/> - -Adding the above ZCML to your application will allow you to use the -``my.package.MyAMFRenderer`` renderer implementation in ``view`` -configurations by referring to it as ``amf`` in the ``renderer`` -attribute: - -.. code-block:: python - :linenos: - - from repoze.bfg.view import bfg_view - - @bfg_view(renderer='amf') - def myview(request): - return {'Hello':'world'} - -By default, when a template extension is unrecognized, an error is -thrown at rendering time. You can associate more than one filename -extension with the same renderer implementation as necessary if you -need to use a different file extension for the same kinds of -templates. For example, to associate the ``.zpt`` extension with the -Chameleon page template renderer factory, use: - -.. code-block:: xml - :linenos: - - <renderer - name=".zpt" - factory="repoze.bfg.chameleon_zpt.renderer_factory"/> - -To override the default mapping in which files with a ``.pt`` -extension are rendered via a Chameleon ZPT page template renderer, use -a variation on the following in your application's ZCML: - -.. code-block:: xml - :linenos: - - <renderer - name=".pt" - factory="my.package.pt_renderer"/> - -To override the default mapping in which files with a ``.txt`` -extension are rendered via a Chameleon text template renderer, use a -variation on the following in your application's ZCML: - -.. code-block:: xml - :linenos: - - <renderer - name=".txt" - factory="my.package.text_renderer"/> - -To associate a *default* renderer with *all* view configurations (even -ones which do not possess a ``renderer`` attribute), use a variation -on the following (ie. omit the ``name`` attribute to the renderer -tag): - -.. code-block:: xml - :linenos: - - <renderer - factory="repoze.bfg.renderers.json_renderer_factory"/> diff --git a/docs/whatsnew-1.2.rst b/docs/whatsnew-1.2.rst index 32a878616..c417ba3ae 100644 --- a/docs/whatsnew-1.2.rst +++ b/docs/whatsnew-1.2.rst @@ -90,6 +90,17 @@ Minor Miscellaneous Feature Additions attached to the constructed model via ``zope.interface.alsoProvides``). +- When the ``repoze.bfg.exceptions.NotFound`` or + ``repoze.bfg.exceptions.Forbidden`` error is raised from within a + custom :term:`root factory` or the factory of a :term:`route`, the + appropriate response is sent to the requesting user agent (the + result of the notfound view or the forbidden view, respectively). + When these errors are raised from within a root factory, the + :term:`context` passed to the notfound or forbidden view will be + ``None``. Also, the request will not be decorated with + ``view_name``, ``subpath``, ``context``, etc. as would normally be + the case if traversal had been allowed to take place. + Backwards Incompatibilites -------------------------- @@ -281,3 +292,7 @@ Documentation Enhancements - Added "Thread Locals" narrative chapter to documentation, and added a API chapter documenting the ``repoze.bfg.threadlocals`` module. +- Added a "Special Exceptions" section to the "Views" narrative + documentation chapter explaining the effect of raising + ``repoze.bfg.exceptions.NotFound`` and + ``repoze.bfg.exceptions.Forbidden`` from within view code. diff --git a/repoze/bfg/router.py b/repoze/bfg/router.py index deffa4e17..630aa201c 100644 --- a/repoze/bfg/router.py +++ b/repoze/bfg/router.py @@ -65,71 +65,73 @@ class Router(object): try: # setup request = Request(environ) + context = None threadlocals['request'] = request attrs = request.__dict__ attrs['registry'] = registry has_listeners and registry.notify(NewRequest(request)) + + try: + # root resolution + 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 + environ['bfg.routes.matchdict'] = match + request.matchdict = match + iface = registry.queryUtility(IRouteRequest, + name=route.name) + if iface is not None: + alsoProvides(request, iface) + root_factory = route.factory or self.root_factory + + root = root_factory(request) + attrs['root'] = root + + # view lookup + traverser = registry.adapters.queryAdapter(root, ITraverser) + if traverser is None: + traverser = ModelGraphTraverser(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 registry.notify(AfterTraversal(request)) + provides = map(providedBy, (context, request)) + view_callable = registry.adapters.lookup( + provides, IView, name=view_name, default=None) - # root resolution - 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 - environ['bfg.routes.matchdict'] = match - request.matchdict = match - iface = registry.queryUtility(IRouteRequest, - name=route.name) - if iface is not None: - alsoProvides(request, iface) - root_factory = route.factory or self.root_factory - - root = root_factory(request) - attrs['root'] = root - - # view lookup - traverser = registry.adapters.queryAdapter(root, ITraverser) - if traverser is None: - traverser = ModelGraphTraverser(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 registry.notify(AfterTraversal(request)) - provides = map(providedBy, (context, request)) - view_callable = registry.adapters.lookup( - provides, IView, name=view_name, default=None) - - # view execution - 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) + # view execution + 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 + environ['repoze.bfg.message'] = msg + response = self.notfound_view(context, request) else: - msg = request.path_info + response = view_callable(context, request) + + except Forbidden, why: + msg = why[0] + environ['repoze.bfg.message'] = msg + response = self.forbidden_view(context, request) + except NotFound, why: + msg = why[0] environ['repoze.bfg.message'] = msg response = self.notfound_view(context, request) - else: - try: - response = view_callable(context, request) - except Forbidden, why: - msg = why[0] - environ['repoze.bfg.message'] = msg - response = self.forbidden_view(context, request) - except NotFound, why: - msg = why[0] - environ['repoze.bfg.message'] = msg - response = self.notfound_view(context, request) # response handling has_listeners and registry.notify(NewResponse(response)) diff --git a/repoze/bfg/tests/test_router.py b/repoze/bfg/tests/test_router.py index 45e66b8fe..5352c6d79 100644 --- a/repoze/bfg/tests/test_router.py +++ b/repoze/bfg/tests/test_router.py @@ -42,7 +42,8 @@ class TestRouter(unittest.TestCase): def _registerTraverserFactory(self, context, view_name='', subpath=None, traversed=None, virtual_root=None, - virtual_root_path=None, **kw): + virtual_root_path=None, raise_error=None, + **kw): from repoze.bfg.interfaces import ITraverser if virtual_root is None: @@ -59,6 +60,8 @@ class TestRouter(unittest.TestCase): self.root = root def __call__(self, request): + if raise_error: + raise raise_error values = {'root':self.root, 'context':context, 'view_name':view_name, @@ -504,6 +507,44 @@ class TestRouter(unittest.TestCase): self.failUnless(req_iface.providedBy(request)) self.failUnless(IFoo.providedBy(request)) + def test_root_factory_raises_notfound(self): + from repoze.bfg.interfaces import IRootFactory + from repoze.bfg.exceptions import NotFound + from zope.interface import Interface + from zope.interface import directlyProvides + def rootfactory(request): + raise NotFound('from root factory') + self.registry.registerUtility(rootfactory, IRootFactory) + class IContext(Interface): + pass + context = DummyContext() + directlyProvides(context, IContext) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(start_response.status, '404 Not Found') + self.failUnless('from root factory' in app_iter[0]) + + def test_root_factory_raises_forbidden(self): + from repoze.bfg.interfaces import IRootFactory + from repoze.bfg.exceptions import Forbidden + from zope.interface import Interface + from zope.interface import directlyProvides + def rootfactory(request): + raise Forbidden('from root factory') + self.registry.registerUtility(rootfactory, IRootFactory) + class IContext(Interface): + pass + context = DummyContext() + directlyProvides(context, IContext) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(start_response.status, '401 Unauthorized') + self.failUnless('from root factory' in app_iter[0]) + class DummyContext: pass |
