summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt111
-rw-r--r--TODO.txt17
-rw-r--r--docs/api/config.rst12
-rw-r--r--docs/api/registry.rst7
-rw-r--r--docs/api/view.rst9
-rw-r--r--docs/narr/hooks.rst110
-rw-r--r--docs/narr/introspector.rst15
-rw-r--r--docs/narr/renderers.rst2
-rw-r--r--docs/narr/urldispatch.rst141
-rw-r--r--docs/tutorials/wiki/authorization.rst28
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/views.py8
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/views.py8
-rw-r--r--docs/tutorials/wiki2/authorization.rst26
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views.py8
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views.py8
-rw-r--r--docs/whatsnew-1.3.rst80
-rw-r--r--pyramid/config/__init__.py51
-rw-r--r--pyramid/config/util.py3
-rw-r--r--pyramid/config/views.py192
-rw-r--r--pyramid/registry.py22
-rw-r--r--pyramid/scripts/pserve.py5
-rw-r--r--pyramid/static.py2
-rw-r--r--pyramid/tests/pkgs/forbiddenview/__init__.py31
-rw-r--r--pyramid/tests/pkgs/notfoundview/__init__.py30
-rw-r--r--pyramid/tests/test_config/test_init.py68
-rw-r--r--pyramid/tests/test_config/test_views.py52
-rw-r--r--pyramid/tests/test_integration.py26
-rw-r--r--pyramid/tests/test_path.py7
-rw-r--r--pyramid/tests/test_registry.py46
-rw-r--r--pyramid/tests/test_scripts/test_pserve.py2
-rw-r--r--pyramid/tests/test_static.py3
-rw-r--r--pyramid/tests/test_view.py90
-rw-r--r--pyramid/view.py156
33 files changed, 955 insertions, 421 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 1df924b4c..39bf59210 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,114 @@
+Next release
+============
+
+Features
+--------
+
+- Add an ``introspection`` boolean to the Configurator constructor. If this
+ is ``True``, actions registered using the Configurator will be registered
+ with the introspector. If it is ``False``, they won't. The default is
+ ``True``. Setting it to ``False`` during action processing will prevent
+ introspection for any following registration statements, and setting it to
+ ``True`` will start them up again. This addition is to service a
+ 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 and does the right thing about permissions. 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.
+
+- New API: ``pyramid.config.Configurator.add_forbidden_view``. This is a
+ wrapper for ``pyramid.Config.configurator.add_view`` which does the right
+ thing about permissions. It should be preferred over calling ``add_view``
+ directly with ``context=HTTPForbidden`` as was previously recommended.
+
+- New API: ``pyramid.view.forbidden_view_config``. This is a decorator
+ constructor like ``pyramid.view.view_config`` that calls
+ ``pyramid.config.Configurator.add_forbidden_view`` when scanned. It should
+ be preferred over using ``pyramid.view.view_config`` with
+ ``context=HTTPForbidden`` as was previously recommended.
+
+Backwards Incompatibilities
+---------------------------
+
+- Remove ``pyramid.config.Configurator.with_context`` class method. It was
+ never an API, it is only used by ``pyramid_zcml`` and its functionality has
+ been moved to that package's latest release. This means that you'll need
+ to use the 0.9.2 or later release of ``pyramid_zcml`` with this release of
+ Pyramid.
+
+- The ``introspector`` argument to the ``pyramid.config.Configurator``
+ constructor API has been removed. It has been replaced by the boolean
+ ``introspection`` flag.
+
+- 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. Likewise, the
+ older deprecated ``set_forbidden_view`` is now an alias for the new
+ ``add_forbidden_view``. This has the following impact: the ``context`` sent
+ to views with a ``(context, request)`` call signature registered via the
+ ``set_notfound_view`` or ``set_forbidden_view`` will now be an 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``, and disuse ``set_forbidden_view`` in favor of
+ ``add_forbidden_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`` and ``set_notfound_view`` methods of the
+ Configurator were removed from the documentation. They have 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 "Creating 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 "Creating a Not Forbidden View" section of the "Hooks" chapter,
+ replacing explanations of registering a view using ``add_view`` or
+ ``view_config`` with ones using ``add_forbidden_view`` or
+ ``forbidden_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``
+
+- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather
+ than ``pyramid.view.view_config`` with an HTTPForbidden context.
+
1.3a8 (2012-02-19)
==================
diff --git a/TODO.txt b/TODO.txt
index 90f5f8547..a58260d1f 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -4,6 +4,9 @@ Pyramid TODOs
Nice-to-Have
------------
+- Expose _FileIter and _FileResponse somehow fbo of
+ manual-static-view-creators.
+
- Add docs about upgrading between Pyramid versions (e.g. how to see
deprecation warnings).
@@ -15,7 +18,16 @@ Nice-to-Have
- Modify the urldispatch chapter examples to assume a scan rather than
``add_view``.
-- Decorator for append_slash_notfound_view_factory.
+- Context manager for creating a new configurator (replacing
+ ``with_package``). E.g.::
+
+ with config.partial(package='bar') as c:
+ c.add_view(...)
+
+ or::
+
+ with config.partial(introspection=False) as c:
+ c.add_view(..)
- Introspection:
@@ -27,9 +39,6 @@ Nice-to-Have
* get rid of "tweens" category (can't sort properly?)
- * Introspection hiding for directives for purposes of omitting toolbar
- registrations. Maybe toolbar can just use a null introspector?
-
- Fix deployment recipes in cookbook (discourage proxying without changing
server).
diff --git a/docs/api/config.rst b/docs/api/config.rst
index b76fed9cb..cd58e74d3 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -24,8 +24,8 @@
.. 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
+ .. automethod:: add_forbidden_view
:methodcategory:`Adding an Event Subscriber`
@@ -76,18 +76,18 @@
.. automethod:: action
.. automethod:: add_directive
.. automethod:: with_package
+ .. automethod:: derive_view
:methodcategory:`Utility Methods`
.. automethod:: absolute_asset_spec
- .. automethod:: derive_view
.. automethod:: maybe_dotted
- .. automethod:: setup_registry
:methodcategory:`ZCA-Related APIs`
.. automethod:: hook_zca
.. automethod:: unhook_zca
+ .. automethod:: setup_registry
:methodcategory:`Testing Helper APIs`
@@ -112,9 +112,7 @@
The :term:`introspector` related to this configuration. It is an
instance implementing the :class:`pyramid.interfaces.IIntrospector`
- interface. If the Configurator constructor was supplied with an
- ``introspector`` argument, this attribute will be that value.
- Otherwise, it will be an instance of a default introspector type.
+ interface.
.. note::
diff --git a/docs/api/registry.rst b/docs/api/registry.rst
index e18d1b6c2..e62e2ba6f 100644
--- a/docs/api/registry.rst
+++ b/docs/api/registry.rst
@@ -38,10 +38,3 @@
This class is new as of :app:`Pyramid` 1.3.
-.. class:: noop_introspector
-
- An introspector which throws away all registrations, useful for disabling
- introspection altogether (pass as ``introspector`` to the
- :term:`Configurator` constructor).
-
- This class is new as of :app:`Pyramid` 1.3.
diff --git a/docs/api/view.rst b/docs/api/view.rst
index 9f59ddae7..21d2bb90d 100644
--- a/docs/api/view.rst
+++ b/docs/api/view.rst
@@ -19,11 +19,14 @@
.. autoclass:: view_defaults
:members:
+ .. autoclass:: notfound_view_config
+ :members:
+
+ .. autoclass:: forbidden_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..b7f052b00 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.
+
+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
-Replace ``helloworld.views.notfound_view`` with a reference to the
-:term:`view callable` you want to use to represent the Not Found view.
+ 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
@@ -90,23 +145,40 @@ the view which generates it can be overridden as necessary.
The :term:`forbidden view` callable is a view callable like any other. The
:term:`view configuration` which causes it to be a "forbidden" view consists
-only of naming the :exc:`pyramid.httpexceptions.HTTPForbidden` class as the
-``context`` of the view configuration.
+of using the meth:`pyramid.config.Configurator.add_forbidden_view` API or the
+:class:`pyramid.view.forbidden_view_config` decorator.
-You can replace the forbidden view by using the
-:meth:`pyramid.config.Configurator.add_view` method to register an "exception
-view":
+For example, you can add a forbidden view by using the
+:meth:`pyramid.config.Configurator.add_forbidden_view` method to register a
+forbidden view:
.. code-block:: python
:linenos:
from helloworld.views import forbidden_view
from pyramid.httpexceptions import HTTPForbidden
- config.add_view(forbidden_view, context=HTTPForbidden)
+ config.add_forbidden_view(forbidden_view)
Replace ``helloworld.views.forbidden_view`` with a reference to the Python
:term:`view callable` you want to use to represent the Forbidden view.
+If instead you prefer to use decorators and a :term:`scan`, you can use the
+:class:`pyramid.view.forbidden_view_config` decorator to mark a view callable
+as a forbidden view:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.view import forbidden_view_config
+
+ forbidden_view_config()
+ def forbidden(request):
+ return Response('forbidden')
+
+ def main(globals, **settings):
+ config = Configurator()
+ config.scan()
+
Like any other view, the forbidden view must accept at least a ``request``
parameter, or both ``context`` and ``request``. The ``context`` (available
as ``request.context`` if you're using the request-only view argument
diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst
index d465c47d9..74595cac8 100644
--- a/docs/narr/introspector.rst
+++ b/docs/narr/introspector.rst
@@ -576,17 +576,14 @@ relationships. It looks something like this:
Disabling Introspection
-----------------------
-You can disable Pyramid introspection by passing the object
-:attr:`pyramid.registry.noop_introspector` to the :term:`Configurator`
-constructor in your application setup:
+You can disable Pyramid introspection by passing the flag
+``introspection=False`` to the :term:`Configurator` constructor in your
+application setup:
.. code-block:: python
from pyramid.config import Configurator
- from pyramid.registry import noop_introspector
- config = Configurator(..., introspector=noop_introspector)
+ config = Configurator(..., introspection=False)
-When the noop introspector is active, all introspectables generated by
-configuration directives are thrown away. A noop introspector behaves just
-like a "real" introspector, but the methods of a noop introspector do nothing
-and return null values.
+When ``introspection`` is ``False``, all introspectables generated by
+configuration directives are thrown away.
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/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index 8f583ece7..c1be2cc72 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -132,14 +132,14 @@ We'll add these views to the existing ``views.py`` file we have in our
project. Here's what the ``login`` view callable will look like:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 83-111
+ :lines: 86-113
:linenos:
:language: python
Here's what the ``logout`` view callable will look like:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 113-117
+ :lines: 115-119
:linenos:
:language: python
@@ -149,18 +149,18 @@ different :term:`view configuration` for the ``login`` view callable.
The first view configuration decorator configures the ``login`` view callable
so it will be invoked when someone visits ``/login`` (when the context is a
-Wiki and the view name is ``login``). The second decorator (with context of
-``pyramid.httpexceptions.HTTPForbidden``) specifies a :term:`forbidden view`.
-This configures our login view to be presented to the user when
-:app:`Pyramid` detects that a view invocation can not be authorized. Because
-we've configured a forbidden view, the ``login`` view callable will be
-invoked whenever one of our users tries to execute a view callable that they
-are not allowed to invoke as determined by the :term:`authorization policy`
-in use. In our application, for example, this means that if a user has not
-logged in, and he tries to add or edit a Wiki page, he will be shown the
-login form. Before being allowed to continue on to the add or edit form, he
-will have to provide credentials that give him permission to add or edit via
-this login form.
+Wiki and the view name is ``login``). The second decorator, named
+``forbidden_view_config`` specifies a :term:`forbidden view`. This
+configures our login view to be presented to the user when :app:`Pyramid`
+detects that a view invocation can not be authorized. Because we've
+configured a forbidden view, the ``login`` view callable will be invoked
+whenever one of our users tries to execute a view callable that they are not
+allowed to invoke as determined by the :term:`authorization policy` in use.
+In our application, for example, this means that if a user has not logged in,
+and he tries to add or edit a Wiki page, he will be shown the login form.
+Before being allowed to continue on to the add or edit form, he will have to
+provide credentials that give him permission to add or edit via this login
+form.
Note that we're relying on some additional imports within the bodies of these
views (e.g. ``remember`` and ``forget``). We'll see a rendering of the
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py
index 2f0502c17..fcbe6fe25 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py
@@ -3,7 +3,10 @@ import re
from pyramid.httpexceptions import HTTPFound
-from pyramid.view import view_config
+from pyramid.view import (
+ view_config,
+ forbidden_view_config,
+ )
from pyramid.security import (
authenticated_userid,
@@ -82,8 +85,7 @@ def edit_page(context, request):
@view_config(context='.models.Wiki', name='login',
renderer='templates/login.pt')
-@view_config(context='pyramid.httpexceptions.HTTPForbidden',
- renderer='templates/login.pt')
+@forbidden_view_config(renderer='templates/login.pt')
def login(request):
login_url = request.resource_url(request.context, 'login')
referrer = request.url
diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py
index 2f0502c17..fcbe6fe25 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/views.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/views.py
@@ -3,7 +3,10 @@ import re
from pyramid.httpexceptions import HTTPFound
-from pyramid.view import view_config
+from pyramid.view import (
+ view_config,
+ forbidden_view_config,
+ )
from pyramid.security import (
authenticated_userid,
@@ -82,8 +85,7 @@ def edit_page(context, request):
@view_config(context='.models.Wiki', name='login',
renderer='templates/login.pt')
-@view_config(context='pyramid.httpexceptions.HTTPForbidden',
- renderer='templates/login.pt')
+@forbidden_view_config(renderer='templates/login.pt')
def login(request):
login_url = request.resource_url(request.context, 'login')
referrer = request.url
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index b1d0bf37c..900bf0975 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -159,33 +159,35 @@ logged in user and redirect back to the front page.
The ``login`` view callable will look something like this:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 87-113
+ :lines: 89-115
:linenos:
:language: python
The ``logout`` view callable will look something like this:
.. literalinclude:: src/authorization/tutorial/views.py
- :lines: 115-119
+ :lines: 117-121
:linenos:
:language: python
-The ``login`` view callable is decorated with two ``@view_config``
-decorators, one which associates it with the ``login`` route, the other which
-associates it with the ``HTTPForbidden`` context. The one which associates
-it with the ``login`` route makes it visible when we visit ``/login``. The
-one which associates it with the ``HTTPForbidden`` context makes it the
-:term:`forbidden view`. The forbidden view is displayed whenever Pyramid or
-your application raises an HTTPForbidden exception. In this case, we'll be
-relying on the forbidden view to show the login form whenver someone attempts
-to execute an action which they're not yet authorized to perform.
+The ``login`` view callable is decorated with two decorators, a
+``@view_config`` decorators, which associates it with the ``login`` route,
+the other a ``@forbidden_view_config`` decorator which turns it in to an
+:term:`exception view` when Pyramid raises a
+:class:`pyramid.httpexceptions.HTTPForbidden` exception. The one which
+associates it with the ``login`` route makes it visible when we visit
+``/login``. The other one makes it a :term:`forbidden view`. The forbidden
+view is displayed whenever Pyramid or your application raises an
+HTTPForbidden exception. In this case, we'll be relying on the forbidden
+view to show the login form whenver someone attempts to execute an action
+which they're not yet authorized to perform.
The ``logout`` view callable is decorated with a ``@view_config`` decorator
which associates it with the ``logout`` route. This makes it visible when we
visit ``/login``.
We'll need to import some stuff to service the needs of these two functions:
-the ``HTTPForbidden`` exception, a number of values from the
+the ``pyramid.view.forbidden_view_config`` class, a number of values from the
``pyramid.security`` module, and a value from our newly added
``tutorial.security`` package.
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
index 087e6076b..1453cd2e6 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py
@@ -4,10 +4,12 @@ from docutils.core import publish_parts
from pyramid.httpexceptions import (
HTTPFound,
HTTPNotFound,
- HTTPForbidden,
)
-from pyramid.view import view_config
+from pyramid.view import (
+ view_config,
+ forbidden_view_config,
+ )
from pyramid.security import (
remember,
@@ -85,7 +87,7 @@ def edit_page(request):
)
@view_config(route_name='login', renderer='templates/login.pt')
-@view_config(context=HTTPForbidden, renderer='templates/login.pt')
+@forbidden_view_config(renderer='templates/login.pt')
def login(request):
login_url = request.route_url('login')
referrer = request.url
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py
index 375f1f5a5..465d98ae1 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/views.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py
@@ -4,10 +4,12 @@ from docutils.core import publish_parts
from pyramid.httpexceptions import (
HTTPFound,
HTTPNotFound,
- HTTPForbidden,
)
-from pyramid.view import view_config
+from pyramid.view import (
+ view_config,
+ forbidden_view_config,
+ )
from pyramid.security import (
remember,
@@ -88,7 +90,7 @@ def edit_page(request):
)
@view_config(route_name='login', renderer='templates/login.pt')
-@view_config(context=HTTPForbidden, renderer='templates/login.pt')
+@forbidden_view_config(renderer='templates/login.pt')
def login(request):
login_url = request.route_url('login')
referrer = request.url
diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst
index d2df88093..101caed94 100644
--- a/docs/whatsnew-1.3.rst
+++ b/docs/whatsnew-1.3.rst
@@ -128,7 +128,6 @@ application developer.
New APIs were added to support introspection
:attr:`pyramid.registry.Introspectable`,
-:attr:`pyramid.registry.noop_introspector`,
:attr:`pyramid.config.Configurator.introspector`,
:attr:`pyramid.config.Configurator.introspectable`,
:attr:`pyramid.registry.Registry.introspector`.
@@ -212,6 +211,38 @@ added, as well, but the configurator method should be preferred as it
provides conflict detection and consistency in the lifetime of the
properties.
+Not Found and Forbidden View Helpers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Not Found helpers:
+
+- New API: :meth:`pyramid.config.Configurator.add_notfound_view`. This is a
+ wrapper for :meth:`pyramid.Config.configurator.add_view` which provides
+ support for an "append_slash" feature as well as doing the right thing when
+ it comes to permissions (a not found view should always be public). It
+ should be preferred over calling ``add_view`` directly with
+ ``context=HTTPNotFound`` as was previously recommended.
+
+- New API: :class:`pyramid.view.notfound_view_config``. This is a decorator
+ constructor like :class:`pyramid.view.view_config` that calls
+ :meth:`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.
+
+Forbidden helpers:
+
+- New API: :meth:`pyramid.config.Configurator.add_forbidden_view`. This is a
+ wrapper for :meth:`pyramid.Config.configurator.add_view` which does the
+ right thing about permissions. It should be preferred over calling
+ ``add_view`` directly with ``context=HTTPForbidden`` as was previously
+ recommended.
+
+- New API: :class:`pyramid.view.forbidden_view_config`. This is a decorator
+ constructor like :class:`pyramid.view.view_config` that calls
+ :meth:`pyramid.config.Configurator.add_forbidden_view` when scanned. It
+ should be preferred over using ``pyramid.view.view_config`` with
+ ``context=HTTPForbidden`` as was previously recommended.
+
Minor Feature Additions
-----------------------
@@ -409,6 +440,38 @@ Backwards Incompatibilities
``pyramid.interfaces.IContextURL`` adapter is found when
:meth:`pyramid.request.Request.resource_url` is called.
+- Remove ``pyramid.config.Configurator.with_context`` class method. It was
+ never an API, it is only used by ``pyramid_zcml`` and its functionality has
+ been moved to that package's latest release. This means that you'll need
+ to use the 0.9.2 or later release of ``pyramid_zcml`` with this release of
+ Pyramid.
+
+- The older deprecated ``set_notfound_view`` Configurator method is now an
+ alias for the new ``add_notfound_view`` Configurator method. Likewise, the
+ older deprecated ``set_forbidden_view`` is now an alias for the new
+ ``add_forbidden_view`` Configurator method. This has the following impact:
+ the ``context`` sent to views with a ``(context, request)`` call signature
+ registered via the ``set_notfound_view`` or ``set_forbidden_view`` will now
+ be an 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``, and disuse ``set_forbidden_view`` in favor of
+ ``add_forbidden_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`` and ``set_notfound_view`` methods of the
+ Configurator were removed from the documentation. They have been
+ deprecated since Pyramid 1.1.
+
Documentation Enhancements
--------------------------
@@ -441,6 +504,21 @@ Documentation Enhancements
Rationale: it provides the correct info for the Python 2.5 version of GAE
only, and this version of Pyramid does not support Python 2.5.
+- Updated the :ref:`changing_the_forbidden_view` section, replacing
+ explanations of registering a view using ``add_view`` or ``view_config``
+ with ones using ``add_forbidden_view`` or ``forbidden_view_config``.
+
+- Updated the :ref:`changing_the_notfound_view` section, replacing
+ explanations of registering a view using ``add_view`` or ``view_config``
+ with ones using ``add_notfound_view`` or ``notfound_view_config``.
+
+- Updated the :ref:`redirecting_to_slash_appended_routes` section, replacing
+ explanations of registering a view using ``add_view`` or ``view_config``
+ with ones using ``add_notfound_view`` or ``notfound_view_config``
+
+- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather
+ than ``pyramid.view.view_config`` with an HTTPForbidden context.
+
Dependency Changes
------------------
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 06d3c6abf..52d7aca83 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -239,11 +239,11 @@ class Configurator(
:meth:`pyramid.config.Configurator.add_route` will have the specified path
prepended to their pattern. This parameter is new in Pyramid 1.2.
- If ``introspector`` is passed, it must be an instance implementing the
- attributes and methods of :class:`pyramid.interfaces.IIntrospector`. If
- ``introspector`` is not passed (or is passed as ``None``), the default
- introspector implementation will be used. This parameter is new in
- Pyramid 1.3.
+ If ``introspection`` is passed, it must be a boolean value. If it's
+ ``True``, introspection values during actions will be kept for for use
+ for tools like the debug toolbar. If it's ``False``, introspection
+ values provided by registrations will be ignored. By default, it is
+ ``True``. This parameter is new as of Pyramid 1.3.
"""
manager = manager # for testing injection
venusian = venusian # for testing injection
@@ -273,7 +273,7 @@ class Configurator(
autocommit=False,
exceptionresponse_view=default_exceptionresponse_view,
route_prefix=None,
- introspector=None,
+ introspection=True,
):
if package is None:
package = caller_package()
@@ -284,6 +284,7 @@ class Configurator(
self.registry = registry
self.autocommit = autocommit
self.route_prefix = route_prefix
+ self.introspection = introspection
if registry is None:
registry = Registry(self.package_name)
self.registry = registry
@@ -301,7 +302,6 @@ class Configurator(
session_factory=session_factory,
default_view_mapper=default_view_mapper,
exceptionresponse_view=exceptionresponse_view,
- introspector=introspector,
)
def setup_registry(self,
@@ -318,7 +318,7 @@ class Configurator(
session_factory=None,
default_view_mapper=None,
exceptionresponse_view=default_exceptionresponse_view,
- introspector=None):
+ ):
""" When you pass a non-``None`` ``registry`` argument to the
:term:`Configurator` constructor, no initial setup is performed
against the registry. This is because the registry you pass in may
@@ -339,10 +339,6 @@ class Configurator(
self._fix_registry()
- if introspector is not None:
- # use nondefault introspector
- self.introspector = introspector
-
self._set_settings(settings)
self._register_response_adapters()
@@ -529,7 +525,8 @@ class Configurator(
``introspectables`` is a sequence of :term:`introspectable` objects
(or the empty sequence if no introspectable objects are associated
- with this action).
+ with this action). If this configurator's ``introspection``
+ attribute is ``False``, these introspectables will be ignored.
``extra`` provides a facility for inserting extra keys and values
into an action dictionary.
@@ -543,14 +540,17 @@ class Configurator(
autocommit = self.autocommit
action_info = self.action_info
- introspector = self.introspector
+
+ if not self.introspection:
+ # if we're not introspecting, ignore any introspectables passed
+ # to us
+ introspectables = ()
if autocommit:
if callable is not None:
callable(*args, **kw)
- if introspector is not None:
- for introspectable in introspectables:
- introspectable.register(introspector, action_info)
+ for introspectable in introspectables:
+ introspectable.register(self.introspector, action_info)
else:
action = extra
@@ -782,22 +782,6 @@ class Configurator(
m = types.MethodType(c, self, self.__class__)
return m
- @classmethod
- def with_context(cls, context):
- """A classmethod used by ``pyramid_zcml`` directives to obtain a
- configurator with 'the right' context. Returns a new Configurator
- instance."""
- configurator = cls(
- registry=context.registry,
- package=context.package,
- autocommit=context.autocommit,
- route_prefix=context.route_prefix
- )
- configurator.basepath = context.basepath
- configurator.includepath = context.includepath
- configurator.info = context.info
- return configurator
-
def with_package(self, package):
""" Return a new Configurator instance with the same registry
as this configurator using the package supplied as the
@@ -809,6 +793,7 @@ class Configurator(
package=package,
autocommit=self.autocommit,
route_prefix=self.route_prefix,
+ introspection=self.introspection,
)
configurator.basepath = self.basepath
configurator.includepath = self.includepath
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..b4216c220 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 (
@@ -1324,87 +1329,124 @@ class ViewsConfiguratorMixin(object):
return deriver(view)
@action_method
- def set_forbidden_view(self, view=None, attr=None, renderer=None,
- wrapper=None):
- """ Add a default forbidden 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_forbidden_view`
- to see how a forbidden 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_forbidden_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):
+ """ Add a forbidden view to the current configuration state. The
+ view will be called when Pyramid or application code raises a
+ :exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of
+ circumstances implied by the predicates provided are matched. The
+ simplest example is:
+
+ .. code-block:: python
+
+ def forbidden(request):
+ return Response('Forbidden', status='403 Forbidden')
+
+ config.add_forbidden_view(forbidden)
+
+ All arguments have the same meaning as
+ :meth:`pyramid.config.Configurator.add_view` and each predicate
+ argument restricts the set of circumstances under which this notfound
+ view will be invoked.
- The ``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)."""
- 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=HTTPForbidden,
- wrapper=wrapper, renderer=renderer)
+ This method is new as of Pyramid 1.3.
+ """
+ settings = dict(
+ view=view,
+ context=HTTPForbidden,
+ 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,
+ permission=NO_PERMISSION_REQUIRED,
+ attr=attr,
+ renderer=renderer,
+ )
+ return self.add_view(**settings)
+ set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias
+
@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.
+ 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 Pyramid or application code raises an
+ :exc:`pyramid.httpexceptions.HTTPForbidden` exception (e.g. when a
+ view cannot be found for the request). The simplest example is:
+
+ .. code-block:: python
+
+ def notfound(request):
+ return Response('Not Found', status='404 Not Found')
+
+ config.add_notfound_view(notfound)
+
+ 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 ``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).
-
- 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,
+ permission=NO_PERMISSION_REQUIRED,
+ )
+ 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/registry.py b/pyramid/registry.py
index 7e373b58a..f0f9c83ea 100644
--- a/pyramid/registry.py
+++ b/pyramid/registry.py
@@ -172,28 +172,6 @@ class Introspector(object):
raise KeyError((category_name, discriminator))
return self._refs.get(intr, [])
-@implementer(IIntrospector)
-class _NoopIntrospector(object):
- def add(self, intr):
- pass
- def get(self, category_name, discriminator, default=None):
- return default
- def get_category(self, category_name, default=None, sort_key=None):
- return default
- def categorized(self, sort_key=None):
- return []
- def categories(self):
- return []
- def remove(self, category_name, discriminator):
- return
- def relate(self, *pairs):
- return
- unrelate = relate
- def related(self, intr):
- return []
-
-noop_introspector = _NoopIntrospector()
-
@implementer(IIntrospectable)
class Introspectable(dict):
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index 087549cd2..c2df7162f 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -581,6 +581,11 @@ class LazyWriter(object):
self.lock.release()
return self.fileobj
+ def __del__(self):
+ fileobj = self.fileobj
+ if fileobj is not None:
+ fileobj.close()
+
def write(self, text):
fileobj = self.open()
fileobj.write(text)
diff --git a/pyramid/static.py b/pyramid/static.py
index 8788d016d..e91485fad 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -68,7 +68,7 @@ class _FileResponse(Response):
if 'wsgi.file_wrapper' in environ:
app_iter = environ['wsgi.file_wrapper'](f, _BLOCK_SIZE)
else:
- app_iter = _FileIter(open(path, 'rb'), _BLOCK_SIZE)
+ app_iter = _FileIter(f, _BLOCK_SIZE)
self.app_iter = app_iter
# assignment of content_length must come after assignment of app_iter
self.content_length = content_length
diff --git a/pyramid/tests/pkgs/forbiddenview/__init__.py b/pyramid/tests/pkgs/forbiddenview/__init__.py
new file mode 100644
index 000000000..631a442d2
--- /dev/null
+++ b/pyramid/tests/pkgs/forbiddenview/__init__.py
@@ -0,0 +1,31 @@
+from pyramid.view import forbidden_view_config, view_config
+from pyramid.response import Response
+from pyramid.authentication import AuthTktAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+
+@forbidden_view_config(route_name='foo')
+def foo_forbidden(request): # pragma: no cover
+ return Response('foo_forbidden')
+
+@forbidden_view_config()
+def forbidden(request):
+ return Response('generic_forbidden')
+
+@view_config(route_name='foo')
+def foo(request): # pragma: no cover
+ return Response('OK foo')
+
+@view_config(route_name='bar')
+def bar(request): # pragma: no cover
+ return Response('OK bar')
+
+def includeme(config):
+ authn_policy = AuthTktAuthenticationPolicy('seekri1')
+ authz_policy = ACLAuthorizationPolicy()
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
+ config.set_default_permission('a')
+ config.add_route('foo', '/foo')
+ config.add_route('bar', '/bar')
+ config.scan('pyramid.tests.pkgs.forbiddenview')
+
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_init.py b/pyramid/tests/test_config/test_init.py
index 283800e1e..da331e5ee 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -228,11 +228,9 @@ class ConfiguratorTests(unittest.TestCase):
request_iface=IRequest)
self.assertTrue(view.__wraps__ is exceptionresponse_view)
- def test_ctor_with_introspector(self):
- introspector = DummyIntrospector()
- config = self._makeOne(introspector=introspector)
- self.assertEqual(config.introspector, introspector)
- self.assertEqual(config.registry.introspector, introspector)
+ def test_ctor_with_introspection(self):
+ config = self._makeOne(introspection=False)
+ self.assertEqual(config.introspection, False)
def test_with_package_module(self):
from pyramid.tests.test_config import test_init
@@ -648,7 +646,7 @@ pyramid.tests.test_config.dummy_include2""",
default = inst.introspector
self.assertTrue(hasattr(default, 'add'))
self.assertEqual(inst.introspector, inst.registry.introspector)
- introspector = DummyIntrospector()
+ introspector = object()
inst.introspector = introspector
new = inst.introspector
self.assertTrue(new is introspector)
@@ -759,25 +757,6 @@ pyramid.tests.test_config.dummy_include2""",
else: # pragma: no cover
raise AssertionError
- def test_with_context(self):
- config = self._makeOne()
- context = DummyZCMLContext()
- context.basepath = 'basepath'
- context.includepath = ('spec',)
- context.package = 'pyramid'
- context.autocommit = True
- context.registry = 'abc'
- context.route_prefix = 'buz'
- context.info = 'info'
- newconfig = config.with_context(context)
- self.assertEqual(newconfig.package_name, 'pyramid')
- self.assertEqual(newconfig.autocommit, True)
- self.assertEqual(newconfig.registry, 'abc')
- self.assertEqual(newconfig.route_prefix, 'buz')
- self.assertEqual(newconfig.basepath, 'basepath')
- self.assertEqual(newconfig.includepath, ('spec',))
- self.assertEqual(newconfig.info, 'info')
-
def test_action_branching_kw_is_None(self):
config = self._makeOne(autocommit=True)
self.assertEqual(config.action('discrim'), None)
@@ -795,6 +774,13 @@ pyramid.tests.test_config.dummy_include2""",
self.assertEqual(intr.registered[0][0], config.introspector)
self.assertEqual(intr.registered[0][1].__class__, ActionInfo)
+ def test_action_autocommit_with_introspectables_introspection_off(self):
+ config = self._makeOne(autocommit=True)
+ config.introspection = False
+ intr = DummyIntrospectable()
+ config.action('discrim', introspectables=(intr,))
+ self.assertEqual(len(intr.registered), 0)
+
def test_action_branching_nonautocommit_with_config_info(self):
config = self._makeOne(autocommit=False)
config.info = 'abc'
@@ -846,6 +832,19 @@ pyramid.tests.test_config.dummy_include2""",
self.assertEqual(
state.actions[0][1]['introspectables'], (intr,))
+ def test_action_nonautocommit_with_introspectables_introspection_off(self):
+ config = self._makeOne(autocommit=False)
+ config.info = ''
+ config._ainfo = []
+ config.introspection = False
+ state = DummyActionState()
+ config.action_state = state
+ state.autocommit = False
+ intr = DummyIntrospectable()
+ config.action('discrim', introspectables=(intr,))
+ self.assertEqual(
+ state.actions[0][1]['introspectables'], ())
+
def test_scan_integration(self):
from zope.interface import alsoProvides
from pyramid.interfaces import IRequest
@@ -1638,7 +1637,7 @@ class TestActionState(unittest.TestCase):
'order':0, 'includepath':(), 'info':None,
'introspectables':(intr,)},
]
- introspector = DummyIntrospector()
+ introspector = object()
c.execute_actions(introspector=introspector)
self.assertEqual(output, [((1,), {})])
self.assertEqual(intr.registered, [(introspector, None)])
@@ -1651,7 +1650,7 @@ class TestActionState(unittest.TestCase):
'order':0, 'includepath':(), 'info':None,
'introspectables':(intr,)},
]
- introspector = DummyIntrospector()
+ introspector = object()
c.execute_actions(introspector=introspector)
self.assertEqual(intr.registered, [(introspector, None)])
@@ -1982,21 +1981,6 @@ class DummyActionState(object):
def action(self, *arg, **kw):
self.actions.append((arg, kw))
-class DummyZCMLContext(object):
- package = None
- registry = None
- autocommit = False
- route_prefix = None
- basepath = None
- includepath = ()
- info = ''
-
-class DummyIntrospector(object):
- def __init__(self):
- self.intrs = []
- def add(self, intr):
- self.intrs.append(intr)
-
class DummyIntrospectable(object):
def __init__(self):
self.registered = []
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index eb18d5c84..7bfe174b7 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1649,14 +1649,14 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertEqual(info.added,
[(config, 'static', static_path, {})])
- def test_set_forbidden_view(self):
+ def test_add_forbidden_view(self):
from pyramid.renderers import null_renderer
from zope.interface import implementedBy
from pyramid.interfaces import IRequest
from pyramid.httpexceptions import HTTPForbidden
config = self._makeOne(autocommit=True)
view = lambda *arg: 'OK'
- config.set_forbidden_view(view, renderer=null_renderer)
+ config.add_forbidden_view(view, renderer=null_renderer)
request = self._makeRequest(config)
view = self._getViewCallable(config,
ctx_iface=implementedBy(HTTPForbidden),
@@ -1664,32 +1664,14 @@ class TestViewsConfigurationMixin(unittest.TestCase):
result = view(None, request)
self.assertEqual(result, 'OK')
- def test_set_forbidden_view_request_has_context(self):
- from pyramid.renderers import null_renderer
- from zope.interface import implementedBy
- from pyramid.interfaces import IRequest
- from pyramid.httpexceptions import HTTPForbidden
- config = self._makeOne(autocommit=True)
- view = lambda *arg: arg
- config.set_forbidden_view(view, renderer=null_renderer)
- request = self._makeRequest(config)
- request.context = 'abc'
- view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPForbidden),
- request_iface=IRequest)
- result = view(None, request)
- self.assertEqual(result, ('abc', request))
-
-
-
- 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 +1679,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,14 +1719,13 @@ 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
config = self._makeOne(autocommit=True)
view = lambda *arg: {}
- config.set_forbidden_view(
+ config.add_forbidden_view(
view,
renderer='pyramid.tests.test_config:files/minimal.pt')
config.begin()
diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py
index 86cd73910..bf3bafc09 100644
--- a/pyramid/tests/test_integration.py
+++ b/pyramid/tests/test_integration.py
@@ -357,6 +357,32 @@ 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 TestForbiddenView(IntegrationBase, unittest.TestCase):
+ package = 'pyramid.tests.pkgs.forbiddenview'
+
+ def test_it(self):
+ res = self.testapp.get('/foo', status=200)
+ self.assertTrue(b'foo_forbidden' in res.body)
+ res = self.testapp.get('/bar', status=200)
+ self.assertTrue(b'generic_forbidden' 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_path.py b/pyramid/tests/test_path.py
index 304afad7c..42b38d785 100644
--- a/pyramid/tests/test_path.py
+++ b/pyramid/tests/test_path.py
@@ -283,7 +283,8 @@ class TestPkgResourcesAssetDescriptor(unittest.TestCase):
inst = self._makeOne()
inst.pkg_resources = DummyPkgResource()
inst.pkg_resources.resource_stream = lambda x, y: '%s:%s' % (x, y)
- self.assertEqual(inst.stream(),
+ s = inst.stream()
+ self.assertEqual(s,
'%s:%s' % ('pyramid.tests', 'test_asset.py'))
def test_isdir(self):
@@ -337,7 +338,9 @@ class TestFSAssetDescriptor(unittest.TestCase):
def test_stream(self):
inst = self._makeOne()
- val = inst.stream().read()
+ s = inst.stream()
+ val = s.read()
+ s.close()
self.assertTrue(b'asset' in val)
def test_isdir_False(self):
diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py
index 29803346a..11019b852 100644
--- a/pyramid/tests/test_registry.py
+++ b/pyramid/tests/test_registry.py
@@ -254,52 +254,6 @@ class TestIntrospector(unittest.TestCase):
del inst._categories['category']
self.assertRaises(KeyError, inst.related, intr)
-class Test_noop_introspector(unittest.TestCase):
- def _makeOne(self):
- from pyramid.registry import noop_introspector
- return noop_introspector
-
- def test_conformance(self):
- from zope.interface.verify import verifyObject
- from pyramid.interfaces import IIntrospector
- verifyObject(IIntrospector, self._makeOne())
-
- def test_add(self):
- inst = self._makeOne()
- self.assertEqual(inst.add('a'), None)
-
- def test_get(self):
- inst = self._makeOne()
- self.assertEqual(inst.get('category', 'd', default='123'), '123')
-
- def test_get_category(self):
- inst = self._makeOne()
- self.assertEqual(inst.get_category('category', default='123'), '123')
-
- def test_categorized(self):
- inst = self._makeOne()
- self.assertEqual(inst.categorized(), [])
-
- def test_categories(self):
- inst = self._makeOne()
- self.assertEqual(inst.categories(), [])
-
- def test_remove(self):
- inst = self._makeOne()
- self.assertEqual(inst.remove('cat', 'discrim'), None)
-
- def test_relate(self):
- inst = self._makeOne()
- self.assertEqual(inst.relate(), None)
-
- def test_unrelate(self):
- inst = self._makeOne()
- self.assertEqual(inst.unrelate(), None)
-
- def test_related(self):
- inst = self._makeOne()
- self.assertEqual(inst.related('a'), [])
-
class TestIntrospectable(unittest.TestCase):
def _getTargetClass(slf):
from pyramid.registry import Introspectable
diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py
index fe489aa66..d19eb6901 100644
--- a/pyramid/tests/test_scripts/test_pserve.py
+++ b/pyramid/tests/test_scripts/test_pserve.py
@@ -87,6 +87,7 @@ class TestLazyWriter(unittest.TestCase):
inst = self._makeOne(filename)
fp = inst.open()
self.assertEqual(fp.name, filename)
+ fp.close()
finally:
os.remove(filename)
@@ -122,6 +123,7 @@ class TestLazyWriter(unittest.TestCase):
inst.flush()
fp = inst.fileobj
self.assertEqual(fp.name, filename)
+ fp.close()
finally:
os.remove(filename)
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index 3d6fbe893..02cd49430 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -128,6 +128,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase):
self.assertTrue(isinstance(app_iter, _Wrapper))
self.assertTrue(b'<html>static</html>' in app_iter.file.read())
self.assertEqual(app_iter.block_size, _BLOCK_SIZE)
+ app_iter.file.close()
def test_resource_is_file_with_cache_max_age(self):
inst = self._makeOne('pyramid.tests:fixtures/static', cache_max_age=600)
@@ -185,6 +186,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase):
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/x-tar')
self.assertEqual(response.content_encoding, 'gzip')
+ response.app_iter.close()
def test_resource_no_content_encoding(self):
inst = self._makeOne('pyramid.tests:fixtures/static')
@@ -194,6 +196,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase):
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'text/html')
self.assertEqual(response.content_encoding, None)
+ response.app_iter.close()
class Test_static_view_use_subpath_True(unittest.TestCase):
def _getTargetClass(self):
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index 03a111828..a775e7bc9 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -48,8 +48,94 @@ 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 Test_forbidden_view_config(BaseTest, unittest.TestCase):
+ def _makeOne(self, **kw):
+ from pyramid.view import forbidden_view_config
+ return forbidden_view_config(**kw)
+
+ def test_ctor(self):
+ inst = self._makeOne(attr='attr', path_info='path_info')
+ self.assertEqual(inst.__dict__,
+ {'attr':'attr', 'path_info':'path_info'})
+
+ def test_it_function(self):
+ def view(request): pass
+ decorator = self._makeOne(attr='attr', renderer='renderer')
+ 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,
+ '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]), 4)
+ 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):
from pyramid.view import render_view_to_response
@@ -672,6 +758,8 @@ class DummyConfig(object):
def add_view(self, **kw):
self.settings.append(kw)
+ add_notfound_view = add_forbidden_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..d722c0cbb 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,145 @@ 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
+
+class forbidden_view_config(object):
+ """
+
+ An analogue of :class:`pyramid.view.view_config` which registers a
+ :term:`forbidden view`.
+
+ The forbidden_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 forbidden exception view instead of a "normal" view.
+
+ Example:
+
+ .. code-block:: python
+
+ from pyramid.view import forbidden_view_config
+ from pyramid.response import Response
+
+ forbidden_view_config()
+ def notfound(request):
+ return Response('You are not allowed', status='401 Unauthorized')
+
+ All 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.
+
+ See :ref:`changing_the_forbidden_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):
+ 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_forbidden_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.