summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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.