Chris McDonough
2012-02-22 0db4a157083d51251b4d3f574a1699fc76359c9d
- New API: ``pyramid.config.Configurator.add_notfound_view``.  This is a
wrapper for ``pyramid.Config.configurator.add_view`` which provides easy
append_slash support. It should be preferred over calling ``add_view``
directly with ``context=HTTPNotFound`` as was previously recommended.

- New API: ``pyramid.view.notfound_view_config``. This is a decorator
constructor like ``pyramid.view.view_config`` that calls
``pyramid.config.Configurator.add_notfound_view`` when scanned. It should
be preferred over using ``pyramid.view.view_config`` with
``context=HTTPNotFound`` as was previously recommended.

- The older deprecated ``set_notfound_view`` Configurator method is now an
alias for the new ``add_notfound_view`` Configurator method. This has the
following impact: the ``context`` sent to views with a ``(context,
request)`` call signature registered via the deprecated
``add_notfound_view``/``set_notfound_view`` will now be the HTTPNotFound
exception object instead of the actual resource context found. Use
``request.context`` to get the actual resource context. It's also
recommended to disuse ``set_notfound_view`` in favor of
``add_notfound_view``, despite the aliasing.

- The API documentation for ``pyramid.view.append_slash_notfound_view`` and
``pyramid.view.AppendSlashNotFoundViewFactory`` was removed. These names
still exist and are still importable, but they are no longer APIs. Use
``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or
``pyramid.view.notfound_view_config(append_slash=True)`` to get the same
behavior.

- The ``set_forbidden_view`` method of the Configurator was removed from the
documentation. It has been deprecated since Pyramid 1.1.

- The AppendSlashNotFoundViewFactory used request.path to match routes. This
was wrong because request.path contains the script name, and this would
cause it to fail in circumstances where the script name was not empty. It
should have used request.path_info, and now does.

- Updated the "Registering a Not Found View" section of the "Hooks" chapter,
replacing explanations of registering a view using ``add_view`` or
``view_config`` with ones using ``add_notfound_view`` or
``notfound_view_config``.

- Updated the "Redirecting to Slash-Appended Routes" section of the "URL
Dispatch" chapter, replacing explanations of registering a view using
``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or
``notfound_view_config``
1 files added
13 files modified
613 ■■■■ changed files
CHANGES.txt 52 ●●●●● patch | view | raw | blame | history
TODO.txt 10 ●●●● patch | view | raw | blame | history
docs/api/config.rst 3 ●●●● patch | view | raw | blame | history
docs/api/view.rst 6 ●●●● patch | view | raw | blame | history
docs/narr/hooks.rst 81 ●●●● patch | view | raw | blame | history
docs/narr/renderers.rst 2 ●●● patch | view | raw | blame | history
docs/narr/urldispatch.rst 145 ●●●● patch | view | raw | blame | history
pyramid/config/util.py 3 ●●●● patch | view | raw | blame | history
pyramid/config/views.py 97 ●●●●● patch | view | raw | blame | history
pyramid/tests/pkgs/notfoundview/__init__.py 30 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_views.py 28 ●●●● patch | view | raw | blame | history
pyramid/tests/test_integration.py 17 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_view.py 48 ●●●●● patch | view | raw | blame | history
pyramid/view.py 91 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -13,6 +13,17 @@
  requirement that the debug toolbar's own views and methods not show up in
  the introspector.
- New API: ``pyramid.config.Configurator.add_notfound_view``.  This is a
  wrapper for ``pyramid.Config.configurator.add_view`` which provides easy
  append_slash support.  It should be preferred over calling ``add_view``
  directly with ``context=HTTPNotFound`` as was previously recommended.
- New API: ``pyramid.view.notfound_view_config``.  This is a decorator
  constructor like ``pyramid.view.view_config`` that calls
  ``pyramid.config.Configurator.add_notfound_view`` when scanned.  It should
  be preferred over using ``pyramid.view.view_config`` with
  ``context=HTTPNotFound`` as was previously recommended.
Backwards Incompatibilities
---------------------------
@@ -28,12 +39,53 @@
- The ``pyramid.registry.noop_introspector`` API object has been removed.
- The older deprecated ``set_notfound_view`` Configurator method is now an
  alias for the new ``add_notfound_view`` Configurator method.  This has the
  following impact: the ``context`` sent to views with a ``(context,
  request)`` call signature registered via the deprecated
  ``add_notfound_view``/``set_notfound_view`` will now be the HTTPNotFound
  exception object instead of the actual resource context found.  Use
  ``request.context`` to get the actual resource context.  It's also
  recommended to disuse ``set_notfound_view`` in favor of
  ``add_notfound_view``, despite the aliasing.
Deprecations
------------
- The API documentation for ``pyramid.view.append_slash_notfound_view`` and
  ``pyramid.view.AppendSlashNotFoundViewFactory`` was removed.  These names
  still exist and are still importable, but they are no longer APIs.  Use
  ``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or
  ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same
  behavior.
- The ``set_forbidden_view`` method of the Configurator was removed from the
  documentation.  It has been deprecated since Pyramid 1.1.
Bug Fixes
---------
- The static file response object used by ``config.add_static_view`` opened
  the static file twice, when it only needed to open it once.
- The AppendSlashNotFoundViewFactory used request.path to match routes.  This
  was wrong because request.path contains the script name, and this would
  cause it to fail in circumstances where the script name was not empty.  It
  should have used request.path_info, and now does.
Documentation
-------------
- Updated the "Registering a Not Found View" section of the "Hooks" chapter,
  replacing explanations of registering a view using ``add_view`` or
  ``view_config`` with ones using ``add_notfound_view`` or
  ``notfound_view_config``.
- Updated the "Redirecting to Slash-Appended Routes" section of the "URL
  Dispatch" chapter, replacing explanations of registering a view using
  ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or
  ``notfound_view_config``
1.3a8 (2012-02-19)
==================
TODO.txt
@@ -1,8 +1,16 @@
Pyramid TODOs
=============
Must-Have
---------
- Fix scaffolds and tutorials to use notfound_view_config rather than
  view_config.
Nice-to-Have
------------
- Add forbidden_view_config?
- Add docs about upgrading between Pyramid versions (e.g. how to see
  deprecation warnings).
@@ -25,8 +33,6 @@
    with config.partial(introspection=False) as c:
        c.add_view(..)
- Decorator for append_slash_notfound_view_factory.
- Introspection:
docs/api/config.rst
@@ -24,8 +24,7 @@
    .. automethod:: add_route
    .. automethod:: add_static_view(name, path, cache_max_age=3600, permission=NO_PERMISSION_REQUIRED)
    .. automethod:: add_view
    .. automethod:: set_forbidden_view
    .. automethod:: set_notfound_view
    .. automethod:: add_notfound_view
  :methodcategory:`Adding an Event Subscriber`
docs/api/view.rst
@@ -19,11 +19,11 @@
  .. autoclass:: view_defaults
     :members:
  .. autoclass:: notfound_view_config
     :members:
  .. autoclass:: static
     :members:
     :inherited-members:
  .. autofunction:: append_slash_notfound_view(context, request)
  .. autoclass:: AppendSlashNotFoundViewFactory
docs/narr/hooks.rst
@@ -19,24 +19,66 @@
exists.  The default not found view can be overridden through application
configuration.
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:`pyramid.httpexceptions.HTTPNotFound` class as the
``context`` of the view configuration.
If your application uses :term:`imperative configuration`, you can replace
the Not Found view by using the :meth:`pyramid.config.Configurator.add_view`
method to register an "exception view":
the Not Found view by using the
:meth:`pyramid.config.Configurator.add_notfound_view` method:
.. code-block:: python
   :linenos:
   from pyramid.httpexceptions import HTTPNotFound
   from helloworld.views import notfound_view
   config.add_view(notfound_view, context=HTTPNotFound)
   from helloworld.views import notfound
   config.add_notfound_view(notfound)
Replace ``helloworld.views.notfound_view`` with a reference to the
:term:`view callable` you want to use to represent the Not Found view.
Replace ``helloworld.views.notfound`` with a reference to the :term:`view
callable` you want to use to represent the Not Found view.  The :term:`not
found view` callable is a view callable like any other.
If your application instead uses :class:`pyramid.view.view_config` decorators
and a :term:`scan`, you can replace the Not Found view by using the
:class:`pyramid.view.notfound_view_config` decorator:
.. code-block:: python
   :linenos:
   from pyramid.view import notfound_view_config
   notfound_view_config()
   def notfound(request):
       return Response('Not Found, dude', status='404 Not Found')
   def main(globals, **settings):
      config = Configurator()
      config.scan()
This does exactly what the imperative example above showed.
Your application can define *multiple* not found views if necessary.  Both
:meth:`pyramid.config.Configurator.add_notfound_view` and
:class:`pyramid.view.notfound_view_config` take most of the same arguments as
:class:`pyramid.config.Configurator.add_view` and
:class:`pyramid.view.view_config`, respectively.  This means that not found
views can carry predicates limiting their applicability.  For example:
.. code-block:: python
   :linenos:
   from pyramid.view import notfound_view_config
   notfound_view_config(request_method='GET')
   def notfound_get(request):
       return Response('Not Found during GET, dude', status='404 Not Found')
   notfound_view_config(request_method='POST')
   def notfound_post(request):
       return Response('Not Found during POST, dude', status='404 Not Found')
   def main(globals, **settings):
      config = Configurator()
      config.scan()
The ``notfound_get`` view will be called when a view could not be found and
the request method was ``GET``.  The ``notfound_post`` view will be called
when a view could not be found and the request method was ``POST``.
Like any other view, the notfound view must accept at least a ``request``
parameter, or both ``context`` and ``request``.  The ``request`` is the
@@ -45,6 +87,11 @@
:exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the view to
be called.
Both :meth:`pyramid.config.Configurator.add_notfound_view` and
:class:`pyramid.view.notfound_view_config` can be used to automatically
redirect requests to slash-appended routes. See
:ref:`redirecting_to_slash_appended_routes` for examples.
Here's some sample code that implements a minimal NotFound view callable:
.. code-block:: python
@@ -52,7 +99,7 @@
   from pyramid.httpexceptions import HTTPNotFound
   def notfound_view(request):
   def notfound(request):
       return HTTPNotFound()
.. note::
@@ -66,6 +113,14 @@
   ``pyramid.debug_notfound`` environment setting is true than it is when it
   is false.
.. note::
   Both :meth:`pyramid.config.Configurator.add_notfound_view` and
   :class:`pyramid.view.notfound_view_config` are new as of Pyramid 1.3.
   Older Pyramid documentation instructed users to use ``add_view`` instead,
   with a ``context`` of ``HTTPNotFound``.  This still works; the convenience
   method and decorator are just wrappers around this functionality.
.. warning::
   When a NotFound view callable accepts an argument list as
docs/narr/renderers.rst
@@ -103,7 +103,7 @@
.. code-block:: python
   :linenos:
   from pyramid.httpexceptions import HTTPNotFound
   from pyramid.httpexceptions import HTTPFound
   from pyramid.view import view_config
   @view_config(renderer='json')
docs/narr/urldispatch.rst
@@ -772,40 +772,92 @@
Redirecting to Slash-Appended Routes
------------------------------------
For behavior like Django's ``APPEND_SLASH=True``, use the
:func:`~pyramid.view.append_slash_notfound_view` view as the :term:`Not Found
view` in your application.  Defining this view as the :term:`Not Found view`
is a way to automatically redirect requests where the URL lacks a trailing
slash, but requires one to match the proper route.  When configured, along
with at least one other route in your application, this view will be invoked
if the value of ``PATH_INFO`` does not already end in a slash, and if the
value of ``PATH_INFO`` *plus* a slash matches any route's pattern.  In this
case it does an HTTP redirect to the slash-appended ``PATH_INFO``.
For behavior like Django's ``APPEND_SLASH=True``, use the ``append_slash``
argument to :meth:`pyramid.config.Configurator.add_notfound_view` or the
equivalent ``append_slash`` argument to the
:class:`pyramid.view.notfound_view_config` decorator.
Let's use an example, because this behavior is a bit magical. If the
``append_slash_notfound_view`` is configured in your application and your
route configuration looks like so:
Adding ``append_slash=True`` is a way to automatically redirect requests
where the URL lacks a trailing slash, but requires one to match the proper
route.  When configured, along with at least one other route in your
application, this view will be invoked if the value of ``PATH_INFO`` does not
already end in a slash, and if the value of ``PATH_INFO`` *plus* a slash
matches any route's pattern.  In this case it does an HTTP redirect to the
slash-appended ``PATH_INFO``.
To configure the slash-appending not found view in your application, change
the application's startup configuration, adding the following stanza:
.. code-block:: python
   :linenos:
   config.add_route('noslash', 'no_slash')
   config.add_route('hasslash', 'has_slash/')
Let's use an example.  If the following routes are configured in your
application:
   config.add_view('myproject.views.no_slash', route_name='noslash')
   config.add_view('myproject.views.has_slash', route_name='hasslash')
.. code-block:: python
   :linenos:
   from pyramid.httpexceptions import HTTPNotFound
   def notfound(request):
       return HTTPNotFound('Not found, bro.')
   def no_slash(request):
       return Response('No slash')
   def has_slash(request):
       return Response('Has slash')
   def main(g, **settings):
       config = Configurator()
       config.add_route('noslash', 'no_slash')
       config.add_route('hasslash', 'has_slash/')
       config.add_view(no_slash, route_name='noslash')
       config.add_view(has_slash, route_name='hasslash')
       config.add_notfound_view(notfound, append_slash=True)
If a request enters the application with the ``PATH_INFO`` value of
``/no_slash``, the first route will match and the browser will show "No
slash".  However, if a request enters the application with the ``PATH_INFO``
value of ``/no_slash/``, *no* route will match, and the slash-appending not
found view will not find a matching route with an appended slash.  As a
result, the ``notfound`` view will be called and it will return a "Not found,
bro." body.
If a request enters the application with the ``PATH_INFO`` value of
``/has_slash/``, the second route will match.  If a request enters the
application with the ``PATH_INFO`` value of ``/has_slash``, a route *will* be
found by the slash-appending not found view.  An HTTP redirect to
``/has_slash/`` will be returned to the user's browser.
``/has_slash/`` will be returned to the user's browser.  As a result, the
``notfound`` view will never actually be called.
If a request enters the application with the ``PATH_INFO`` value of
``/no_slash``, the first route will match.  However, if a request enters the
application with the ``PATH_INFO`` value of ``/no_slash/``, *no* route will
match, and the slash-appending not found view will *not* find a matching
route with an appended slash.
The following application uses the :class:`pyramid.view.notfound_view_config`
and :class:`pyramid.view.view_config` decorators and a :term:`scan` to do
exactly the same job:
.. code-block:: python
   :linenos:
   from pyramid.httpexceptions import HTTPNotFound
   from pyramid.view import notfound_view_config, view_config
   @notfound_view_config(append_slash=True)
   def notfound(request):
       return HTTPNotFound('Not found, bro.')
   @view_config(route_name='noslash')
   def no_slash(request):
       return Response('No slash')
   @view_config(route_name='hasslash')
   def has_slash(request):
       return Response('Has slash')
   def main(g, **settings):
       config = Configurator()
       config.add_route('noslash', 'no_slash')
       config.add_route('hasslash', 'has_slash/')
       config.scan()
.. warning::
@@ -814,53 +866,8 @@
   request into a ``GET``, losing any ``POST`` data in the original
   request.
To configure the slash-appending not found view in your application, change
the application's startup configuration, adding the following stanza:
.. code-block:: python
   :linenos:
   config.add_view('pyramid.view.append_slash_notfound_view',
                   context='pyramid.httpexceptions.HTTPNotFound')
See :ref:`view_module` and :ref:`changing_the_notfound_view` for more
information about the slash-appending not found view and for a more general
description of how to configure a not found view.
Custom Not Found View With Slash Appended Routes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There can only be one :term:`Not Found view` in any :app:`Pyramid`
application.  Even if you use :func:`~pyramid.view.append_slash_notfound_view`
as the Not Found view, :app:`Pyramid` 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:`~pyramid.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:`~pyramid.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
     :linenos:
     from pyramid.httpexceptions import HTTPNotFound
     from pyramid.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=HTTPNotFound)
The ``notfound_view`` supplied must adhere to the two-argument view callable
calling convention of ``(context, request)`` (``context`` will be the
exception object).
See :ref:`view_module` and :ref:`changing_the_notfound_view` for for a more
general description of how to configure a view and/or a not found view.
.. index::
   pair: debugging; route matching
pyramid/config/util.py
@@ -204,7 +204,8 @@
    if containment is not None:
        def containment_predicate(context, request):
            return find_interface(context, containment) is not None
            ctx = getattr(request, 'context', context)
            return find_interface(ctx, containment) is not None
        containment_predicate.__text__ = "containment = %s" % containment
        weights.append(1 << 7)
        predicates.append(containment_predicate)
pyramid/config/views.py
@@ -54,7 +54,12 @@
from pyramid.security import NO_PERMISSION_REQUIRED
from pyramid.static import static_view
from pyramid.threadlocal import get_current_registry
from pyramid.view import render_view_to_response
from pyramid.view import (
    render_view_to_response,
    AppendSlashNotFoundViewFactory,
    )
from pyramid.util import object_description
from pyramid.config.util import (
@@ -1353,10 +1358,6 @@
        The ``wrapper`` argument should be the name of another view
        which will wrap this view when rendered (see the ``add_view``
        method's ``wrapper`` argument for a description)."""
        if isinstance(renderer, string_types):
            renderer = renderers.RendererHelper(
                name=renderer, package=self.package,
                registry = self.registry)
        view = self._derive_view(view, attr=attr, renderer=renderer)
        def bwcompat_view(context, request):
            context = getattr(request, 'context', None)
@@ -1365,46 +1366,66 @@
                             wrapper=wrapper, renderer=renderer)
    @action_method
    def set_notfound_view(self, view=None, attr=None, renderer=None,
                          wrapper=None):
        """ Add a default not found view to the current configuration
        state.
    def add_notfound_view(
            self, view=None, attr=None, renderer=None,  wrapper=None,
            route_name=None, request_type=None, request_method=None,
            request_param=None, containment=None, xhr=None, accept=None,
            header=None, path_info=None,  custom_predicates=(), decorator=None,
            mapper=None, match_param=None, append_slash=False):
        """ Add a default notfound view to the current configuration state.
        The view will be called when a view cannot otherwise be found for the
        set of circumstances implied by the predicates provided.  The
        simplest example is:
        .. warning::
          .. code-block:: python
           This method has been deprecated in :app:`Pyramid` 1.0.  *Do not use
           it for new development; it should only be used to support older code
           bases which depend upon it.* See :ref:`changing_the_notfound_view` to
           see how a not found view should be registered in new projects.
           config.add_notfound_view(someview)
        The ``view`` argument should be a :term:`view callable` or a
        :term:`dotted Python name` which refers to a view callable.
        All arguments except ``append_slash`` have the same meaning as
        :meth:`pyramid.config.Configurator.add_view` and each predicate
        argument restricts the set of circumstances under which this notfound
        view will be invoked.
        The ``attr`` argument should be the attribute of the view
        callable used to retrieve the response (see the ``add_view``
        method's ``attr`` argument for a description).
        If ``append_slash`` is ``True``, when this notfound view is invoked,
        and the current path info does not end in a slash, the notfound logic
        will attempt to find a :term:`route` that matches the request's path
        info suffixed with a slash.  If such a route exists, Pyramid will
        issue a redirect to the URL implied by the route; if it does not,
        Pyramid will return the result of the view callable provided as
        ``view``, as normal.
        The ``renderer`` argument should be the name of (or path to) a
        :term:`renderer` used to generate a response for this view
        (see the
        :meth:`pyramid.config.Configurator.add_view`
        method's ``renderer`` argument for information about how a
        configurator relates to a renderer).
        .. note::
        The ``wrapper`` argument should be the name of another view
        which will wrap this view when rendered (see the ``add_view``
        method's ``wrapper`` argument for a description).
           This method is new as of Pyramid 1.3.
        """
        if isinstance(renderer, string_types):
            renderer = renderers.RendererHelper(
                name=renderer, package=self.package,
                registry=self.registry)
        view = self._derive_view(view, attr=attr, renderer=renderer)
        def bwcompat_view(context, request):
            context = getattr(request, 'context', None)
            return view(context, request)
        return self.add_view(bwcompat_view, context=HTTPNotFound,
                             wrapper=wrapper, renderer=renderer)
        settings = dict(
            view=view,
            context=HTTPNotFound,
            wrapper=wrapper,
            request_type=request_type,
            request_method=request_method,
            request_param=request_param,
            containment=containment,
            xhr=xhr,
            accept=accept,
            header=header,
            path_info=path_info,
            custom_predicates=custom_predicates,
            decorator=decorator,
            mapper=mapper,
            match_param=match_param,
            route_name=route_name
            )
        if append_slash:
            view = self._derive_view(view, attr=attr, renderer=renderer)
            view = AppendSlashNotFoundViewFactory(view)
            settings['view'] = view
        else:
            settings['attr'] = attr
            settings['renderer'] = renderer
        return self.add_view(**settings)
    set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias
    @action_method
    def set_view_mapper(self, mapper):
pyramid/tests/pkgs/notfoundview/__init__.py
New file
@@ -0,0 +1,30 @@
from pyramid.view import notfound_view_config, view_config
from pyramid.response import Response
@notfound_view_config(route_name='foo', append_slash=True)
def foo_notfound(request): # pragma: no cover
    return Response('foo_notfound')
@notfound_view_config(route_name='baz')
def baz_notfound(request):
    return Response('baz_notfound')
@notfound_view_config(append_slash=True)
def notfound(request):
    return Response('generic_notfound')
@view_config(route_name='bar')
def bar(request):
    return Response('OK bar')
@view_config(route_name='foo2')
def foo2(request):
    return Response('OK foo2')
def includeme(config):
    config.add_route('foo', '/foo')
    config.add_route('foo2', '/foo/')
    config.add_route('bar', '/bar/')
    config.add_route('baz', '/baz')
    config.scan('pyramid.tests.pkgs.notfoundview')
pyramid/tests/test_config/test_views.py
@@ -1682,14 +1682,14 @@
    def test_set_notfound_view(self):
    def test_add_notfound_view(self):
        from pyramid.renderers import null_renderer
        from zope.interface import implementedBy
        from pyramid.interfaces import IRequest
        from pyramid.httpexceptions import HTTPNotFound
        config = self._makeOne(autocommit=True)
        view = lambda *arg: arg
        config.set_notfound_view(view, renderer=null_renderer)
        config.add_notfound_view(view, renderer=null_renderer)
        request = self._makeRequest(config)
        view = self._getViewCallable(config,
                                     ctx_iface=implementedBy(HTTPNotFound),
@@ -1697,30 +1697,33 @@
        result = view(None, request)
        self.assertEqual(result, (None, request))
    def test_set_notfound_view_request_has_context(self):
    def test_add_notfound_view_append_slash(self):
        from pyramid.response import Response
        from pyramid.renderers import null_renderer
        from zope.interface import implementedBy
        from pyramid.interfaces import IRequest
        from pyramid.httpexceptions import HTTPNotFound
        config = self._makeOne(autocommit=True)
        view = lambda *arg: arg
        config.set_notfound_view(view, renderer=null_renderer)
        config.add_route('foo', '/foo/')
        def view(request): return Response('OK')
        config.add_notfound_view(view, renderer=null_renderer,append_slash=True)
        request = self._makeRequest(config)
        request.context = 'abc'
        request.environ['PATH_INFO'] = '/foo'
        request.query_string = 'a=1&b=2'
        request.path = '/scriptname/foo'
        view = self._getViewCallable(config,
                                     ctx_iface=implementedBy(HTTPNotFound),
                                     request_iface=IRequest)
        result = view(None, request)
        self.assertEqual(result, ('abc', request))
    @testing.skip_on('java')
    def test_set_notfound_view_with_renderer(self):
        self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2')
    def test_add_notfound_view_with_renderer(self):
        from zope.interface import implementedBy
        from pyramid.interfaces import IRequest
        from pyramid.httpexceptions import HTTPNotFound
        config = self._makeOne(autocommit=True)
        view = lambda *arg: {}
        config.set_notfound_view(
        config.add_notfound_view(
            view,
            renderer='pyramid.tests.test_config:files/minimal.pt')
        config.begin()
@@ -1734,8 +1737,7 @@
            config.end()
        self.assertTrue(b'div' in result.body)
    @testing.skip_on('java')
    def test_set_forbidden_view_with_renderer(self):
    def test_add_forbidden_view_with_renderer(self):
        from zope.interface import implementedBy
        from pyramid.interfaces import IRequest
        from pyramid.httpexceptions import HTTPForbidden
pyramid/tests/test_integration.py
@@ -357,6 +357,23 @@
        res = self.testapp.get('/second', status=200)
        self.assertTrue(b'OK2' in res.body)
class TestNotFoundView(IntegrationBase, unittest.TestCase):
    package = 'pyramid.tests.pkgs.notfoundview'
    def test_it(self):
        res = self.testapp.get('/wontbefound', status=200)
        self.assertTrue(b'generic_notfound' in res.body)
        res = self.testapp.get('/bar', status=302)
        self.assertEqual(res.location, 'http://localhost/bar/')
        res = self.testapp.get('/bar/', status=200)
        self.assertTrue(b'OK bar' in res.body)
        res = self.testapp.get('/foo', status=302)
        self.assertEqual(res.location, 'http://localhost/foo/')
        res = self.testapp.get('/foo/', status=200)
        self.assertTrue(b'OK foo2' in res.body)
        res = self.testapp.get('/baz', status=200)
        self.assertTrue(b'baz_notfound' in res.body)
class TestViewPermissionBug(IntegrationBase, unittest.TestCase):
    # view_execution_permitted bug as reported by Shane at http://lists.repoze.org/pipermail/repoze-dev/2010-October/003603.html
    package = 'pyramid.tests.pkgs.permbugapp'
pyramid/tests/test_view.py
@@ -48,7 +48,51 @@
        context = DummyContext()
        directlyProvides(context, IContext)
        return context
class Test_notfound_view_config(BaseTest, unittest.TestCase):
    def _makeOne(self, **kw):
        from pyramid.view import notfound_view_config
        return notfound_view_config(**kw)
    def test_ctor(self):
        inst = self._makeOne(attr='attr', path_info='path_info',
                             append_slash=True)
        self.assertEqual(inst.__dict__,
                         {'attr':'attr', 'path_info':'path_info',
                          'append_slash':True})
    def test_it_function(self):
        def view(request): pass
        decorator = self._makeOne(attr='attr', renderer='renderer',
                                  append_slash=True)
        venusian = DummyVenusian()
        decorator.venusian = venusian
        wrapped = decorator(view)
        self.assertTrue(wrapped is view)
        config = call_venusian(venusian)
        settings = config.settings
        self.assertEqual(
            settings,
            [{'attr': 'attr', 'venusian': venusian, 'append_slash': True,
              'renderer': 'renderer', '_info': 'codeinfo', 'view': None}]
            )
    def test_it_class(self):
        decorator = self._makeOne()
        venusian = DummyVenusian()
        decorator.venusian = venusian
        decorator.venusian.info.scope = 'class'
        class view(object): pass
        wrapped = decorator(view)
        self.assertTrue(wrapped is view)
        config = call_venusian(venusian)
        settings = config.settings
        self.assertEqual(len(settings), 1)
        self.assertEqual(len(settings[0]), 5)
        self.assertEqual(settings[0]['venusian'], venusian)
        self.assertEqual(settings[0]['view'], None) # comes from call_venusian
        self.assertEqual(settings[0]['attr'], 'view')
        self.assertEqual(settings[0]['_info'], 'codeinfo')
class RenderViewToResponseTests(BaseTest, unittest.TestCase):
    def _callFUT(self, *arg, **kw):
@@ -672,6 +716,8 @@
    def add_view(self, **kw):
        self.settings.append(kw)
    add_notfound_view = add_view
    def with_package(self, pkg):
        self.pkg = pkg
        return self
pyramid/view.py
@@ -9,11 +9,16 @@
    IViewClassifier,
    )
from pyramid.compat import map_
from pyramid.compat import (
    map_,
    decode_path_info,
    )
from pyramid.httpexceptions import (
    HTTPFound,
    default_exceptionresponse_view,
    )
from pyramid.path import caller_package
from pyramid.static import static_view
from pyramid.threadlocal import get_current_registry
@@ -274,11 +279,7 @@
        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) or context
        path = request.path
        path = decode_path_info(request.environ['PATH_INFO'] or '/')
        registry = request.registry
        mapper = registry.queryUtility(IRoutesMapper)
        if mapper is not None and not path.endswith('/'):
@@ -287,8 +288,8 @@
                if route.match(slashpath) is not None:
                    qs = request.query_string
                    if qs:
                        slashpath += '?' + qs
                    return HTTPFound(location=slashpath)
                        qs = '?' + qs
                    return HTTPFound(location=request.path+'/'+qs)
        return self.notfound_view(context, request)
append_slash_notfound_view = AppendSlashNotFoundViewFactory()
@@ -316,6 +317,80 @@
"""
class notfound_view_config(object):
    """
    An analogue of :class:`pyramid.view.view_config` which registers a
    :term:`not found view`.
    The notfound_view_config constructor accepts most of the same arguments
    as the constructor of :class:`pyramid.view.view_config`.  It can be used
    in the same places, and behaves in largely the same way, except it always
    registers a not found exception view instead of a "normal" view.
    Example:
    .. code-block:: python
        from pyramid.view import notfound_view_config
        from pyramid.response import Response
        notfound_view_config()
        def notfound(request):
            return Response('Not found, dude!', status='404 Not Found')
    All arguments except ``append_slash`` have the same meaning as
    :meth:`pyramid.view.view_config` and each predicate
    argument restricts the set of circumstances under which this notfound
    view will be invoked.
    If ``append_slash`` is ``True``, when the notfound view is invoked, and
    the current path info does not end in a slash, the notfound logic will
    attempt to find a :term:`route` that matches the request's path info
    suffixed with a slash.  If such a route exists, Pyramid will issue a
    redirect to the URL implied by the route; if it does not, Pyramid will
    return the result of the view callable provided as ``view``, as normal.
    See :ref:`changing_the_notfound_view` for detailed usage information.
    .. note::
       This class is new as of Pyramid 1.3.
    """
    venusian = venusian
    def __init__(self, request_type=default, request_method=default,
                 route_name=default, request_param=default, attr=default,
                 renderer=default, containment=default, wrapper=default,
                 xhr=default, accept=default, header=default,
                 path_info=default,  custom_predicates=default,
                 decorator=default, mapper=default, match_param=default,
                 append_slash=False):
        L = locals()
        for k, v in L.items():
            if k not in ('self', 'L') and v is not default:
                self.__dict__[k] = v
    def __call__(self, wrapped):
        settings = self.__dict__.copy()
        def callback(context, name, ob):
            config = context.config.with_package(info.module)
            config.add_notfound_view(view=ob, **settings)
        info = self.venusian.attach(wrapped, callback, category='pyramid')
        if info.scope == 'class':
            # if the decorator was attached to a method in a class, or
            # otherwise executed at class scope, we need to set an
            # 'attr' into the settings if one isn't already in there
            if settings.get('attr') is None:
                settings['attr'] = wrapped.__name__
        settings['_info'] = info.codeinfo # fbo "action_method"
        return wrapped
def is_response(ob):
    """ Return ``True`` if ``ob`` implements the interface implied by
    :ref:`the_response`. ``False`` if not.