Permalink
Browse files

Add "exception views" work contributed primarily by Andrey Popp by me…

…rging the "phash" branch.
  • Loading branch information...
Chris McDonough
Chris McDonough committed Apr 14, 2010
1 parent 2b6bc8a commit ff1213e8f2aed987108ba57aed517c033491b1aa
Showing with 3,046 additions and 1,509 deletions.
  1. +46 −989 CHANGES.txt
  2. +996 −0 HISTORY.txt
  3. +33 −4 TODO.txt
  4. +2 −3 docs/narr/configuration.rst
  5. +66 −41 docs/narr/hooks.rst
  6. +2 −1 docs/narr/urldispatch.rst
  7. +89 −2 docs/narr/views.rst
  8. +1 −1 docs/tutorials/bfgwiki/authorization.rst
  9. +3 −2 docs/tutorials/bfgwiki/src/authorization/tutorial/configure.zcml
  10. +3 −3 docs/tutorials/bfgwiki2/authorization.rst
  11. +3 −2 docs/tutorials/bfgwiki2/src/authorization/tutorial/configure.zcml
  12. +14 −2 docs/zcml/forbidden.rst
  13. +14 −2 docs/zcml/notfound.rst
  14. +6 −0 repoze/bfg/compat/__init__.py
  15. +221 −103 repoze/bfg/configuration.py
  16. +11 −2 repoze/bfg/interfaces.py
  17. +5 −1 repoze/bfg/request.py
  18. +19 −22 repoze/bfg/router.py
  19. +2 −1 repoze/bfg/security.py
  20. +2 −0 repoze/bfg/testing.py
  21. +1 −0 repoze/bfg/tests/exceptionviewapp/__init__.py
  22. +44 −0 repoze/bfg/tests/exceptionviewapp/configure.zcml
  23. +18 −0 repoze/bfg/tests/exceptionviewapp/models.py
  24. +17 −0 repoze/bfg/tests/exceptionviewapp/views.py
  25. +15 −0 repoze/bfg/tests/fixtureapp/configure.zcml
  26. +13 −1 repoze/bfg/tests/fixtureapp/views.py
  27. +56 −0 repoze/bfg/tests/hybridapp/configure.zcml
  28. +22 −0 repoze/bfg/tests/hybridapp/views.py
  29. +661 −82 repoze/bfg/tests/test_configuration.py
  30. +57 −2 repoze/bfg/tests/test_integration.py
  31. +2 −0 repoze/bfg/tests/test_request.py
  32. +439 −139 repoze/bfg/tests/test_router.py
  33. +2 −1 repoze/bfg/tests/test_security.py
  34. +2 −1 repoze/bfg/tests/test_view.py
  35. +113 −80 repoze/bfg/tests/test_zcml.py
  36. +2 −1 repoze/bfg/view.py
  37. +44 −21 repoze/bfg/zcml.py
View
1,035 CHANGES.txt

Large diffs are not rendered by default.

Oops, something went wrong.
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -1,12 +1,41 @@
:mod:`repoze.bfg` TODOs
=======================
-- Decide on ``unhook_zca`` argument to ``tearDown``.
-
-- Named notfound views.
-
- Supply ``X-Vhm-Host`` support.
- Review tutorials.
- Basic WSGI documentation (pipeline / app / server).
+
+- Decide on INotFoundView and IForbidden interface, which are obsolete now.
+
+- Document exception view lookup machinery:
+
+ - Lookup proceeds by request interface first and then by interface provided
+ by exception.
+
+ - If lookup fails with more special request interface (read as request
+ interface related to some route) it will fallback to lookup by IRequest.
+
+ - Current order of interfaces used for exception view lookup leads to the
+ following statement: view with more special request interface and more
+ common context interfaces always matched first, event if we have view
+ with IRequest, but more special context interfaces (see integration tests
+ with hybridapp for route9).
+
+- Exception view backwards compat / features:
+
+ - Add an "exception" attr to the request before calling an exception
+ view.
+
+ - Register wrapper views within set_notfound_view and
+ set_forbidden_view (and ZCML if it doesn't call those) so that
+ "context" is either the "real" context or None.
+
+- Use Venusian for decorator scanning (fix Venusian to have scan
+ categories first).
+
+- Allow a translator to be supplied for template rendering.
+
+- Figure out a way to expose some of the functionality of
+ ``Configurator._derive_view`` as an API.
@@ -198,9 +198,8 @@ effectively a "macro" which calls the
behalf.
The ``<view>`` tag is an example of a :mod:`repoze.bfg` declaration
-tag. Other such tags include ``<route>``, ``<scan>``, ``<notfound>``,
-``<forbidden>``, and others. Each of these tags is effectively a
-"macro" which calls methods of a
+tag. Other such tags include ``<route>`` and ``<scan>``. Each of
+these tags is effectively a "macro" which calls methods of a
:class:`repoze.bfg.configuration.Configurator` object on your behalf.
Essentially, using a :term:`ZCML` file and loading it from the
View
@@ -3,8 +3,8 @@
Using Hooks
===========
-"Hooks" can be used to influence the behavior of the
-:mod:`repoze.bfg` framework in various ways.
+"Hooks" can be used to influence the behavior of the :mod:`repoze.bfg`
+framework in various ways.
.. index::
single: not found view
@@ -15,23 +15,29 @@ Changing the Not Found View
---------------------------
When :mod:`repoze.bfg` can't map a URL to view code, it invokes a
-:term:`not found view`, which is a :term:`view callable`. The view it
-invokes can be customized through application configuration. This
-view can be configured via :term:`imperative configuration` or
-:term:`ZCML`.
+:term:`not found view`, which is a :term:`view callable`. A default
+notfound view exists. The default not found view can be overridden
+through application configuration. This override can be done via
+:term:`imperative configuration` or :term:`ZCML`.
+
+The :term:`not found view` callable is a view callable like any other.
+The :term:`view configuration` which causes it to be a "not found"
+view consists only of naming the :exc:`repoze.bfg.exceptions.NotFound`
+class as the ``context`` of the view configuration.
.. topic:: Using Imperative Configuration
If your application uses :term:`imperative configuration`, you can
replace the Not Found view by using the
- :meth:`repoze.bfg.configuration.Configurator.set_notfound_view`
- method:
+ :meth:`repoze.bfg.configuration.Configurator.add_view` method to
+ register an "exception view":
.. code-block:: python
:linenos:
- import helloworld.views
- config.set_notfound_view(helloworld.views.notfound_view)
+ from repoze.bfg.exceptions import NotFound
+ from helloworld.views import notfound_view
+ config.add_view(notfound_view, context=NotFound)
Replace ``helloworld.views.notfound_view`` with a reference to the
Python :term:`view callable` you want to use to represent the Not
@@ -46,16 +52,22 @@ view can be configured via :term:`imperative configuration` or
.. code-block:: xml
:linenos:
- <notfound
- view="helloworld.views.notfound_view"/>
+ <view
+ view="helloworld.views.notfound_view"
+ context="repoze.bfg.exceptions.NotFound"/>
Replace ``helloworld.views.notfound_view`` with the Python dotted name
to the notfound view you want to use.
- Other attributes of the ``notfound`` directive are documented at
- :ref:`notfound_directive`.
+Like any other view, the notfound view must accept at least a
+``request`` parameter, or both ``context`` and ``request``. The
+``request`` is the current :term:`request` representing the denied
+action. The ``context`` (if used in the call signature) will be the
+instance of the :exc:`repoze.bfg.exceptions.NotFound` exception that
+caused the view to be called.
-Here's some sample code that implements a minimal NotFound view:
+Here's some sample code that implements a minimal NotFound view
+callable:
.. code-block:: python
:linenos:
@@ -65,13 +77,21 @@ Here's some sample code that implements a minimal NotFound view:
def notfound_view(request):
return HTTPNotFound()
-.. note:: When a NotFound view is invoked, it is passed a
- :term:`request`. The ``environ`` attribute of the request is the
- WSGI environment. Within the WSGI environ will be a key named
- ``repoze.bfg.message`` that has a value explaining why the not
- found error was raised. This error will be different when the
- ``debug_notfound`` environment setting is true than it is when it
- is false.
+.. note:: When a NotFound view callable is invoked, it is passed a
+ :term:`request`. The ``exception`` attribute of the request will
+ be an instance of the :exc:`repoze.bfg.exceptions.NotFound`
+ exception that caused the not found view to be called. The value
+ of ``request.exception.args[0]`` will be a value explaining why the
+ not found error was raised. This message will be different when
+ the ``debug_notfound`` environment setting is true than it is when
+ it is false.
+
+.. warning:: When a NotFound view callable accepts an argument list as
+ described in :ref:`request_and_context_view_definitions`, the
+ ``context`` passed as the first argument to the view callable will
+ be the :exc:`repoze.bfg.exceptions.NotFound` exception instance.
+ If available, the *model* context will still be available as
+ ``request.context``.
.. index::
single: forbidden view
@@ -84,21 +104,28 @@ Changing the Forbidden View
When :mod:`repoze.bfg` can't authorize execution of a view based on
the :term:`authorization policy` in use, it invokes a :term:`forbidden
view`. The default forbidden response has a 401 status code and is
-very plain, but it can be overridden as necessary using either
-:term:`imperative configuration` or :term:`ZCML`:
+very plain, but the view which generates it can be overridden as
+necessary using either :term:`imperative configuration` or
+:term:`ZCML`:
+
+The :term:`forbidden view` callable is a view callable like any other.
+The :term:`view configuration` which causes it to be a "not found"
+view consists only of naming the :exc:`repoze.bfg.exceptions.Forbidden`
+class as the ``context`` of the view configuration.
.. topic:: Using Imperative Configuration
If your application uses :term:`imperative configuration`, you can
replace the Forbidden view by using the
- :meth:`repoze.bfg.configuration.Configurator.set_forbidden_view`
- method:
+ :meth:`repoze.bfg.configuration.Configurator.add_view` method to
+ register an "exception view":
.. code-block:: python
:linenos:
- import helloworld.views
- config.set_forbiddden_view(helloworld.views.forbidden_view)
+ from helloworld.views import forbidden_view
+ from repoze.bfg.exceptions import Forbidden
+ config.add_view(forbidden_view, context=Forbidden)
Replace ``helloworld.views.forbidden_view`` with a reference to the
Python :term:`view callable` you want to use to represent the
@@ -113,16 +140,13 @@ very plain, but it can be overridden as necessary using either
.. code-block:: xml
:linenos:
- <forbidden
- view="helloworld.views.forbidden_view"/>
-
+ <view
+ view="helloworld.views.notfound_view"
+ context="repoze.bfg.exceptions.Forbidden"/>
Replace ``helloworld.views.forbidden_view`` with the Python
dotted name to the forbidden view you want to use.
- Other attributes of the ``forbidden`` directive are documented at
- :ref:`forbidden_directive`.
-
Like any other view, the forbidden view must accept at least a
``request`` parameter, or both ``context`` and ``request``. The
``context`` (available as ``request.context`` if you're using the
@@ -140,13 +164,14 @@ Here's some sample code that implements a minimal forbidden view:
def forbidden_view(request):
return render_template_to_response('templates/login_form.pt')
-.. note:: When a forbidden view is invoked, it is passed the
- :term:`request` as the second argument. An attribute of the
- request is ``environ``, which is the WSGI environment. Within the
- WSGI environ will be a key named ``repoze.bfg.message`` that has a
- value explaining why the current view invocation was forbidden.
- This error will be different when the ``debug_authorization``
- environment setting is true than it is when it is false.
+.. note:: When a forbidden view callable is invoked, it is passed a
+ :term:`request`. The ``exception`` attribute of the request will
+ be an instance of the :exc:`repoze.bfg.exceptions.Forbidden`
+ exception that caused the forbidden view to be called. The value
+ of ``request.exception.args[0]`` will be a value explaining why the
+ forbidden was raised. This message will be different when the
+ ``debug_authorization`` environment setting is true than it is when
+ it is false.
.. warning:: the default forbidden view sends a response with a ``401
Unauthorized`` status code for backwards compatibility reasons.
@@ -936,7 +936,8 @@ stanza:
.. code-block:: xml
:linenos:
- <notfound
+ <view
+ context="repoze.bfg.exceptions.NotFound"
view="repoze.bfg.views.append_slash_notfound_view"
/>
View
@@ -133,7 +133,7 @@ represent the method expected to return a response, you can use an
.. _request_and_context_view_definitions:
-Request-And-Context View Callable Definitions
+Context-And-Request View Callable Definitions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Usually, view callables are defined to accept only a single argument:
@@ -813,6 +813,8 @@ See also :ref:`renderer_directive` and
.. index::
single: view exceptions
+.. _special_exceptions_in_callables:
+
Using Special Exceptions In View Callables
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -836,7 +838,92 @@ 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']``.
+``request.exception.args[0]``.
+
+Exception Views
+~~~~~~~~~~~~~~~~
+
+The machinery which allows the special
+:exc:`repoze.bfg.exceptions.NotFound` and
+:exc:`repoze.bfg.exceptions.Forbidden` exceptions to be caught by
+specialized views as described in
+:ref:`special_exceptions_in_callables` can also be used by application
+developers to convert arbitrary exceptions to responses.
+
+To register a view that should be called whenever a particular
+exception is raised from with :mod:`repoze.bfg` view code, use the
+exception class or one of its superclasses as the ``context`` of a
+view configuration which points at a view callable you'd like to
+generate a response.
+
+For example, given the following exception class in a module named
+``helloworld.exceptions``:
+
+.. code-block:: python
+ :linenos:
+
+ class ValidationFailure(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+
+You can wire a view callable to be called whenever any of your *other*
+code raises a ``hellworld.exceptions.ValidationFailure`` exception:
+
+.. code-block:: python
+ :linenos:
+
+ from helloworld.exceptions import ValidationFailure
+
+ @bfg_view(context=ValidationFailure)
+ def failed_validation(exc, request):
+ response = Response('Failed validation: %s' % exc.msg)
+ response.status_int = 500
+ return response
+
+Assuming that a :term:`scan` was run to pick up this view
+registration, this view callable will be invoked whenever a
+``helloworld.exceptions.ValidationError`` is raised by your
+application's view code. The same exception raised by a custom root
+factory or a custom traverser is also caught and hooked.
+
+Other normal view predicates can also be used in combination with an
+exception view registration:
+
+.. code-block:: python
+ :linenos:
+
+ from repoze.bfg.view import bfg_view
+ from repoze.bfg.exceptions import NotFound
+ from webob.exc import HTTPNotFound
+
+ @bfg_view(context=NotFound, route_name='home')
+ def notfound_view(request):
+ return HTTPNotFound()
+
+The above exception view names the ``route_name`` of ``home``, meaning
+that it will only be called when the route matched has a name of
+``home``. You can therefore have more than one exception view for any
+given exception in the system: the "most specific" one will be called
+when the set of request circumstances which match the view
+registration.
+
+The only view predicate that cannot be not be used successfully when
+creating an exception view configuration is ``name``. The name used
+to look up an exception view is always the empty string. Views
+registered as exception views which have a name will be ignored.
+
+.. note::
+
+ Normal (non-exception) views registered against a context which
+ inherits from :exc:`Exception` will work normally. When an
+ exception view configuraton is processed, *two* exceptions are
+ registered. One as a "normal" view, the other as an "exception"
+ view. This means that you can use an exception as ``context`` for a
+ normal view.
+
+The feature can be used with any view registration mechanism
+(``@bfg_view`` decorator, ZCML, or imperative ``add_view`` styles).
.. index::
single: unicode, views, and forms
@@ -27,7 +27,7 @@ Changing ``configure.zcml``
We'll change our ``configure.zcml`` file to enable an
``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to
-enable declarative security checking. We'll also add a ``forbidden``
+enable declarative security checking. We'll also add a new view
stanza, which species a :term:`forbidden view`. This configures our
login view to show up when :mod:`repoze.bfg` detects that a view
invocation can not be authorized. When you're done, your
@@ -5,9 +5,10 @@
<scan package="."/>
- <forbidden
+ <view
view=".login.login"
- renderer="templates/login.pt"/>
+ renderer="templates/login.pt"
+ context="repoze.bfg.exceptions.Forbidden"/>
<authtktauthenticationpolicy
secret="sosecret"
@@ -84,9 +84,9 @@ Changing ``configure.zcml``
We'll change our ``configure.zcml`` file to enable an
``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to
enable declarative security checking. We'll also change
-``configure.zcml`` to add a ``forbidden`` stanza which points at our
-``login`` :term:`view callable`, also known as a :term:`forbidden
-view`. This configures our newly created login view to show up when
+``configure.zcml`` to add a view stanza which points at our ``login``
+:term:`view callable`, also known as a :term:`forbidden view`. This
+configures our newly created login view to show up when
:mod:`repoze.bfg` detects that a view invocation can not be
authorized. Also, we'll add ``view_permission`` attributes with the
value ``edit`` to the ``edit_page`` and ``add_page`` route
Oops, something went wrong.

0 comments on commit ff1213e

Please sign in to comment.