summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-02-22 15:37:50 -0500
committerChris McDonough <chrism@plope.com>2012-02-22 15:37:50 -0500
commit0db4a157083d51251b4d3f574a1699fc76359c9d (patch)
tree9edb3e9a9c23ab4e2d7c4b8c31484bca357f224d
parent3f7681efc96f815008abc30e152cd906851b00b0 (diff)
downloadpyramid-0db4a157083d51251b4d3f574a1699fc76359c9d.tar.gz
pyramid-0db4a157083d51251b4d3f574a1699fc76359c9d.tar.bz2
pyramid-0db4a157083d51251b4d3f574a1699fc76359c9d.zip
- 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``
-rw-r--r--CHANGES.txt52
-rw-r--r--TODO.txt10
-rw-r--r--docs/api/config.rst3
-rw-r--r--docs/api/view.rst6
-rw-r--r--docs/narr/hooks.rst81
-rw-r--r--docs/narr/renderers.rst2
-rw-r--r--docs/narr/urldispatch.rst141
-rw-r--r--pyramid/config/util.py3
-rw-r--r--pyramid/config/views.py105
-rw-r--r--pyramid/tests/pkgs/notfoundview/__init__.py30
-rw-r--r--pyramid/tests/test_config/test_views.py28
-rw-r--r--pyramid/tests/test_integration.py17
-rw-r--r--pyramid/tests/test_view.py48
-rw-r--r--pyramid/view.py91
14 files changed, 464 insertions, 153 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 8595e726e..efeba0447 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -13,6 +13,17 @@ Features
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 @@ Backwards Incompatibilities
- 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)
==================
diff --git a/TODO.txt b/TODO.txt
index 0143d275e..20a677bea 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,9 +1,17 @@
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).
@@ -26,8 +34,6 @@ Nice-to-Have
with config.partial(introspection=False) as c:
c.add_view(..)
-- Decorator for append_slash_notfound_view_factory.
-
- Introspection:
* ``default root factory`` category (prevent folks from needing to searh
diff --git a/docs/api/config.rst b/docs/api/config.rst
index 6b4ed7b1b..bf5fdbb7c 100644
--- a/docs/api/config.rst
+++ b/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`
diff --git a/docs/api/view.rst b/docs/api/view.rst
index 9f59ddae7..cb269e48e 100644
--- a/docs/api/view.rst
+++ b/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
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index eaccc14a3..cbc40ceee 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -19,24 +19,66 @@ found view`, which is a :term:`view callable`. A default notfound view
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`` 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.
-Replace ``helloworld.views.notfound_view`` with a reference to the
-:term:`view callable` you want to use to represent the Not Found view.
+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 @@ used in the call signature) will be the instance of the
: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 @@ Here's some sample code that implements a minimal NotFound view callable:
from pyramid.httpexceptions import HTTPNotFound
- def notfound_view(request):
+ def notfound(request):
return HTTPNotFound()
.. note::
@@ -66,6 +113,14 @@ Here's some sample code that implements a minimal NotFound view callable:
``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
diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst
index 1f1b1943b..76035cbdf 100644
--- a/docs/narr/renderers.rst
+++ b/docs/narr/renderers.rst
@@ -103,7 +103,7 @@ Likewise for an :term:`HTTP exception` response:
.. code-block:: python
:linenos:
- from pyramid.httpexceptions import HTTPNotFound
+ from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
@view_config(renderer='json')
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index a7bf74786..7c0b437c1 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -772,95 +772,102 @@ ignored when ``static`` is ``True``.
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``.
-
-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:
+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.
+
+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:
+
+.. 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')
- config.add_view('myproject.views.no_slash', route_name='noslash')
- config.add_view('myproject.views.has_slash', 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.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.
-
-.. warning::
-
- You **should not** rely on this mechanism to redirect ``POST`` requests.
- The redirect of the slash-appending not found view will turn a ``POST``
- 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:
+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:
- 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.
+ from pyramid.httpexceptions import HTTPNotFound
+ from pyramid.view import notfound_view_config, view_config
-Custom Not Found View With Slash Appended Routes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ @notfound_view_config(append_slash=True)
+ def notfound(request):
+ return HTTPNotFound('Not found, bro.')
-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.
+ @view_config(route_name='noslash')
+ def no_slash(request):
+ return Response('No slash')
-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:
+ @view_config(route_name='hasslash')
+ def has_slash(request):
+ return Response('Has slash')
-.. code-block:: python
- :linenos:
-
- from pyramid.httpexceptions import HTTPNotFound
- from pyramid.view import AppendSlashNotFoundViewFactory
+ def main(g, **settings):
+ config = Configurator()
+ config.add_route('noslash', 'no_slash')
+ config.add_route('hasslash', 'has_slash/')
+ config.scan()
- def notfound_view(context, request):
- return HTTPNotFound('It aint there, stop trying!')
+.. warning::
- custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
- config.add_view(custom_append_slash, context=HTTPNotFound)
+ You **should not** rely on this mechanism to redirect ``POST`` requests.
+ The redirect of the slash-appending not found view will turn a ``POST``
+ request into a ``GET``, losing any ``POST`` data in the original
+ request.
-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
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index 4c7ecd359..b8d0f2319 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -204,7 +204,8 @@ def make_predicates(xhr=None, request_method=None, path_info=None,
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)
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 9d2e15537..7f6a37cc4 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -54,7 +54,12 @@ from pyramid.httpexceptions import (
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 @@ class ViewsConfiguratorMixin(object):
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 @@ class ViewsConfiguratorMixin(object):
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.
-
- .. warning::
-
- 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.
-
- The ``view`` argument should be a :term:`view callable` or a
- :term:`dotted Python name` which refers to a view callable.
-
- 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).
+ 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:
+
+ .. code-block:: python
+
+ config.add_notfound_view(someview)
+
+ 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.
+
+ 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):
diff --git a/pyramid/tests/pkgs/notfoundview/__init__.py b/pyramid/tests/pkgs/notfoundview/__init__.py
new file mode 100644
index 000000000..ae148ea8c
--- /dev/null
+++ b/pyramid/tests/pkgs/notfoundview/__init__.py
@@ -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')
+
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index eb18d5c84..668fd7185 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1682,14 +1682,14 @@ class TestViewsConfigurationMixin(unittest.TestCase):
- 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 @@ class TestViewsConfigurationMixin(unittest.TestCase):
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 @@ class TestViewsConfigurationMixin(unittest.TestCase):
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
diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py
index 86cd73910..57b7e40b2 100644
--- a/pyramid/tests/test_integration.py
+++ b/pyramid/tests/test_integration.py
@@ -357,6 +357,23 @@ class TestViewDecoratorApp(IntegrationBase, unittest.TestCase):
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'
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index 03a111828..f092f281b 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -48,7 +48,51 @@ class BaseTest(object):
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 @@ class DummyConfig(object):
def add_view(self, **kw):
self.settings.append(kw)
+ add_notfound_view = add_view
+
def with_package(self, pkg):
self.pkg = pkg
return self
diff --git a/pyramid/view.py b/pyramid/view.py
index a68f9ad8a..9f049bf09 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -9,11 +9,16 @@ from pyramid.interfaces import (
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 @@ class AppendSlashNotFoundViewFactory(object):
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 @@ class AppendSlashNotFoundViewFactory(object):
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 @@ See also :ref:`changing_the_notfound_view`.
"""
+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.