diff options
| author | Chris McDonough <chrism@plope.com> | 2012-02-22 19:28:58 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-02-22 19:28:58 -0500 |
| commit | 6d0bce1dabce486cd6a6bc1fa7668ca863c0656a (patch) | |
| tree | 18f0c2d4c7cee39e8938d6c77b4e0792761382bd | |
| parent | bb40d09a0241b9e19cdc86186a7abd30d4d491d3 (diff) | |
| parent | e9b51b719db22e3e41e3b22c584f40b20971aa98 (diff) | |
| download | pyramid-6d0bce1dabce486cd6a6bc1fa7668ca863c0656a.tar.gz pyramid-6d0bce1dabce486cd6a6bc1fa7668ca863c0656a.tar.bz2 pyramid-6d0bce1dabce486cd6a6bc1fa7668ca863c0656a.zip | |
Merge branch '1.3-branch'
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) ================== @@ -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. |
