summaryrefslogtreecommitdiff
path: root/repoze
diff options
context:
space:
mode:
Diffstat (limited to 'repoze')
-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
7 files changed, 309 insertions, 212 deletions
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)