summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2010-08-08 07:25:28 +0000
committerChris McDonough <chrism@agendaless.com>2010-08-08 07:25:28 +0000
commitd96ff9144f98bb44254f77f56e55967c46b09774 (patch)
tree7cabc160ce28460ebffd70a5e419f1c74178bd96
parent9192964c9ccc4b0c1c2f1948af1b62012a11ef7c (diff)
downloadpyramid-d96ff9144f98bb44254f77f56e55967c46b09774.tar.gz
pyramid-d96ff9144f98bb44254f77f56e55967c46b09774.tar.bz2
pyramid-d96ff9144f98bb44254f77f56e55967c46b09774.zip
- New public interface: ``repoze.bfg.exceptions.IExceptionResponse``.
This interface is provided by all internal exception classes (such as ``repoze.bfg.exceptions.NotFound`` and ``repoze.bfg.exceptions.Forbidden``), instances of which are both exception objects and can behave as WSGI response objects. This interface is made public so that exception classes which are also valid WSGI response factories can be configured to implement them or exception instances which are also or response instances can be configured to provide them. - New API class: ``repoze.bfg.view.AppendSlashNotFoundViewFactory`` (undoes previous custom_notfound_view on request passsed to append_slash_notfound_view). - Previously, two default view functions were registered at Configurator setup (one for ``repoze.bfg.exceptions.NotFound`` named ``default_notfound_view`` and one for ``repoze.bfg.exceptions.Forbidden`` named ``default_forbidden_view``) to render internal exception responses. Those default view functions have been removed, replaced with a generic default view function which is registered at Configurator setup for the ``repoze.bfg.interfaces.IExceptionResponse`` interface that simply returns the exception instance; the ``NotFound` and ``Forbidden`` classes are now still exception factories but they are also response factories which generate instances that implement the new ``repoze.bfg.interfaces.IExceptionResponse`` interface.
-rw-r--r--CHANGES.txt78
-rw-r--r--docs/api/interfaces.rst8
-rw-r--r--docs/api/view.rst3
-rw-r--r--docs/narr/urldispatch.rst39
-rw-r--r--docs/whatsnew-1.3.rst48
-rw-r--r--repoze/bfg/configuration.py17
-rw-r--r--repoze/bfg/exceptions.py68
-rw-r--r--repoze/bfg/interfaces.py22
-rw-r--r--repoze/bfg/tests/test_configuration.py57
-rw-r--r--repoze/bfg/tests/test_exceptions.py45
-rw-r--r--repoze/bfg/tests/test_view.py119
-rw-r--r--repoze/bfg/view.py193
12 files changed, 442 insertions, 255 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 65a08a781..351d284df 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,42 +4,48 @@ Next release
Features
--------
-- There can only be one Not Found view in any ``repoze.bfg``
- application. If you use
+- New public interface: ``repoze.bfg.exceptions.IExceptionResponse``.
+ This interface is provided by all internal exception classes (such
+ as ``repoze.bfg.exceptions.NotFound`` and
+ ``repoze.bfg.exceptions.Forbidden``), instances of which are both
+ exception objects and can behave as WSGI response objects. This
+ interface is made public so that exception classes which are also
+ valid WSGI response factories can be configured to implement them
+ or exception instances which are also or response instances can be
+ configured to provide them.
+
+- New API class: ``repoze.bfg.view.AppendSlashNotFoundViewFactory``.
+
+ There can only be one Not Found view in any :mod:`repoze.bfg
+ application. Even if you use
``repoze.bfg.view.append_slash_notfound_view`` as the Not Found
- view, it still must generate a NotFound response when it cannot
- redirect to a slash-appended URL; this not found response will be
- visible to site users.
-
- As of this release, if you wish to use a custom notfound view
- callable when ``append_slash_notfound_view`` does not redirect to a
- slash-appended URL, use a wrapper function as the
- ``repoze.bfg.exceptions.NotFound`` view; have this wrapper attach a
- view callable which returns a response to the request object named
- ``custom_notfound_view`` before calling
- ``append_slash_notfound_view``. For example::
-
- from webob.exc import HTTPNotFound
+ view, ``repoze.bfg`` still must generate a ``404 Not Found``
+ response when it cannot redirect to a slash-appended URL; this not
+ found response will be visible to site users.
+
+ If you don't care what this 404 response looks like, and you only
+ need redirections to slash-appended route URLs, you may use the
+ ``repoze.bfg.view.append_slash_notfound_view`` object as the Not
+ Found view. However, if you wish to use a *custom* notfound view
+ callable when a URL cannot be redirected to a slash-appended URL,
+ you may wish to use an instance of the
+ ``repoze.bfg.view.AppendSlashNotFoundViewFactory`` class as the Not
+ Found view, supplying the notfound view callable as the first
+ argument to its constructor. For instance::
+
from repoze.bfg.exceptions import NotFound
- from repoze.bfg.view import append_slash_notfound_view
+ from repoze.bfg.view import AppendSlashNotFoundViewFactory
- def notfound_view(exc, request):
- def fallback_notfound_view(exc, request):
- return HTTPNotFound('It aint there, stop trying!')
- request.fallback_notfound_view = fallback_notfound_view
- return append_slash_notfound_view(exc, request)
+ def notfound_view(context, request):
+ return HTTPNotFound('It aint there, stop trying!')
- config.add_view(notfound_view, context=NotFound)
+ custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
+ config.add_view(custom_append_slash, context=NotFound)
- ``custom_notfound_view`` must adhere to the two-argument view
+ The ``notfound_view`` supplied must adhere to the two-argument view
callable calling convention of ``(context, request)`` (``context``
will be the exception object).
- If ``custom_notfound_view`` is not found on the request object, a
- default notfound response will be generated when the
- ``append_slash_notfound_view`` doesn't redirect to a slash-appended
- URL.
-
Documentation
--------------
@@ -49,6 +55,22 @@ Documentation
- Expanded the "Redirecting to Slash-Appended Routes" section of the
URL Dispatch narrative chapter.
+Internal
+--------
+
+- Previously, two default view functions were registered at
+ Configurator setup (one for ``repoze.bfg.exceptions.NotFound`` named
+ ``default_notfound_view`` and one for
+ ``repoze.bfg.exceptions.Forbidden`` named
+ ``default_forbidden_view``) to render internal exception responses.
+ Those default view functions have been removed, replaced with a
+ generic default view function which is registered at Configurator
+ setup for the ``repoze.bfg.interfaces.IExceptionResponse`` interface
+ that simply returns the exception instance; the ``NotFound` and
+ ``Forbidden`` classes are now still exception factories but they are
+ also response factories which generate instances that implement the
+ new ``repoze.bfg.interfaces.IExceptionResponse`` interface.
+
1.3a7 (2010-08-01)
==================
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index 3e68d2e9f..e024fcff7 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -5,6 +5,9 @@
.. automodule:: repoze.bfg.interfaces
+Event-Related Interfaces
+++++++++++++++++++++++++
+
.. autoclass:: IAfterTraversal
.. autoclass:: INewRequest
@@ -13,4 +16,9 @@
.. autoclass:: IWSGIApplicationCreatedEvent
+Other Interfaces
+++++++++++++++++
+
+ .. autoclass:: IExceptionResponse
+
diff --git a/docs/api/view.rst b/docs/api/view.rst
index e345a0015..f15f1bc0d 100644
--- a/docs/api/view.rst
+++ b/docs/api/view.rst
@@ -19,6 +19,7 @@
.. autoclass:: static
:members:
- .. autofunction:: append_slash_notfound_view
+ .. autofunction:: append_slash_notfound_view(context, request)
+ .. autoclass:: AppendSlashNotFoundViewFactory
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 6e3a68c97..dd45c8d4d 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -1132,6 +1132,45 @@ general description of how to configure a not found view.
.. note:: This feature is new as of :mod:`repoze.bfg` 1.1.
+Custom Not Found View With Slash Appended Routes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There can only be one :term:`Not Found view` in any :mod:`repoze.bfg
+application. Even if you use
+:func:`repoze.bfg.view.append_slash_notfound_view` as the Not Found
+view, :mod:`repoze.bfg` still must generate a ``404 Not Found``
+response when it cannot redirect to a slash-appended URL; this not
+found response will be visible to site users.
+
+If you don't care what this 404 response looks like, and only you need
+redirections to slash-appended route URLs, you may use the
+:func:`repoze.bfg.view.append_slash_notfound_view` object as the Not
+Found view as described above. However, if you wish to use a *custom*
+notfound view callable when a URL cannot be redirected to a
+slash-appended URL, you may wish to use an instance of the
+:class:`repoze.bfg.view.AppendSlashNotFoundViewFactory` class as the
+Not Found view, supplying a :term:`view callable` to be used as the
+custom notfound view as the first argument to its constructor. For
+instance:
+
+.. code-block:: python
+
+ from repoze.bfg.exceptions import NotFound
+ from repoze.bfg.view import AppendSlashNotFoundViewFactory
+
+ def notfound_view(context, request):
+ return HTTPNotFound('It aint there, stop trying!')
+
+ custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
+ config.add_view(custom_append_slash, context=NotFound)
+
+The ``notfound_view`` supplied must adhere to the two-argument view
+callable calling convention of ``(context, request)`` (``context``
+will be the exception object).
+
+.. note:: The :class:`repoze.bfg.view.AppendSlashNotFoundViewFactory`
+ class is new as of BFG 1.3.
+
.. _cleaning_up_after_a_request:
Cleaning Up After a Request
diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst
index f0a7b87f7..07f63b9d6 100644
--- a/docs/whatsnew-1.3.rst
+++ b/docs/whatsnew-1.3.rst
@@ -235,21 +235,41 @@ Minor Feature Additions
- The :func:`repoze.bfg.configuration.Configurator.add_route` API now
returns the route object that was added.
-- There can only be one Not Found view in any :mod:`repoze.bfg`
- application. If you use
+- New API class:
+ :class:`repoze.bfg.view.AppendSlashNotFoundViewFactory`.
+
+ There can only be one :term:`Not Found view` in any :mod:`repoze.bfg
+ application. Even if you use
:func:`repoze.bfg.view.append_slash_notfound_view` as the Not Found
- view, it still must generate a 404 response when it cannot redirect
- to a slash-appended URL; this not found response will be visible to
- site users. As of this release, if you wish to use a custom
- notfound view callable when
- :func:`repoze.bfg.view.append_slash_notfound_view` does not redirect
- to a slash-appended URL, use a wrapper function as the
- :exc:`repoze.bfg.exceptions.NotFound` exception view; have this
- wrapper attach a view callable which returns a response to the
- request object named ``custom_notfound_view`` before calling
- :func:`repoze.bfg.view.append_slash_notfound_view`. See
- :func:`repoze.bfg.view.append_slash_notfound_view` for more
- information.
+ view, :mod:`repoze.bfg` still must generate a ``404 Not Found``
+ response when it cannot redirect to a slash-appended URL; this not
+ found response will be visible to site users.
+
+ If you don't care what this 404 response looks like, and only you
+ need redirections to slash-appended route URLs, you may use the
+ :func:`repoze.bfg.view.append_slash_notfound_view` object as the Not
+ Found view. However, if you wish to use a *custom* notfound view
+ callable when a URL cannot be redirected to a slash-appended URL,
+ you may wish to use an instance of the
+ :class:`repoze.bfg.view.AppendSlashNotFoundViewFactory` class as the
+ Not Found view, supplying a :term:`view callable` to be used as the
+ custom notfound view as the first argument to its constructor. For
+ instance:
+
+ .. code-block:: python
+
+ from repoze.bfg.exceptions import NotFound
+ from repoze.bfg.view import AppendSlashNotFoundViewFactory
+
+ def notfound_view(context, request):
+ return HTTPNotFound('It aint there, stop trying!')
+
+ custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
+ config.add_view(custom_append_slash, context=NotFound)
+
+ The ``notfound_view`` supplied must adhere to the two-argument view
+ callable calling convention of ``(context, request)`` (``context``
+ will be the exception object).
Backwards Incompatibilities
---------------------------
diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py
index f694d7737..0ab4abcb5 100644
--- a/repoze/bfg/configuration.py
+++ b/repoze/bfg/configuration.py
@@ -38,6 +38,8 @@ from repoze.bfg.interfaces import ITranslationDirectories
from repoze.bfg.interfaces import ITraverser
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IViewClassifier
+from repoze.bfg.interfaces import IExceptionResponse
+from repoze.bfg.interfaces import IException
from repoze.bfg import chameleon_text
from repoze.bfg import chameleon_zpt
@@ -69,8 +71,7 @@ from repoze.bfg.traversal import DefaultRootFactory
from repoze.bfg.traversal import find_interface
from repoze.bfg.urldispatch import RoutesMapper
from repoze.bfg.view import render_view_to_response
-from repoze.bfg.view import default_notfound_view
-from repoze.bfg.view import default_forbidden_view
+from repoze.bfg.view import default_exceptionresponse_view
MAX_ORDER = 1 << 30
DEFAULT_PHASH = md5().hexdigest()
@@ -408,8 +409,8 @@ class Configurator(object):
authorization_policy)
for name, renderer in renderers:
self.add_renderer(name, renderer)
- self.add_view(default_notfound_view, context=NotFound)
- self.add_view(default_forbidden_view, context=Forbidden)
+ self.add_view(default_exceptionresponse_view,
+ context=IExceptionResponse)
if locale_negotiator:
registry.registerUtility(locale_negotiator, ILocaleNegotiator)
if request_factory:
@@ -2312,8 +2313,12 @@ def _attr_wrap(view, accept, order, phash):
return attr_view
def isexception(o):
- return isinstance(o, Exception) or (
- inspect.isclass(o) and issubclass(o, Exception)
+ if IInterface.providedBy(o):
+ if IException.isEqualOrExtendedBy(o):
+ return True
+ return (
+ isinstance(o, Exception) or
+ (inspect.isclass(o) and (issubclass(o, Exception)))
)
# note that ``options`` is a b/w compat alias for ``settings`` and
diff --git a/repoze/bfg/exceptions.py b/repoze/bfg/exceptions.py
index 00cb76883..9b885d9dc 100644
--- a/repoze/bfg/exceptions.py
+++ b/repoze/bfg/exceptions.py
@@ -1,7 +1,43 @@
from zope.configuration.exceptions import ConfigurationError as ZCE
+from zope.interface import implements
-class Forbidden(Exception):
- """\
+from repoze.bfg.decorator import reify
+from repoze.bfg.interfaces import IExceptionResponse
+import cgi
+
+class ExceptionResponse(Exception):
+ """ Abstract class to support behaving as a WSGI response object """
+ implements(IExceptionResponse)
+ status = None
+
+ def __init__(self, message=''):
+ Exception.__init__(self, message) # B / C
+ self.message = message
+
+ @reify # defer execution until asked explicitly
+ def app_iter(self):
+ return [
+ """
+ <html>
+ <title>%s</title>
+ <body>
+ <h1>%s</h1>
+ <code>%s</code>
+ </body>
+ </html>
+ """ % (self.status, self.status, cgi.escape(self.message))
+ ]
+
+ @reify # defer execution until asked explicitly
+ def headerlist(self):
+ return [
+ ('Content-Length', str(len(self.app_iter[0]))),
+ ('Content-Type', 'text/html')
+ ]
+
+
+class Forbidden(ExceptionResponse):
+ """
Raise this exception within :term:`view` code to immediately
return the :term:`forbidden view` to the invoking user. Usually
this is a basic ``401`` page, but the forbidden view can be
@@ -11,10 +47,12 @@ class Forbidden(Exception):
which should be a string. The value of this string will be placed
into the WSGI environment by the router under the
``repoze.bfg.message`` key, for availability to the
- :term:`Forbidden View`."""
+ :term:`Forbidden View`.
+ """
+ status = '401 Unauthorized'
-class NotFound(Exception):
- """\
+class NotFound(ExceptionResponse):
+ """
Raise this exception within :term:`view` code to immediately
return the :term:`Not Found view` to the invoking user. Usually
this is a basic ``404`` page, but the Not Found view can be
@@ -24,7 +62,18 @@ class NotFound(Exception):
which should be a string. The value of this string will be placed
into the WSGI environment by the router under the
``repoze.bfg.message`` key, for availability to the :term:`Not Found
- View`."""
+ View`.
+ """
+ status = '404 Not Found'
+
+class PredicateMismatch(NotFound):
+ """
+ Internal exception (not an API) raised by multiviews when no
+ view matches. This exception subclasses the ``NotFound``
+ exception only one reason: if it reaches the main exception
+ handler, it should be treated like a ``NotFound`` by any exception
+ view registrations.
+ """
class URLDecodeError(UnicodeDecodeError):
"""
@@ -41,10 +90,3 @@ class ConfigurationError(ZCE):
""" Raised when inappropriate input values are supplied to an API
method of a :term:`Configurator`"""
-class PredicateMismatch(NotFound):
- """ Internal exception (not an API) raised by multiviews when no
- view matches. This exception subclasses the ``NotFound``
- exception only one reason: if it reaches the main exception
- handler, it should be treated like a ``NotFound`` by any exception
- view registrations."""
-
diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py
index def957dad..086c93f3a 100644
--- a/repoze/bfg/interfaces.py
+++ b/repoze/bfg/interfaces.py
@@ -24,6 +24,22 @@ class IWSGIApplicationCreatedEvent(Interface):
is called."""
app = Attribute(u"Published application")
+class IResponse(Interface): # not an API
+ status = Attribute('WSGI status code of response')
+ headerlist = Attribute('List of response headers')
+ app_iter = Attribute('Iterable representing the response body')
+
+class IException(Interface): # not an API
+ """ An interface representing a generic exception """
+
+class IExceptionResponse(IException, IResponse):
+ """ An interface representing a WSGI response which is also an
+ exception object. Register an exception view using this interface
+ as a ``context`` to apply the registered view for all exception
+ types raised by :mod:`repoze.bfg` internally
+ (:class:`repoze.bfg.exceptions.NotFound` and
+ :class:`repoze.bfg.exceptions.Forbidden`)."""
+
# internal interfaces
class IRequest(Interface):
@@ -35,11 +51,6 @@ class IRouteRequest(Interface):
""" *internal only* interface used as in a utility lookup to find
route-specific interfaces. Not an API."""
-class IResponse(Interface):
- status = Attribute('WSGI status code of response')
- headerlist = Attribute('List of response headers')
- app_iter = Attribute('Iterable representing the response body')
-
class IAuthenticationPolicy(Interface):
""" An object representing a BFG authentication policy. """
def authenticated_userid(request):
@@ -271,3 +282,4 @@ class ILocaleNegotiator(Interface):
class ITranslationDirectories(Interface):
""" A list object representing all known translation directories
for an application"""
+
diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py
index 2f001ba8d..1eae66094 100644
--- a/repoze/bfg/tests/test_configuration.py
+++ b/repoze/bfg/tests/test_configuration.py
@@ -194,11 +194,9 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(reg.notify(1), None)
self.assertEqual(reg.events, (1,))
- def test_setup_registry_registers_default_exception_views(self):
- from repoze.bfg.exceptions import NotFound
- from repoze.bfg.exceptions import Forbidden
- from repoze.bfg.view import default_notfound_view
- from repoze.bfg.view import default_forbidden_view
+ def test_setup_registry_registers_default_exceptionresponse_view(self):
+ from repoze.bfg.interfaces import IExceptionResponse
+ from repoze.bfg.view import default_exceptionresponse_view
class DummyRegistry(object):
def registerUtility(self, *arg, **kw):
pass
@@ -207,10 +205,25 @@ class ConfiguratorTests(unittest.TestCase):
views = []
config.add_view = lambda *arg, **kw: views.append((arg, kw))
config.setup_registry()
- self.assertEqual(views[0], ((default_notfound_view,),
- {'context':NotFound}))
- self.assertEqual(views[1], ((default_forbidden_view,),
- {'context':Forbidden}))
+ self.assertEqual(views[0], ((default_exceptionresponse_view,),
+ {'context':IExceptionResponse}))
+
+ def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self):
+ from zope.interface import implementedBy
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.exceptions import NotFound
+ from repoze.bfg.registry import Registry
+ reg = Registry()
+ config = self._makeOne(reg)
+ config.setup_registry() # registers IExceptionResponse default view
+ def myview(context, request):
+ return 'OK'
+ config.add_view(myview, context=NotFound)
+ request = self._makeRequest(config)
+ view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound),
+ request_iface=IRequest)
+ result = view(None, request)
+ self.assertEqual(result, 'OK')
def test_setup_registry_custom_settings(self):
from repoze.bfg.registry import Registry
@@ -3702,6 +3715,32 @@ class TestDottedNameResolver(unittest.TestCase):
self.assertEqual(e.args[0],
"The dotted name 'cant.be.found' cannot be imported")
+class Test_isexception(unittest.TestCase):
+ def _callFUT(self, ob):
+ from repoze.bfg.configuration import isexception
+ return isexception(ob)
+
+ def test_is_exception_instance(self):
+ class E(Exception):
+ pass
+ e = E()
+ self.assertEqual(self._callFUT(e), True)
+
+ def test_is_exception_class(self):
+ class E(Exception):
+ pass
+ self.assertEqual(self._callFUT(E), True)
+
+ def test_is_IException(self):
+ from repoze.bfg.interfaces import IException
+ self.assertEqual(self._callFUT(IException), True)
+
+ def test_is_IException_subinterface(self):
+ from repoze.bfg.interfaces import IException
+ class ISubException(IException):
+ pass
+ self.assertEqual(self._callFUT(ISubException), True)
+
class DummyRequest:
subpath = ()
def __init__(self):
diff --git a/repoze/bfg/tests/test_exceptions.py b/repoze/bfg/tests/test_exceptions.py
new file mode 100644
index 000000000..4091eb941
--- /dev/null
+++ b/repoze/bfg/tests/test_exceptions.py
@@ -0,0 +1,45 @@
+import unittest
+
+class TestExceptionResponse(unittest.TestCase):
+ def _makeOne(self, message):
+ from repoze.bfg.exceptions import ExceptionResponse
+ return ExceptionResponse(message)
+
+ def test_app_iter(self):
+ exc = self._makeOne('')
+ self.failUnless('<code></code>' in exc.app_iter[0])
+
+ def test_headerlist(self):
+ exc = self._makeOne('')
+ headerlist = exc.headerlist
+ headerlist.sort()
+ app_iter = exc.app_iter
+ clen = str(len(app_iter[0]))
+ self.assertEqual(headerlist[0], ('Content-Length', clen))
+ self.assertEqual(headerlist[1], ('Content-Type', 'text/html'))
+
+ def test_withmessage(self):
+ exc = self._makeOne('abc&123')
+ self.failUnless('<code>abc&amp;123</code>' in exc.app_iter[0])
+
+class TestNotFound(unittest.TestCase):
+ def _makeOne(self, message):
+ from repoze.bfg.exceptions import NotFound
+ return NotFound(message)
+
+ def test_it(self):
+ from repoze.bfg.exceptions import ExceptionResponse
+ e = self._makeOne('notfound')
+ self.failUnless(isinstance(e, ExceptionResponse))
+ self.assertEqual(e.status, '404 Not Found')
+
+class TestForbidden(unittest.TestCase):
+ def _makeOne(self, message):
+ from repoze.bfg.exceptions import Forbidden
+ return Forbidden(message)
+
+ def test_it(self):
+ from repoze.bfg.exceptions import ExceptionResponse
+ e = self._makeOne('unauthorized')
+ self.failUnless(isinstance(e, ExceptionResponse))
+ self.assertEqual(e.status, '401 Unauthorized')
diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py
index 27f468c74..18a46a205 100644
--- a/repoze/bfg/tests/test_view.py
+++ b/repoze/bfg/tests/test_view.py
@@ -342,59 +342,7 @@ class TestBFGViewDecorator(unittest.TestCase):
self.assertEqual(settings[0]['renderer'],
'repoze.bfg.tests:fixtures/minimal.pt')
-class TestDefaultView(BaseTest):
- def test_no_registry_on_request(self):
- request = None
- context = Exception()
- response = self._callFUT(context, request)
- self.assertEqual(response.status, self.status)
- self.failUnless('<code></code>' in response.body)
-
- def test_nomessage(self):
- request = self._makeRequest()
- context = Exception()
- response = self._callFUT(context, request)
- self.assertEqual(response.status, self.status)
- self.failUnless('<code></code>' in response.body)
-
- def test_withmessage(self):
- request = self._makeRequest()
- context = Exception('abc&123')
- response = self._callFUT(context, request)
- self.assertEqual(response.status, self.status)
- self.failUnless('<code>abc&amp;123</code>' in response.body)
-
- def test_context_not_exception(self):
- request = self._makeRequest()
- request.exception = Exception('woo')
- context = None
- response = self._callFUT(context, request)
- self.assertEqual(response.status, self.status)
- self.failUnless('<code>woo</code>' in response.body)
-
- def test_msg_exception_raised(self):
- request = self._makeRequest()
- context = None
- response = self._callFUT(context, request)
- self.assertEqual(response.status, self.status)
- self.failUnless('<code></code>' in response.body)
-
-class TestDefaultForbiddenView(TestDefaultView, unittest.TestCase):
- status = '401 Unauthorized'
-
- def _callFUT(self, context, request):
- from repoze.bfg.view import default_forbidden_view
- return default_forbidden_view(context, request)
-
-
-class TestDefaultNotFoundView(TestDefaultView, unittest.TestCase):
- status = '404 Not Found'
-
- def _callFUT(self, context, request):
- from repoze.bfg.view import default_notfound_view
- return default_notfound_view(context, request)
-
-class AppendSlashNotFoundView(BaseTest, unittest.TestCase):
+class Test_append_slash_notfound_view(BaseTest, unittest.TestCase):
def _callFUT(self, context, request):
from repoze.bfg.view import append_slash_notfound_view
return append_slash_notfound_view(context, request)
@@ -417,49 +365,81 @@ class AppendSlashNotFoundView(BaseTest, unittest.TestCase):
def test_context_is_not_exception(self):
request = self._makeRequest(PATH_INFO='/abc')
- request.exception = Exception('halloo')
+ request.exception = ExceptionResponse()
context = DummyContext()
response = self._callFUT(context, request)
self.assertEqual(response.status, '404 Not Found')
- self.failUnless('halloo' in response.body)
+ self.assertEqual(response.app_iter, ['Not Found'])
def test_no_mapper(self):
request = self._makeRequest(PATH_INFO='/abc')
- context = Exception()
+ context = ExceptionResponse()
response = self._callFUT(context, request)
self.assertEqual(response.status, '404 Not Found')
- def test_custom_notfound_view(self):
- request = self._makeRequest(PATH_INFO='/abc')
- def notfound(exc, request):
- return 'abc'
- request.custom_notfound_view = notfound
- context = Exception()
- response = self._callFUT(context, request)
- self.assertEqual(response, 'abc')
-
def test_no_path(self):
request = self._makeRequest()
- context = Exception()
+ context = ExceptionResponse()
self._registerMapper(request.registry, True)
response = self._callFUT(context, request)
self.assertEqual(response.status, '404 Not Found')
def test_mapper_path_already_slash_ending(self):
request = self._makeRequest(PATH_INFO='/abc/')
- context = Exception()
+ context = ExceptionResponse()
self._registerMapper(request.registry, True)
response = self._callFUT(context, request)
self.assertEqual(response.status, '404 Not Found')
def test_matches(self):
request = self._makeRequest(PATH_INFO='/abc')
- context = Exception()
+ context = ExceptionResponse()
self._registerMapper(request.registry, True)
response = self._callFUT(context, request)
self.assertEqual(response.status, '302 Found')
self.assertEqual(response.location, '/abc/')
+class TestAppendSlashNotFoundViewFactory(BaseTest, unittest.TestCase):
+ def _makeOne(self, notfound_view):
+ from repoze.bfg.view import AppendSlashNotFoundViewFactory
+ return AppendSlashNotFoundViewFactory(notfound_view)
+
+ def test_custom_notfound_view(self):
+ request = self._makeRequest(PATH_INFO='/abc')
+ context = ExceptionResponse()
+ def custom_notfound(context, request):
+ return 'OK'
+ view = self._makeOne(custom_notfound)
+ response = view(context, request)
+ self.assertEqual(response, 'OK')
+
+class Test_default_exceptionresponse_view(unittest.TestCase):
+ def _callFUT(self, context, request):
+ from repoze.bfg.view import default_exceptionresponse_view
+ return default_exceptionresponse_view(context, request)
+
+ def test_is_exception(self):
+ context = Exception()
+ result = self._callFUT(context, None)
+ self.failUnless(result is context)
+
+ def test_is_not_exception_no_request_exception(self):
+ context = object()
+ request = DummyRequest()
+ result = self._callFUT(context, request)
+ self.failUnless(result is context)
+
+ def test_is_not_exception_request_exception(self):
+ context = object()
+ request = DummyRequest()
+ request.exception = 'abc'
+ result = self._callFUT(context, request)
+ self.assertEqual(result, 'abc')
+
+class ExceptionResponse(Exception):
+ status = '404 Not Found'
+ app_iter = ['Not Found']
+ headerlist = []
class DummyContext:
pass
@@ -469,6 +449,9 @@ def make_view(response):
return response
return view
+class DummyRequest:
+ pass
+
class DummyResponse:
status = '200 OK'
headerlist = ()
diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py
index 30076b775..1d1839530 100644
--- a/repoze/bfg/view.py
+++ b/repoze/bfg/view.py
@@ -1,4 +1,3 @@
-import cgi
import mimetypes
import os
@@ -12,7 +11,6 @@ import os
if hasattr(mimetypes, 'init'):
mimetypes.init()
-from webob import Response
from webob.exc import HTTPFound
import venusian
@@ -20,7 +18,6 @@ import venusian
from zope.deprecation import deprecated
from zope.interface import providedBy
-from repoze.bfg.interfaces import IResponseFactory
from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IViewClassifier
@@ -447,129 +444,103 @@ class bfg_view(object):
return wrapped
-def default_view(context, request, status):
+def default_exceptionresponse_view(context, request):
if not isinstance(context, Exception):
- # backwards compat for a default_view registered via
+ # backwards compat for an exception response view registered via
# config.set_notfound_view or config.set_forbidden_view
# instead of as a proper exception view
- context = getattr(request, 'exception', None)
- try:
- msg = cgi.escape('%s' % context.args[0])
- except Exception:
- msg = ''
- html = """
- <html>
- <title>%s</title>
- <body>
- <h1>%s</h1>
- <code>%s</code>
- </body>
- </html>
- """ % (status, status, msg)
- headers = [('Content-Length', str(len(html))),
- ('Content-Type', 'text/html')]
- try:
- registry = request.registry
- except AttributeError:
- registry = get_current_registry()
- response_factory = registry.queryUtility(IResponseFactory,
- default=Response)
- return response_factory(status = status,
- headerlist = headers,
- app_iter = [html])
-
-def default_forbidden_view(context, request):
- return default_view(context, request, '401 Unauthorized')
-
-def default_notfound_view(context, request):
- return default_view(context, request, '404 Not Found')
-
-def append_slash_notfound_view(context, request):
- """For behavior like Django's ``APPEND_SLASH=True``, use this view
- as the :term:`Not Found view` in your application.
-
- When this view is the Not Found view (indicating that no view was
- found), and any routes have been defined in the configuration of
- your application, if the value of the ``PATH_INFO`` WSGI
- environment variable does not already end in a slash, and if the
- value of ``PATH_INFO`` *plus* a slash matches any route's path, do
- an HTTP redirect to the slash-appended PATH_INFO. Note that this
- will *lose* ``POST`` data information (turning it into a GET), so
- you shouldn't rely on this to redirect POST requests.
-
- If you use :term:`ZCML`, add the following to your application's
- ``configure.zcml`` to use this view as the Not Found view::
+ context = getattr(request, 'exception', context)
+ return context
+
+class AppendSlashNotFoundViewFactory(object):
+ """ There can only be one :term:`Not Found view` in any
+ :mod:`repoze.bfg application. Even if you use
+ :func:`repoze.bfg.view.append_slash_notfound_view` as the Not
+ Found view, :mod:`repoze.bfg` still must generate a ``404 Not
+ Found`` response when it cannot redirect to a slash-appended URL;
+ this not found response will be visible to site users.
+
+ If you don't care what this 404 response looks like, and you only
+ need redirections to slash-appended route URLs, you may use the
+ :func:`repoze.bfg.view.append_slash_notfound_view` object as the
+ Not Found view. However, if you wish to use a *custom* notfound
+ view callable when a URL cannot be redirected to a slash-appended
+ URL, you may wish to use an instance of this class as the Not
+ Found view, supplying a :term:`view callable` to be used as the
+ custom notfound view as the first argument to its constructor.
+ For instance:
- <view
- context="repoze.bfg.exceptions.NotFound"
- view="repoze.bfg.view.append_slash_notfound_view"/>
+ .. code-block:: python
- Or use the
- :meth:`repoze.bfg.configuration.Configurator.add_view`
- method if you don't use ZCML::
+ from repoze.bfg.exceptions import NotFound
+ from repoze.bfg.view import AppendSlashNotFoundViewFactory
- from repoze.bfg.exceptions import NotFound
- from repoze.bfg.view import append_slash_notfound_view
- config.add_view(append_slash_notfound_view, context=NotFound)
+ def notfound_view(context, request):
+ return HTTPNotFound('It aint there, stop trying!')
- See also :ref:`changing_the_notfound_view`.
+ custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
+ config.add_view(custom_append_slash, context=NotFound)
- .. note:: This function is new as of :mod:`repoze.bfg` version 1.1.
+ The ``notfound_view`` supplied must adhere to the two-argument
+ view callable calling convention of ``(context, request)``
+ (``context`` will be the exception object).
- There can only be one Not Found view in any :mod:`repoze.bfg
- application. If you use ``append_slash_notfound_view`` as the Not
- Found view, it still must generate a NotFound response when it
- cannot redirect to a slash-appended URL; this not found response
- will be visible to site users.
+ .. note:: This class is new as of :mod:`repoze.bfg` version 1.3.
- If you wish to use a custom notfound view callable when
- ``append_slash_notfound_view`` does not redirect to a
- slash-appended URL, use a wrapper function as the
- :exc:`repoze.bfg.exceptions.NotFound` view; have this wrapper
- attach a :term:`view callable` which returns a response to the
- request object named ``custom_notfound_view`` before calling
- ``append_slash_notfound_view``. For example:
+ """
+ def __init__(self, notfound_view=None):
+ if notfound_view is None:
+ notfound_view = default_exceptionresponse_view
+ self.notfound_view = notfound_view
+
+ def __call__(self, context, request):
+ if not isinstance(context, Exception):
+ # backwards compat for an append_notslash_view registered via
+ # config.set_notfound_view instead of as a proper exception view
+ context = getattr(request, 'exception', None)
+ path = request.environ.get('PATH_INFO', '/')
+ registry = request.registry
+ mapper = registry.queryUtility(IRoutesMapper)
+ if mapper is not None and not path.endswith('/'):
+ slashpath = path + '/'
+ for route in mapper.get_routes():
+ if route.match(slashpath) is not None:
+ return HTTPFound(location=slashpath)
+ return self.notfound_view(context, request)
+
+append_slash_notfound_view = AppendSlashNotFoundViewFactory()
+append_slash_notfound_view.__doc__ = """\
+For behavior like Django's ``APPEND_SLASH=True``, use this view as the
+:term:`Not Found view` in your application.
+
+When this view is the Not Found view (indicating that no view was
+found), and any routes have been defined in the configuration of your
+application, if the value of the ``PATH_INFO`` WSGI environment
+variable does not already end in a slash, and if the value of
+``PATH_INFO`` *plus* a slash matches any route's path, do an HTTP
+redirect to the slash-appended PATH_INFO. Note that this will *lose*
+``POST`` data information (turning it into a GET), so you shouldn't
+rely on this to redirect POST requests.
+
+If you use :term:`ZCML`, add the following to your application's
+``configure.zcml`` to use this view as the Not Found view::
- .. code-block:: python
+ <view
+ context="repoze.bfg.exceptions.NotFound"
+ view="repoze.bfg.view.append_slash_notfound_view"/>
- from webob.exc import HTTPNotFound
- from repoze.bfg.exceptions import NotFound
- from repoze.bfg.view import append_slash_notfound_view
+Or use the
+:meth:`repoze.bfg.configuration.Configurator.add_view`
+method if you don't use ZCML::
- def notfound_view(exc, request):
- def fallback_notfound_view(exc, request):
- return HTTPNotFound('It aint there, stop trying!')
- request.fallback_notfound_view = fallback_notfound_view
- return append_slash_notfound_view(exc, request)
+ from repoze.bfg.exceptions import NotFound
+ from repoze.bfg.view import append_slash_notfound_view
+ config.add_view(append_slash_notfound_view, context=NotFound)
- config.add_view(notfound_view, context=NotFound)
+See also :ref:`changing_the_notfound_view`.
- ``custom_notfound_view`` must adhere to the two-argument view
- callable calling convention of ``(context, request)`` (``context``
- will be the exception object).
+.. note:: This function is new as of :mod:`repoze.bfg` version 1.1.
+"""
- If ``custom_notfound_view`` is not found on the request object, a
- default notfound response will be generated when the
- ``append_slash_notfound_view`` doesn't redirect to a
- slash-appended URL.
- .. note:: The checking for ``request.custom_notfound_view`` by
- ``append_slash_notfound_view`` is new as of :mod:`repoze.bfg`
- version 1.3.
- """
- if not isinstance(context, Exception):
- # backwards compat for an append_notslash_view registered via
- # config.set_notfound_view instead of as a proper exception view
- context = getattr(request, 'exception', None)
- path = request.environ.get('PATH_INFO', '/')
- registry = request.registry
- mapper = registry.queryUtility(IRoutesMapper)
- if mapper is not None and not path.endswith('/'):
- slashpath = path + '/'
- for route in mapper.get_routes():
- if route.match(slashpath) is not None:
- return HTTPFound(location=slashpath)
- notfound_view = getattr(request, 'custom_notfound_view',
- default_notfound_view)
- return notfound_view(context, request)