diff options
| -rw-r--r-- | CHANGES.txt | 52 | ||||
| -rw-r--r-- | TODO.txt | 10 | ||||
| -rw-r--r-- | docs/api/config.rst | 3 | ||||
| -rw-r--r-- | docs/api/view.rst | 6 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 81 | ||||
| -rw-r--r-- | docs/narr/renderers.rst | 2 | ||||
| -rw-r--r-- | docs/narr/urldispatch.rst | 141 | ||||
| -rw-r--r-- | pyramid/config/util.py | 3 | ||||
| -rw-r--r-- | pyramid/config/views.py | 105 | ||||
| -rw-r--r-- | pyramid/tests/pkgs/notfoundview/__init__.py | 30 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 28 | ||||
| -rw-r--r-- | pyramid/tests/test_integration.py | 17 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 48 | ||||
| -rw-r--r-- | pyramid/view.py | 91 |
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) ================== @@ -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. |
