diff options
47 files changed, 2684 insertions, 663 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 0254ac2b0..c58ff755b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -39,6 +39,11 @@ Documentation - Added API docs for ``pyramid.authentication.SessionAuthenticationPolicy``. +- Added API docs for ``pyramid.httpexceptions.responsecode``. + +- Added "HTTP Exceptions" section to Views narrative chapter including a + description of ``pyramid.httpexceptions.responsecode``. + Features -------- @@ -109,6 +114,48 @@ Features section entitled "Static Routes" in the URL Dispatch narrative chapter for more information. +- A default exception view for the context + ``pyramid.interfaces.IExceptionResponse`` is now registered by default. + This means that an instance of any exception response class imported from + ``pyramid.httpexceptions`` (such as ``HTTPFound``) can now be raised from + within view code; when raised, this exception view will render the + exception to a response. + +- A function named ``pyramid.httpexceptions.responsecode`` is a shortcut that + can be used to create HTTP exception response objects using an HTTP integer + status code. + +- The Configurator now accepts an additional keyword argument named + ``exceptionresponse_view``. By default, this argument is populated with a + default exception view function that will be used when a response is raised + as an exception. When ``None`` is passed for this value, an exception view + for responses will not be registered. Passing ``None`` returns the + behavior of raising an HTTP exception to that of Pyramid 1.0 (the exception + will propagate to middleware and to the WSGI server). + +- The ``pyramid.request.Request`` class now has a ``ResponseClass`` interface + which points at ``pyramid.response.Response``. + +- The ``pyramid.request.Response`` class now has a ``RequestClass`` interface + which points at ``pyramid.response.Request``. + +- It is now possible to return an arbitrary object from a Pyramid view + callable even if a renderer is not used, as long as a suitable adapter to + ``pyramid.interfaces.IResponse`` is registered for the type of the returned + object by using the new + ``pyramid.config.Configurator.add_response_adapter`` API. See the section + in the Hooks chapter of the documentation entitled "Changing How Pyramid + Treats View Responses". + +- The Pyramid router will now, by default, call the ``__call__`` method of + WebOb response objects when returning a WSGI response. This means that, + among other things, the ``conditional_response`` feature of WebOb response + objects will now behave properly. + +- New method named ``pyramid.request.Request.is_response``. This method + should be used instead of the ``pyramid.view.is_response`` function, which + has been deprecated. + Bug Fixes --------- @@ -239,6 +286,14 @@ Deprecations 1.0 and before). In a future version, these methods will be removed entirely. +- Deprecated ``pyramid.view.is_response`` function in favor of (newly-added) + ``pyramid.request.Request.is_response`` method. Determining if an object + is truly a valid response object now requires access to the registry, which + is only easily available as a request attribute. The + ``pyramid.view.is_response`` function will still work until it is removed, + but now may return an incorrect answer under some (very uncommon) + circumstances. + Behavior Changes ---------------- @@ -249,7 +304,7 @@ Behavior Changes For example, ${ myhtml | n }. See https://github.com/Pylons/pyramid/issues/193. -- A custom request factory is now required to return a response object that +- A custom request factory is now required to return a request object that has a ``response`` attribute (or "reified"/lazy property) if they the request is meant to be used in a view that uses a renderer. This ``response`` attribute should be an instance of the class @@ -280,6 +335,50 @@ Behavior Changes implements its own ``__getattr__``, ``__setattr__`` or ``__delattr__`` as a result. +- ``pyramid.response.Response`` is now a *subclass* of + ``webob.response.Response`` (in order to directly implement the + ``pyramid.interfaces.IResponse`` interface). + +- The "exception response" objects importable from ``pyramid.httpexceptions`` + (e.g. ``HTTPNotFound``) are no longer just import aliases for classes that + actually live in ``webob.exc``. Instead, we've defined our own exception + classes within the module that mirror and emulate the ``webob.exc`` + exception response objects almost entirely. We do this in order to a) + allow the exception responses to subclass ``pyramid.response.Response``, + which speeds up response generation slightly due to the way the Pyramid + router works, b) allows us to provide alternate __call__ logic which also + speeds up response generation, c) allows the exception classes to provide + for the proper value of ``self.RequestClass`` (pyramid.request.Request), d) + allows us freedom from having to think about backwards compatibility code + present in ``webob.exc`` having to do with Python 2.4, which we no longer + support, e) We change the behavior of two classes (HTTPNotFound and + HTTPForbidden) in the module so that they can be used internally for + notfound and forbidden exceptions, f) allows us to influence the docstrings + of the exception classes to provide Pyramid-specific documentation, and g) + allows us to silence a stupid deprecation warning under Python 2.6 when the + response objects are used as exceptions (related to ``self.message``). + +Backwards Incompatibilities +--------------------------- + +- The Pyramid router now, by default, expects response objects returned from + view callables to implement the ``pyramid.interfaces.IResponse`` interface. + Unlike the Pyramid 1.0 version of this interface, objects which implement + IResponse now must define a ``__call__`` method that accepts ``environ`` + and ``start_response``, and which returns an ``app_iter`` iterable, among + other things. Previously, it was possible to return any object which had + the three WebOb ``app_iter``, ``headerlist``, and ``status`` attributes as + a response, so this is a backwards incompatibility. It is possible to get + backwards compatibility back by registering an adapter to IResponse from + the type of object you're now returning from view callables. See the + section in the Hooks chapter of the documentation entitled "Changing How + Pyramid Treats View Responses". + +- The ``pyramid.interfaces.IResponse`` interface is now much more extensive. + Previously it defined only ``app_iter``, ``status`` and ``headerlist``; now + it is basically intended to directly mirror the ``webob.Response`` API, + which has many methods and attributes. + Dependencies ------------ @@ -1,13 +1,21 @@ Pyramid TODOs ============= +Must-Have +--------- + +- Copy exception templates from webob.exc into pyramid.httpexceptions and + ensure they all work. + +- Docs mention ``exception.args[0]`` as a way to get messages; check that + this works. + +- Deprecate response_foo attrs on request at attribute set time rather than + lookup time. + Should-Have ----------- -- Consider adding a default exception view for HTTPException and attendant - ``redirect`` and ``abort`` functions ala Pylons (promised Mike I'd enable - this in 1.1). - - Add narrative docs for wsgiapp and wsgiapp2. Nice-to-Have diff --git a/docs/api/config.rst b/docs/api/config.rst index 2b9d7bcef..274ee0292 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -40,6 +40,8 @@ .. automethod:: add_renderer(name, factory) + .. automethod:: add_response_adapter + .. automethod:: add_route .. automethod:: add_static_view(name, path, cache_max_age=3600, permission='__no_permission_required__') diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 57ca8092c..325d5af03 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -11,6 +11,8 @@ integer "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). + .. autofunction:: responsecode + .. autoclass:: HTTPException .. autoclass:: HTTPOk diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index ac282fbcc..51a1963b5 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -57,4 +57,6 @@ Other Interfaces .. autointerface:: IMultiDict :members: + .. autointerface:: IResponse + :members: diff --git a/docs/api/request.rst b/docs/api/request.rst index 8cb424658..27ce395ac 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -107,7 +107,9 @@ return {'text':'Value that will be used by the renderer'} Mutations to this response object will be preserved in the response sent - to the client after rendering. + to the client after rendering. For more information about using + ``request.response`` in conjunction with a renderer, see + :ref:`request_response_attr`. Non-renderer code can also make use of request.response instead of creating a response "by hand". For example, in view code:: @@ -162,20 +164,21 @@ .. attribute:: response_* - .. warning:: As of Pyramid 1.1, assignment to ``response_*`` attrs are - deprecated. Assigning to one will cause a deprecation warning to be - emitted. Instead of assigning ``response_*`` attributes to the - request, use API of the the :attr:`pyramid.request.Request.response` - object (exposed to view code as ``request.response``) to influence - response behavior. - - You can set attributes on a :class:`pyramid.request.Request` which will - influence the behavor of *rendered* responses (views which use a - :term:`renderer` and which don't directly return a response). These - attributes begin with ``response_``, such as ``response_headerlist``. If - you need to influence response values from a view that uses a renderer - (such as the status code, a header, the content type, etc) see, - :ref:`response_prefixed_attrs`. + In Pyramid 1.0, you could set attributes on a + :class:`pyramid.request.Request` which influenced the behavor of + *rendered* responses (views which use a :term:`renderer` and which + don't directly return a response). These attributes began with + ``response_``, such as ``response_headerlist``. If you needed to + influence response values from a view that uses a renderer (such as the + status code, a header, the content type, etc) you would set these + attributes. See :ref:`response_prefixed_attrs` for further discussion. + As of Pyramid 1.1, assignment to ``response_*`` attrs are deprecated. + Assigning to one is still supported but will cause a deprecation + warning to be emitted, and eventually the feature will be removed. For + new code, instead of assigning ``response_*`` attributes to the + request, use API of the the :attr:`pyramid.request.Request.response` + object (exposed to view code as ``request.response``) to influence + rendered response behavior. .. note:: diff --git a/docs/api/response.rst b/docs/api/response.rst index c545b4977..e67b15568 100644 --- a/docs/api/response.rst +++ b/docs/api/response.rst @@ -8,3 +8,4 @@ .. autoclass:: Response :members: :inherited-members: + diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 0321113fa..77711016d 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -428,7 +428,7 @@ allowing people to define "custom" view predicates: :linenos: from pyramid.view import view_config - from webob import Response + from pyramid.response import Response def subpath(context, request): return request.subpath and request.subpath[0] == 'abc' @@ -1558,7 +1558,7 @@ comments take into account what we've discussed in the .. code-block:: python :linenos: - from webob import Response # explicit response objects, no TL + from pyramid.response import Response # explicit response objects, no TL from paste.httpserver import serve # explicitly WSGI def hello_world(request): # accepts a request; no request thread local reqd diff --git a/docs/glossary.rst b/docs/glossary.rst index 579d89afd..e45317dae 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -16,12 +16,12 @@ Glossary positional argument, returns a ``WebOb`` compatible request. response - An object that has three attributes: ``app_iter`` (representing an - iterable body), ``headerlist`` (representing the http headers sent - to the user agent), and ``status`` (representing the http status - string sent to the user agent). This is the interface defined for - ``WebOb`` response objects. See :ref:`webob_chapter` for - information about response objects. + An object returned by a :term:`view callable` that represents response + data returned to the requesting user agent. It must implements the + :class:`pyramid.interfaces.IResponse` interface. A response object is + typically an instance of the :class:`pyramid.response.Response` class or + a subclass such as :class:`pyramid.httpexceptions.HTTPFound`. See + :ref:`webob_chapter` for information about response objects. Repoze "Repoze" is essentially a "brand" of software developed by `Agendaless @@ -594,7 +594,7 @@ Glossary Not Found view An :term:`exception view` invoked by :app:`Pyramid` when the - developer explicitly raises a ``pyramid.exceptions.NotFound`` + developer explicitly raises a ``pyramid.httpexceptions.HTTPNotFound`` exception from within :term:`view` code or :term:`root factory` code, or when the current request doesn't match any :term:`view configuration`. :app:`Pyramid` provides a default @@ -604,7 +604,7 @@ Glossary Forbidden view An :term:`exception view` invoked by :app:`Pyramid` when the developer explicitly raises a - ``pyramid.exceptions.Forbidden`` exception from within + ``pyramid.httpexceptions.HTTPForbidden`` exception from within :term:`view` code or :term:`root factory` code, or when the :term:`view configuration` and :term:`authorization policy` found for a request disallows a particular view invocation. @@ -618,6 +618,12 @@ Glossary request processing. See :ref:`exception_views` for more information. + HTTP Exception + The set of exception classes defined in :mod:`pyramid.httpexceptions`. + These can be used to generate responses with various status codes when + raised or returned from a :term:`view callable`. See also + :ref:`http_exceptions`. + thread local A thread-local variable is one which is essentially a global variable in terms of how it is accessed and treated, however, each `thread @@ -894,5 +900,7 @@ Glossary http://docs.python.org/distutils/index.html for more information. :term:`setuptools` is actually an *extension* of the Distutils. - + exception response + A :term:`response` that is generated as the result of a raised exception + being caught by an :term:`exception view`. diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 8d0e7058c..0d50b0106 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -358,7 +358,7 @@ do so, do things "by hand". First define the view callable. :linenos: import os - from webob import Response + from pyramid.response import Response def favicon_view(request): here = os.path.dirname(__file__) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index be139ad74..8426f11fd 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -21,7 +21,7 @@ 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.exceptions.NotFound` class as the +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 @@ -31,9 +31,9 @@ method to register an "exception view": .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound from helloworld.views import notfound_view - config.add_view(notfound_view, context=NotFound) + config.add_view(notfound_view, context=HTTPNotFound) Replace ``helloworld.views.notfound_view`` with a reference to the :term:`view callable` you want to use to represent the Not Found view. @@ -42,8 +42,8 @@ Like any other view, the notfound view must accept at least a ``request`` parameter, or both ``context`` and ``request``. The ``request`` is the current :term:`request` representing the denied action. The ``context`` (if used in the call signature) will be the instance of the -:exc:`~pyramid.exceptions.NotFound` exception that caused the view to be -called. +:exc:`~pyramid.httpexceptions.HTTPNotFound` exception that caused the view to +be called. Here's some sample code that implements a minimal NotFound view callable: @@ -56,19 +56,20 @@ Here's some sample code that implements a minimal NotFound view callable: return HTTPNotFound() .. note:: When a NotFound view callable is invoked, it is passed a - :term:`request`. The ``exception`` attribute of the request will - be an instance of the :exc:`~pyramid.exceptions.NotFound` - exception that caused the not found view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - not found error was raised. This message will be different when - the ``debug_notfound`` environment setting is true than it is when - it is false. + :term:`request`. The ``exception`` attribute of the request will be an + instance of the :exc:`~pyramid.httpexceptions.HTTPNotFound` exception that + caused the not found view to be called. The value of + ``request.exception.args[0]`` will be a value explaining why the not found + error was raised. This message will be different when the + ``debug_notfound`` environment setting is true than it is when it is + false. .. warning:: When a NotFound view callable accepts an argument list as described in :ref:`request_and_context_view_definitions`, the ``context`` passed as the first argument to the view callable will be the - :exc:`~pyramid.exceptions.NotFound` exception instance. If available, the - resource context will still be available as ``request.context``. + :exc:`~pyramid.httpexceptions.HTTPNotFound` exception instance. If + available, the resource context will still be available as + ``request.context``. .. index:: single: forbidden view @@ -85,7 +86,7 @@ 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.exceptions.Forbidden` class as the +only of naming the :exc:`pyramid.httpexceptions.HTTPForbidden` class as the ``context`` of the view configuration. You can replace the forbidden view by using the @@ -96,8 +97,8 @@ view": :linenos: from helloworld.views import forbidden_view - from pyramid.exceptions import Forbidden - config.add_view(forbidden_view, context=Forbidden) + from pyramid.httpexceptions import HTTPForbidden + config.add_view(forbidden_view, context=HTTPForbidden) Replace ``helloworld.views.forbidden_view`` with a reference to the Python :term:`view callable` you want to use to represent the Forbidden view. @@ -121,13 +122,13 @@ Here's some sample code that implements a minimal forbidden view: return Response('forbidden') .. note:: When a forbidden view callable is invoked, it is passed a - :term:`request`. The ``exception`` attribute of the request will - be an instance of the :exc:`~pyramid.exceptions.Forbidden` - exception that caused the forbidden view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - forbidden was raised. This message will be different when the - ``debug_authorization`` environment setting is true than it is when - it is false. + :term:`request`. The ``exception`` attribute of the request will be an + instance of the :exc:`~pyramid.httpexceptions.HTTPForbidden` exception + that caused the forbidden view to be called. The value of + ``request.exception.args[0]`` will be a value explaining why the forbidden + was raised. This message will be different when the + ``debug_authorization`` environment setting is true than it is when it is + false. .. index:: single: request factory @@ -522,6 +523,100 @@ The default context URL generator is available for perusal as the class :term:`Pylons` GitHub Pyramid repository. .. index:: + single: IResponse + +.. _using_iresponse: + +Changing How Pyramid Treats View Responses +------------------------------------------ + +It is possible to control how Pyramid treats the result of calling a view +callable on a per-type basis by using a hook involving +:method:`pyramid.config.Configurator.add_response_adapter`. + +.. note:: This feature is new as of Pyramid 1.1. + +Pyramid, in various places, adapts the result of calling a view callable to +the :class:`~pyramid.interfaces.IResponse` interface to ensure that the +object returned by the view callable is a "true" response object. The vast +majority of time, the result of this adaptation is the result object itself, +as view callables written by "civilians" who read the narrative documentation +contained in this manual will always return something that implements the +:class:`~pyramid.interfaces.IResponse` interface. Most typically, this will +be an instance of the :class:`pyramid.response.Response` class or a subclass. +If a civilian returns a non-Response object from a view callable that isn't +configured to use a :term:`renderer`, he will typically expect the router to +raise an error. However, you can hook Pyramid in such a way that users can +return arbitrary values from a view callable by providing an adapter which +converts the arbitrary return value into something that implements +:class:`~pyramid.interfaces.IResponse`. + +For example, if you'd like to allow view callables to return bare string +objects (without requiring a a :term:`renderer` to convert a string to a +response object), you can register an adapter which converts the string to a +Response: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def string_response_adapter(s): + response = Response(s) + return response + + # config is an instance of pyramid.config.Configurator + + config.add_response_adapter(string_response_adapter, str) + +Likewise, if you want to be able to return a simplified kind of response +object from view callables, you can use the IResponse hook to register an +adapter to the more complex IResponse interface: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + class SimpleResponse(object): + def __init__(self, body): + self.body = body + + def simple_response_adapter(simple_response): + response = Response(simple_response.body) + return response + + # config is an instance of pyramid.config.Configurator + + config.add_response_adapter(simple_response_adapter, SimpleResponse) + +If you want to implement your own Response object instead of using the +:class:`pyramid.response.Response` object in any capacity at all, you'll have +to make sure the object implements every attribute and method outlined in +:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it's +marked up with ``zope.interface.implements(IResponse)``: + + from pyramid.interfaces import IResponse + from zope.interface import implements + + class MyResponse(object): + implements(IResponse) + # ... an implementation of every method and attribute + # documented in IResponse should follow ... + +When an alternate response object implementation is returned by a view +callable, if that object asserts that it implements +:class:`~pyramid.interfaces.IResponse` (via +``zope.interface.implements(IResponse)``) , an adapter needn't be registered +for the object; Pyramid will use it directly. + +An IResponse adapter for ``webob.Response`` (as opposed to +:class:`pyramid.response.Response`) is registered by Pyramid by default at +startup time, as by their nature, instances of this class (and instances of +subclasses of the class) will natively provide IResponse. The adapter +registered for ``webob.Response`` simply returns the response object. + +.. index:: single: view mapper .. _using_a_view_mapper: @@ -590,7 +685,7 @@ A user might make use of these framework components like so: # user application - from webob import Response + from pyramid.response import Response from pyramid.config import Configurator import pyramid_handlers from paste.httpserver import serve diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index b284fe73f..18cc8e539 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -72,30 +72,56 @@ When this configuration is added to an application, the which renders view return values to a :term:`JSON` response serialization. Other built-in renderers include renderers which use the :term:`Chameleon` -templating language to render a dictionary to a response. +templating language to render a dictionary to a response. Additional +renderers can be added by developers to the system as necessary (see +:ref:`adding_and_overriding_renderers`). + +Views which use a renderer and return a non-Response value can vary non-body +response attributes (such as headers and the HTTP status code) by attaching a +property to the ``request.response`` attribute See +:ref:`request_response_attr`. If the :term:`view callable` associated with a :term:`view configuration` -returns a Response object directly (an object with the attributes ``status``, -``headerlist`` and ``app_iter``), any renderer associated with the view +returns a Response object directly, any renderer associated with the view configuration is ignored, and the response is passed back to :app:`Pyramid` unchanged. For example, if your view callable returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class as a response, no renderer +:class:`pyramid.response.Response` class as a response, no renderer will be employed. .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.response import Response + from pyramid.view import view_config + + @view_config(renderer='json') + def view(request): + return Response('OK') # json renderer avoided + +Likewise for an :term:`HTTP exception` response: + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import view_config + @view_config(renderer='json') def view(request): - return HTTPFound(location='http://example.com') # any renderer avoided + return HTTPFound(location='http://example.com') # json renderer avoided -Views which use a renderer can vary non-body response attributes (such as -headers and the HTTP status code) by attaching a property to the -``request.response`` attribute See :ref:`request_response_attr`. +You can of course also return the ``request.response`` attribute instead to +avoid rendering: -Additional renderers can be added by developers to the system as necessary -(see :ref:`adding_and_overriding_renderers`). +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + @view_config(renderer='json') + def view(request): + request.response.body = 'OK' + return request.response # json renderer avoided .. index:: single: renderers (built-in) @@ -363,9 +389,34 @@ callable that uses a renderer, assign the ``status`` attribute to the request.response.status = '404 Not Found' return {'URL':request.URL} +Note that mutations of ``request.response`` in views which return a Response +object directly will have no effect unless the response object returned *is* +``request.response``. For example, the following example calls +``request.response.set_cookie``, but this call will have no effect, because a +different Response object is returned. + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def view(request): + request.response.set_cookie('abc', '123') # this has no effect + return Response('OK') # because we're returning a different response + +If you mutate ``request.response`` and you'd like the mutations to have an +effect, you must return ``request.response``: + +.. code-block:: python + :linenos: + + def view(request): + request.response.set_cookie('abc', '123') + return request.response + For more information on attributes of the request, see the API documentation in :ref:`request_module`. For more information on the API of -``request.response``, see :class:`pyramid.response.Response`. +``request.response``, see :attr:`pyramid.request.Request.response`. .. _response_prefixed_attrs: diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 11f84d4ea..0812f7ec7 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -77,40 +77,37 @@ processing? #. A :class:`~pyramid.events.ContextFound` :term:`event` is sent to any subscribers. -#. :app:`Pyramid` looks up a :term:`view` callable using the - context, the request, and the view name. If a view callable - doesn't exist for this combination of objects (based on the type of - the context, the type of the request, and the value of the view - name, and any :term:`predicate` attributes applied to the view - configuration), :app:`Pyramid` raises a - :class:`~pyramid.exceptions.NotFound` exception, which is meant - to be caught by a surrounding exception handler. +#. :app:`Pyramid` looks up a :term:`view` callable using the context, the + request, and the view name. If a view callable doesn't exist for this + combination of objects (based on the type of the context, the type of the + request, and the value of the view name, and any :term:`predicate` + attributes applied to the view configuration), :app:`Pyramid` raises a + :class:`~pyramid.httpexceptions.HTTPNotFound` exception, which is meant to + be caught by a surrounding :term:`exception view`. #. If a view callable was found, :app:`Pyramid` attempts to call the view function. -#. If an :term:`authorization policy` is in use, and the view was - protected by a :term:`permission`, :app:`Pyramid` passes the - context, the request, and the view_name to a function which - determines whether the view being asked for can be executed by the - requesting user, based on credential information in the request and - security information attached to the context. If it returns - ``True``, :app:`Pyramid` calls the view callable to obtain a - response. If it returns ``False``, it raises a - :class:`~pyramid.exceptions.Forbidden` exception, which is meant - to be called by a surrounding exception handler. +#. If an :term:`authorization policy` is in use, and the view was protected + by a :term:`permission`, :app:`Pyramid` passes the context, the request, + and the view_name to a function which determines whether the view being + asked for can be executed by the requesting user, based on credential + information in the request and security information attached to the + context. If it returns ``True``, :app:`Pyramid` calls the view callable + to obtain a response. If it returns ``False``, it raises a + :class:`~pyramid.httpexceptions.HTTPForbidden` exception, which is meant + to be called by a surrounding :term:`exception view`. #. If any exception was raised within a :term:`root factory`, by - :term:`traversal`, by a :term:`view callable` or by - :app:`Pyramid` itself (such as when it raises - :class:`~pyramid.exceptions.NotFound` or - :class:`~pyramid.exceptions.Forbidden`), the router catches the - exception, and attaches it to the request as the ``exception`` - attribute. It then attempts to find a :term:`exception view` for - the exception that was caught. If it finds an exception view - callable, that callable is called, and is presumed to generate a - response. If an :term:`exception view` that matches the exception - cannot be found, the exception is reraised. + :term:`traversal`, by a :term:`view callable` or by :app:`Pyramid` itself + (such as when it raises :class:`~pyramid.httpexceptions.HTTPNotFound` or + :class:`~pyramid.httpexceptions.HTTPForbidden`), the router catches the + exception, and attaches it to the request as the ``exception`` attribute. + It then attempts to find a :term:`exception view` for the exception that + was caught. If it finds an exception view callable, that callable is + called, and is presumed to generate a response. If an :term:`exception + view` that matches the exception cannot be found, the exception is + reraised. #. The following steps occur only when a :term:`response` could be successfully generated by a normal :term:`view callable` or an @@ -118,9 +115,9 @@ processing? any :term:`response callback` functions attached via :meth:`~pyramid.request.Request.add_response_callback`. A :class:`~pyramid.events.NewResponse` :term:`event` is then sent to any - subscribers. The response object's ``app_iter``, ``status``, and - ``headerlist`` attributes are then used to generate a WSGI response. The - response is sent back to the upstream WSGI server. + subscribers. The response object's ``__call__`` method is then used to + generate a WSGI response. The response is sent back to the upstream WSGI + server. #. :app:`Pyramid` will attempt to execute any :term:`finished callback` functions attached via diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index bd45388c2..05e851fde 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -191,11 +191,11 @@ function. :linenos: from pyramid.security import has_permission - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden def view_fn(request): if not has_permission('edit', request.context, request): - raise Forbidden + raise HTTPForbidden return {'greeting':'hello'} Without doing anything special during a unit test, the call to @@ -207,7 +207,7 @@ application registry is not created and populated (e.g. by initializing the configurator with an authorization policy), like when you invoke application code via a unit test, :app:`Pyramid` API functions will tend to either fail or return default results. So how do you test the branch of the code in this -view function that raises :exc:`Forbidden`? +view function that raises :exc:`HTTPForbidden`? The testing API provided by :app:`Pyramid` allows you to simulate various application registry registrations for use under a unit testing framework @@ -230,16 +230,15 @@ without needing to invoke the actual application configuration implied by its testing.tearDown() def test_view_fn_forbidden(self): - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=False) request = testing.DummyRequest() request.context = testing.DummyResource() - self.assertRaises(Forbidden, view_fn, request) + self.assertRaises(HTTPForbidden, view_fn, request) def test_view_fn_allowed(self): - from pyramid.exceptions import Forbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=True) @@ -265,7 +264,7 @@ We call the function being tested with the manufactured request. When the function is called, :func:`pyramid.security.has_permission` will call the "dummy" authentication policy we've registered through :meth:`~pyramid.config.Configuration.testing_securitypolicy`, which denies -access. We check that the view function raises a :exc:`Forbidden` error. +access. We check that the view function raises a :exc:`HTTPForbidden` error. The second test method, named ``test_view_fn_allowed`` tests the alternate case, where the authentication policy allows access. Notice that we pass diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 5df1eb3af..f94ed3ba8 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -917,7 +917,7 @@ the application's startup configuration, adding the following stanza: :linenos: config.add_view('pyramid.view.append_slash_notfound_view', - context='pyramid.exceptions.NotFound') + 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 @@ -945,14 +945,14 @@ view as the first argument to its constructor. For instance: .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): return HTTPNotFound('It aint there, stop trying!') custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=NotFound) + config.add_view(custom_append_slash, context=HTTPNotFound) The ``notfound_view`` supplied must adhere to the two-argument view callable calling convention of ``(context, request)`` (``context`` will be the diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 5c9bd91af..e3d0a37e5 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -51,14 +51,14 @@ the request object contains everything your application needs to know about the specific HTTP request being made. A view callable's ultimate responsibility is to create a :mod:`Pyramid` -:term:`Response` object. This can be done by creating the response -object in the view callable code and returning it directly, as we will -be doing in this chapter. However, if a view callable does not return a -response itself, it can be configured to use a :term:`renderer` that -converts its return value into a :term:`Response` object. Using -renderers is the common way that templates are used with view callables -to generate markup. See the :ref:`renderers_chapter` chapter for -details. +:term:`Response` object. This can be done by creating the response object in +the view callable code and returning it directly, as we will be doing in this +chapter. However, if a view callable does not return a response itself, it +can be configured to use a :term:`renderer` that converts its return value +into a :term:`Response` object. Using renderers is the common way that +templates are used with view callables to generate markup: see the +:ref:`renderers_chapter` chapter for details. In some cases, a response may +also be generated by raising an exception within a view callable. .. index:: single: view calling convention @@ -230,112 +230,130 @@ implements the :term:`Response` interface is to return a def view(request): return Response('OK') -You don't need to always use :class:`~pyramid.response.Response` to represent -a response. :app:`Pyramid` provides a range of different "exception" classes -which can act as response objects too. For example, an instance of the class -:class:`pyramid.httpexceptions.HTTPFound` is also a valid response object -(see :ref:`http_redirect`). A view can actually return any object that has -the following attributes. +:app:`Pyramid` provides a range of different "exception" classes which +inherit from :class:`pyramid.response.Response`. For example, an instance of +the class :class:`pyramid.httpexceptions.HTTPFound` is also a valid response +object because it inherits from :class:`~pyramid.response.Response`. For +examples, see :ref:`http_exceptions` and ref:`http_redirect`. -status - The HTTP status code (including the name) for the response as a string. - E.g. ``200 OK`` or ``401 Unauthorized``. +You can also return objects from view callables that aren't instances of (or +instances of classes which are subclasses of) +:class:`pyramid.response.Response` in various circumstances. This can be +helpful when writing tests and when attempting to share code between view +callables. See :ref:`renderers_chapter` for the common way to allow for +this. A much less common way to allow for view callables to return +non-Response objects is documented in :ref:`using_iresponse`. -headerlist - A sequence of tuples representing the list of headers that should be - set in the response. E.g. ``[('Content-Type', 'text/html'), - ('Content-Length', '412')]`` +.. index:: + single: view exceptions -app_iter - An iterable representing the body of the response. This can be a - list, e.g. ``['<html><head></head><body>Hello - world!</body></html>']`` or it can be a file-like object, or any - other sort of iterable. +.. _special_exceptions_in_callables: + +Using Special Exceptions In View Callables +------------------------------------------ + +Usually when a Python exception is raised within a view callable, +:app:`Pyramid` allows the exception to propagate all the way out to the +:term:`WSGI` server which invoked the application. It is usually caught and +logged there. -These attributes form the structure of the "Pyramid Response interface". +However, for convenience, a special set of exceptions exists. When one of +these exceptions is raised within a view callable, it will always cause +:app:`Pyramid` to generate a response. These are known as :term:`HTTP +exception` objects. .. index:: - single: view http redirect - single: http redirect (from a view) + single: HTTP exceptions -.. _http_redirect: +.. _http_exceptions: -Using a View Callable to Do an HTTP Redirect --------------------------------------------- +HTTP Exceptions +~~~~~~~~~~~~~~~ + +All classes documented in the :mod:`pyramid.httpexceptions` module documented +as inheriting from the :class:`pryamid.httpexceptions.HTTPException` are +:term:`http exception` objects. An instances of an HTTP exception object may +either be *returned* or *raised* from within view code. In either case +(return or raise) the instance will be used as as the view's response. -You can issue an HTTP redirect from within a view by returning a particular -kind of response. +For example, the :class:`pyramid.httpexceptions.HTTPUnauthorized` exception +can be raised. This will cause a response to be generated with a ``401 +Unauthorized`` status: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.httpexceptions import HTTPUnauthorized - def myview(request): - return HTTPFound(location='http://example.com') + def aview(request): + raise HTTPUnauthorized() -All exception types from the :mod:`pyramid.httpexceptions` module implement -the :term:`Response` interface; any can be returned as the response from a -view. See :mod:`pyramid.httpexceptions` for the documentation for the -``HTTPFound`` exception; it also includes other response types that imply -other HTTP response codes, such as ``HTTPUnauthorized`` for ``401 -Unauthorized``. +An HTTP exception, instead of being raised, can alternately be *returned* +(HTTP exceptions are also valid response objects): -.. note:: +.. code-block:: python + :linenos: - Although exception types from the :mod:`pyramid.httpexceptions` module are - in fact bona fide Python :class:`Exception` types, the :app:`Pyramid` view - machinery expects them to be *returned* by a view callable rather than - *raised*. + from pyramid.httpexceptions import HTTPUnauthorized - It is possible, however, in Python 2.5 and above, to configure an - *exception view* to catch these exceptions, and return an appropriate - :class:`~pyramid.response.Response`. The simplest such view could just - catch and return the original exception. See :ref:`exception_views` for - more details. + def aview(request): + return HTTPUnauthorized() -.. index:: - single: view exceptions +A shortcut for creating an HTTP exception is the +:func:`pyramid.httpexceptions.responsecode` function. This function accepts +an HTTP status code and returns the corresponding HTTP exception. For +example, instead of importing and constructing a +:class:`~pyramid.httpexceptions.HTTPUnauthorized` response object, you can +use the :func:`~pyramid.httpexceptions.responsecode` function to construct +and return the same object. -.. _special_exceptions_in_callables: +.. code-block:: python + :linenos: -Using Special Exceptions In View Callables ------------------------------------------- + from pyramid.httpexceptions import responsecode -Usually when a Python exception is raised within a view callable, -:app:`Pyramid` allows the exception to propagate all the way out to the -:term:`WSGI` server which invoked the application. + def aview(request): + raise responsecode(401) + +This is the case because ``401`` is the HTTP status code for "HTTP +Unauthorized". Therefore, ``raise responsecode(401)`` is functionally +equivalent to ``raise HTTPUnauthorized()``. Documentation which maps each +HTTP response code to its purpose and its associated HTTP exception object is +provided within :mod:`pyramid.httpexceptions`. -However, for convenience, two special exceptions exist which are always -handled by :app:`Pyramid` itself. These are -:exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden`. -Both are exception classes which accept a single positional constructor -argument: a ``message``. +How Pyramid Uses HTTP Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If :exc:`~pyramid.exceptions.NotFound` is raised within view code, the result -of the :term:`Not Found View` will be returned to the user agent which -performed the request. +HTTP exceptions are meant to be used directly by application application +developers. However, Pyramid itself will raise two HTTP exceptions at +various points during normal operations: +:exc:`pyramid.httpexceptions.HTTPNotFound` and +:exc:`pyramid.httpexceptions.HTTPForbidden`. Pyramid will raise the +:exc:`~pyramid.httpexceptions.HTTPNotFound` exception are raised when it +cannot find a view to service a request. Pyramid will raise the +:exc:`~pyramid.httpexceptions.Forbidden` exception or when authorization was +forbidden by a security policy. -If :exc:`~pyramid.exceptions.Forbidden` is raised within view code, the result -of the :term:`Forbidden View` will be returned to the user agent which -performed the request. +If :exc:`~pyramid.httpexceptions.HTTPNotFound` is raised by Pyramid itself or +within view code, the result of the :term:`Not Found View` will be returned +to the user agent which performed the request. -In all cases, the message provided to the exception constructor is made -available to the view which :app:`Pyramid` invokes as -``request.exception.args[0]``. +If :exc:`~pyramid.httpexceptions.HTTPForbidden` is raised by Pyramid itself +within view code, the result of the :term:`Forbidden View` will be returned +to the user agent which performed the request. .. index:: single: exception views .. _exception_views: -Exception Views ---------------- +Custom Exception Views +---------------------- -The machinery which allows the special :exc:`~pyramid.exceptions.NotFound` and -:exc:`~pyramid.exceptions.Forbidden` exceptions to be caught by specialized -views as described in :ref:`special_exceptions_in_callables` can also be used -by application developers to convert arbitrary exceptions to responses. +The machinery which allows HTTP exceptions to be raised and caught by +specialized views as described in :ref:`special_exceptions_in_callables` can +also be used by application developers to convert arbitrary exceptions to +responses. To register a view that should be called whenever a particular exception is raised from with :app:`Pyramid` view code, use the exception class or one of @@ -359,6 +377,7 @@ raises a ``helloworld.exceptions.ValidationFailure`` exception: .. code-block:: python :linenos: + from pyramid.view import view_config from helloworld.exceptions import ValidationFailure @view_config(context=ValidationFailure) @@ -370,8 +389,8 @@ raises a ``helloworld.exceptions.ValidationFailure`` exception: Assuming that a :term:`scan` was run to pick up this view registration, this view callable will be invoked whenever a ``helloworld.exceptions.ValidationFailure`` is raised by your application's -view code. The same exception raised by a custom root factory or a custom -traverser is also caught and hooked. +view code. The same exception raised by a custom root factory, a custom +traverser, or a custom view or route predicate is also caught and hooked. Other normal view predicates can also be used in combination with an exception view registration: @@ -380,12 +399,13 @@ exception view registration: :linenos: from pyramid.view import view_config - from pyramid.exceptions import NotFound - from pyramid.httpexceptions import HTTPNotFound + from helloworld.exceptions import ValidationFailure - @view_config(context=NotFound, route_name='home') - def notfound_view(request): - return HTTPNotFound() + @view_config(context=ValidationFailure, route_name='home') + def failed_validation(exc, request): + response = Response('Failed validation: %s' % exc.msg) + response.status_int = 500 + return response The above exception view names the ``route_name`` of ``home``, meaning that it will only be called when the route matched has a name of ``home``. You @@ -407,7 +427,45 @@ exception views which have a name will be ignored. can use an exception as ``context`` for a normal view. Exception views can be configured with any view registration mechanism: -``@view_config`` decorator, ZCML, or imperative ``add_view`` styles. +``@view_config`` decorator or imperative ``add_view`` styles. + +.. index:: + single: view http redirect + single: http redirect (from a view) + +.. _http_redirect: + +Using a View Callable to Do an HTTP Redirect +-------------------------------------------- + +You can issue an HTTP redirect by using the +:class:`pyramid.httpexceptions.HTTPFound` class. Raising or returning an +instance of this class will cause the client to receive a "302 Found" +response. + +To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` +instance. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + return HTTPFound(location='http://example.com') + +Alternately, you can *raise* an HTTPFound exception instead of returning one. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + raise HTTPFound(location='http://example.com') + +When the instance is raised, it is caught by the default :term:`exception +response` handler and turned into a response. .. index:: single: unicode, views, and forms diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 072ca1c74..0ff8e1de7 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -10,15 +10,15 @@ Request and Response Objects .. note:: This chapter is adapted from a portion of the :term:`WebOb` documentation, originally written by Ian Bicking. -:app:`Pyramid` uses the :term:`WebOb` package to supply +:app:`Pyramid` uses the :term:`WebOb` package as a basis for its :term:`request` and :term:`response` object implementations. The -:term:`request` object that is passed to a :app:`Pyramid` -:term:`view` is an instance of the :class:`pyramid.request.Request` -class, which is a subclass of :class:`webob.Request`. The -:term:`response` returned from a :app:`Pyramid` :term:`view` -:term:`renderer` is an instance of the :mod:`webob.Response` class. -Users can also return an instance of :mod:`webob.Response` directly -from a view as necessary. +:term:`request` object that is passed to a :app:`Pyramid` :term:`view` is an +instance of the :class:`pyramid.request.Request` class, which is a subclass +of :class:`webob.Request`. The :term:`response` returned from a +:app:`Pyramid` :term:`view` :term:`renderer` is an instance of the +:mod:`pyramid.response.Response` class, which is a subclass of the +:class:`webob.Response` class. Users can also return an instance of +:class:`pyramid.response.Response` directly from a view as necessary. WebOb is a project separate from :app:`Pyramid` with a separate set of authors and a fully separate `set of documentation @@ -26,16 +26,15 @@ authors and a fully separate `set of documentation standard WebOb request, which is documented in the :ref:`request_module` API documentation. -WebOb provides objects for HTTP requests and responses. Specifically -it does this by wrapping the `WSGI <http://wsgi.org>`_ request -environment and response status/headers/app_iter (body). +WebOb provides objects for HTTP requests and responses. Specifically it does +this by wrapping the `WSGI <http://wsgi.org>`_ request environment and +response status, header list, and app_iter (body) values. -WebOb request and response objects provide many conveniences for -parsing WSGI requests and forming WSGI responses. WebOb is a nice way -to represent "raw" WSGI requests and responses; however, we won't -cover that use case in this document, as users of :app:`Pyramid` -don't typically need to use the WSGI-related features of WebOb -directly. The `reference documentation +WebOb request and response objects provide many conveniences for parsing WSGI +requests and forming WSGI responses. WebOb is a nice way to represent "raw" +WSGI requests and responses; however, we won't cover that use case in this +document, as users of :app:`Pyramid` don't typically need to use the +WSGI-related features of WebOb directly. The `reference documentation <http://pythonpaste.org/webob/reference.html>`_ shows many examples of creating requests and using response objects in this manner, however. @@ -170,9 +169,9 @@ of the request. I'll show various values for an example URL Methods +++++++ -There are `several methods -<http://pythonpaste.org/webob/class-webob.Request.html#__init__>`_ but -only a few you'll use often: +There are methods of request objects documented in +:class:`pyramid.request.Request` but you'll find that you won't use very many +of them. Here are a couple that might be useful: ``Request.blank(base_url)``: Creates a new request with blank information, based at the given @@ -183,9 +182,9 @@ only a few you'll use often: subrequests). ``req.get_response(wsgi_application)``: - This method calls the given WSGI application with this request, - and returns a `Response`_ object. You can also use this for - subrequests, or testing. + This method calls the given WSGI application with this request, and + returns a :class:`pyramid.response.Response` object. You can also use + this for subrequests, or testing. .. index:: single: request (and unicode) @@ -259,8 +258,10 @@ Response ~~~~~~~~ The :app:`Pyramid` response object can be imported as -:class:`pyramid.response.Response`. This import location is merely a facade -for its original location: ``webob.Response``. +:class:`pyramid.response.Response`. This class is a subclass of the +``webob.Response`` class. The subclass does not add or change any +functionality, so the WebOb Response documentation will be completely +relevant for this class as well. A response object has three fundamental parts: @@ -283,8 +284,8 @@ A response object has three fundamental parts: ``response.body_file`` (a file-like object; writing to it appends to ``app_iter``). -Everything else in the object derives from this underlying state. -Here's the highlights: +Everything else in the object typically derives from this underlying state. +Here are some highlights: ``response.content_type`` The content type *not* including the ``charset`` parameter. @@ -359,16 +360,18 @@ Exception Responses +++++++++++++++++++ To facilitate error responses like ``404 Not Found``, the module -:mod:`webob.exc` contains classes for each kind of error response. These -include boring, but appropriate error bodies. The exceptions exposed by this -module, when used under :app:`Pyramid`, should be imported from the -:mod:`pyramid.httpexceptions` "facade" module. This import location is merely -a facade for the original location of these exceptions: ``webob.exc``. - -Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the reason -for the error. For instance, :class:`pyramid.httpexceptions.HTTPNotFound`. It -subclasses :class:`pyramid.Response`, so you can manipulate the instances in -the same way. A typical example is: +:mod:`pyramid.httpexceptions` contains classes for each kind of error +response. These include boring, but appropriate error bodies. The +exceptions exposed by this module, when used under :app:`Pyramid`, should be +imported from the :mod:`pyramid.httpexceptions` module. This import location +contains subclasses and replacements that mirror those in the ``webob.exc`` +module. + +Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the +reason for the error. For instance, +:class:`pyramid.httpexceptions.HTTPNotFound` subclasses +:class:`pyramid.Response`, so you can manipulate the instances in the same +way. A typical example is: .. ignore-next-block .. code-block:: python @@ -381,33 +384,11 @@ the same way. A typical example is: # or: response = HTTPMovedPermanently(location=new_url) -These are not exceptions unless you are using Python 2.5+, because -they are new-style classes which are not allowed as exceptions until -Python 2.5. To get an exception object use ``response.exception``. -You can use this like: - -.. code-block:: python - :linenos: - - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import HTTPNotFound - - def aview(request): - try: - # ... stuff ... - raise HTTPNotFound('No such resource').exception - except HTTPException, e: - return request.get_response(e) - -The exceptions are still WSGI applications, but you cannot set -attributes like ``content_type``, ``charset``, etc. on these exception -objects. - More Details ++++++++++++ More details about the response object API are available in the -:mod:`pyramid.response` documentation. More details about exception responses -are in the :mod:`pyramid.httpexceptions` API documentation. The `WebOb -documentation <http://pythonpaste.org/webob>`_ is also useful. +:mod:`pyramid.response` documentation. More details about exception +responses are in the :mod:`pyramid.httpexceptions` API documentation. The +`WebOb documentation <http://pythonpaste.org/webob>`_ is also useful. diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 358c1d5eb..46c953f6d 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -145,17 +145,17 @@ 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.exceptions.Forbidden``) 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. +``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. Changing Existing Views ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index ae4fa6ffb..b111c9b4a 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -84,10 +84,12 @@ No renderer is necessary when a view returns a response object. The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` -API. :func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` -page resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the -"location" of the HTTPFound response, forming an HTTP redirect. +the :class:`pyramid.interfaces.IResponse` interface like +:class:`pyramid.response.Response` does). The +:func:`pyramid.url.resource_url` API. :func:`pyramid.url.resource_url` +constructs a URL to the ``FrontPage`` page resource +(e.g. ``http://localhost:6543/FrontPage``), and uses it as the "location" of +the HTTPFound response, forming an HTTP redirect. The ``view_page`` view function ------------------------------- diff --git a/docs/tutorials/wiki/src/authorization/tutorial/login.py b/docs/tutorials/wiki/src/authorization/tutorial/login.py index 463db71a6..334115880 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/login.py @@ -9,7 +9,7 @@ from tutorial.security import USERS @view_config(context='tutorial.models.Wiki', name='login', renderer='templates/login.pt') -@view_config(context='pyramid.exceptions.Forbidden', +@view_config(context='pyramid.httpexceptions.HTTPForbidden', renderer='templates/login.pt') def login(request): login_url = resource_url(request.context, request, 'login') diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index cea376b77..c91d1d914 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -91,7 +91,8 @@ a URL which represents the path to our "FrontPage". The ``view_wiki`` function returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface), It will use the +the :class:`pyramid.interfaces.IResponse` interface like +:class:`pyramid.response.Response` does), It will use the :func:`pyramid.url.route_url` API to construct a URL to the ``FrontPage`` page (e.g. ``http://localhost:6543/FrontPage``), and will use it as the "location" of the HTTPFound response, forming an HTTP redirect. diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py index 05183d3d4..4cd84eda5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -39,7 +39,7 @@ def main(global_config, **settings): config.add_view('tutorial.views.edit_page', route_name='edit_page', renderer='tutorial:templates/edit.pt', permission='edit') config.add_view('tutorial.login.login', - context='pyramid.exceptions.Forbidden', + context='pyramid.httpexceptions.HTTPForbidden', renderer='tutorial:templates/login.pt') return config.make_wsgi_app() diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index ce2f7210a..172a20343 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -18,6 +18,9 @@ The major feature additions in Pyramid 1.1 are: - Support for "static" routes. +- Default HTTP exception view and associated ``redirect`` and ``abort`` + convenience functions. + ``request.response`` ~~~~~~~~~~~~~~~~~~~~ @@ -50,6 +53,25 @@ Static Routes be useful for URL generation via ``route_url`` and ``route_path``. See the section entitled :ref:`static_route_narr` for more information. +Default HTTP Exception View +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- A default exception view for the context :exc:`webob.exc.HTTPException` + (aka :class:`pyramid.httpexceptions.HTTPException`) is now registered by + default. This means that an instance of any exception class imported from + :mod:`pyramid.httpexceptions` (such as ``HTTPFound``) can now be raised + from within view code; when raised, this exception view will render the + exception to a response. + + To allow for configuration of this feature, the :term:`Configurator` now + accepts an additional keyword argument named ``httpexception_view``. By + default, this argument is populated with a default exception view function + that will be used when an HTTP exception is raised. When ``None`` is + passed for this value, an exception view for HTTP exceptions will not be + registered. Passing ``None`` returns the behavior of raising an HTTP + exception to that of Pyramid 1.0 (the exception will propagate to + middleware and to the WSGI server). + Minor Feature Additions ----------------------- @@ -57,6 +79,10 @@ Minor Feature Additions :class:`pyramid.authentication.SessionAuthenticationPolicy`, which uses a session to store credentials. +- A function named :func:`pyramid.httpexceptions.responsecode` is a shortcut + that can be used to create HTTP exception response objects using an HTTP + integer status code. + - Integers and longs passed as ``elements`` to :func:`pyramid.url.resource_url` or :meth:`pyramid.request.Request.resource_url` e.g. ``resource_url(context, @@ -162,7 +188,7 @@ Deprecations and Behavior Differences expected an environ object in BFG 1.0 and before). In a future version, these methods will be removed entirely. -- A custom request factory is now required to return a response object that +- A custom request factory is now required to return a request object that has a ``response`` attribute (or "reified"/lazy property) if they the request is meant to be used in a view that uses a renderer. This ``response`` attribute should be an instance of the class @@ -235,3 +261,10 @@ Documentation Enhancements - Added a section to the "URL Dispatch" narrative chapter regarding the new "static" route feature entitled :ref:`static_route_narr`. + +- Added API docs for :func:`pyramid.httpexceptions.abort` and + :func:`pyramid.httpexceptions.redirect`. + +- Added :ref:`http_exceptions` section to Views narrative chapter including a + description of :func:`pyramid.httpexceptions.abort`` and + :func:`pyramid.httpexceptions.redirect`. diff --git a/pyramid/__init__.py b/pyramid/__init__.py index 5f6a326f8..473d5e1c6 100644 --- a/pyramid/__init__.py +++ b/pyramid/__init__.py @@ -1,2 +1,5 @@ -# pyramid package - +from pyramid.request import Request +from pyramid.response import Response +Response.RequestClass = Request +Request.ResponseClass = Response +del Request, Response diff --git a/pyramid/config.py b/pyramid/config.py index 4e06a9b2e..70b5cd639 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -36,6 +36,7 @@ from pyramid.interfaces import IRendererFactory from pyramid.interfaces import IRendererGlobalsFactory from pyramid.interfaces import IRequest from pyramid.interfaces import IRequestFactory +from pyramid.interfaces import IResponse from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IRoutesMapper @@ -56,9 +57,10 @@ from pyramid.compat import md5 from pyramid.compat import any from pyramid.events import ApplicationCreated from pyramid.exceptions import ConfigurationError -from pyramid.exceptions import Forbidden -from pyramid.exceptions import NotFound from pyramid.exceptions import PredicateMismatch +from pyramid.httpexceptions import default_exceptionresponse_view +from pyramid.httpexceptions import HTTPForbidden +from pyramid.httpexceptions import HTTPNotFound from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger from pyramid.mako_templating import renderer_factory as mako_renderer_factory @@ -80,9 +82,7 @@ from pyramid.traversal import find_interface from pyramid.traversal import traversal_path from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver -from pyramid.view import default_exceptionresponse_view from pyramid.view import render_view_to_response -from pyramid.view import is_response DEFAULT_RENDERERS = ( ('.mak', mako_renderer_factory), @@ -139,7 +139,8 @@ class Configurator(object): ``package``, ``settings``, ``root_factory``, ``authentication_policy``, ``authorization_policy``, ``renderers`` ``debug_logger``, ``locale_negotiator``, ``request_factory``, ``renderer_globals_factory``, - ``default_permission``, ``session_factory``, and ``autocommit``. + ``default_permission``, ``session_factory``, ``default_view_mapper``, + ``autocommit``, and ``exceptionresponse_view``. If the ``registry`` argument is passed as a non-``None`` value, it must be an instance of the :class:`pyramid.registry.Registry` @@ -254,7 +255,17 @@ class Configurator(object): :term:`view mapper` factory for view configurations that don't otherwise specify one (see :class:`pyramid.interfaces.IViewMapperFactory`). If a default_view_mapper is not passed, a superdefault view mapper will be - used. """ + used. + + If ``exceptionresponse_view`` is passed, it must be a :term:`view + callable` or ``None``. If it is a view callable, it will be used as an + exception view callable when an :term:`exception response` is raised. If + ``exceptionresponse_view`` is ``None``, no exception response view will + be registered, and all raised exception responses will be bubbled up to + Pyramid's caller. By + default, the ``pyramid.httpexceptions.default_exceptionresponse_view`` + function is used as the ``exceptionresponse_view``. This argument is new + in Pyramid 1.1. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -277,6 +288,7 @@ class Configurator(object): session_factory=None, default_view_mapper=None, autocommit=False, + exceptionresponse_view=default_exceptionresponse_view, ): if package is None: package = caller_package() @@ -302,6 +314,7 @@ class Configurator(object): default_permission=default_permission, session_factory=session_factory, default_view_mapper=default_view_mapper, + exceptionresponse_view=exceptionresponse_view, ) def _set_settings(self, mapping): @@ -400,7 +413,8 @@ class Configurator(object): def _fix_registry(self): """ Fix up a ZCA component registry that is not a pyramid.registry.Registry by adding analogues of ``has_listeners``, - and ``notify`` through monkey-patching.""" + ``notify``, ``queryAdapterOrSelf``, and ``registerSelfAdapter`` + through monkey-patching.""" _registry = self.registry @@ -412,6 +426,23 @@ class Configurator(object): if not hasattr(_registry, 'has_listeners'): _registry.has_listeners = True + if not hasattr(_registry, 'queryAdapterOrSelf'): + def queryAdapterOrSelf(object, interface, default=None): + if not interface.providedBy(object): + return _registry.queryAdapter(object, interface, + default=default) + return object + _registry.queryAdapterOrSelf = queryAdapterOrSelf + + if not hasattr(_registry, 'registerSelfAdapter'): + def registerSelfAdapter(required=None, provided=None, + name=u'', info=u'', event=True): + return _registry.registerAdapter(lambda x: x, + required=required, + provided=provided, name=name, + info=info, event=event) + _registry.registerSelfAdapter = registerSelfAdapter + def _make_context(self, autocommit=False): context = PyramidConfigurationMachine() registerCommonDirectives(context) @@ -658,7 +689,8 @@ class Configurator(object): renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, renderer_globals_factory=None, default_permission=None, - session_factory=None, default_view_mapper=None): + session_factory=None, default_view_mapper=None, + exceptionresponse_view=default_exceptionresponse_view): """ 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 @@ -679,6 +711,9 @@ class Configurator(object): self._fix_registry() self._set_settings(settings) self._set_root_factory(root_factory) + # cope with WebOb response objects that aren't decorated with IResponse + from webob import Response as WebobResponse + registry.registerSelfAdapter((WebobResponse,), IResponse) debug_logger = self.maybe_dotted(debug_logger) if debug_logger is None: debug_logger = make_stream_logger('pyramid.debug', sys.stderr) @@ -688,8 +723,9 @@ class Configurator(object): authorization_policy) for name, renderer in renderers: self.add_renderer(name, renderer) - self.add_view(default_exceptionresponse_view, - context=IExceptionResponse) + if exceptionresponse_view is not None: + exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) + self.add_view(exceptionresponse_view, context=IExceptionResponse) if locale_negotiator: locale_negotiator = self.maybe_dotted(locale_negotiator) registry.registerUtility(locale_negotiator, ILocaleNegotiator) @@ -705,7 +741,7 @@ class Configurator(object): if session_factory is not None: self.set_session_factory(session_factory) # commit before adding default_view_mapper, as the - # default_exceptionresponse_view above requires the superdefault view + # exceptionresponse_view above requires the superdefault view # mapper self.commit() if default_view_mapper is not None: @@ -851,6 +887,30 @@ class Configurator(object): self.action(None, register) return subscriber + @action_method + def add_response_adapter(self, adapter, type_or_iface): + """ When an object of type (or interface) ``type_or_iface`` is + returned from a view callable, Pyramid will use the adapter + ``adapter`` to convert it into an object which implements the + :class:`pyramid.interfaces.IResponse` interface. If ``adapter`` is + None, an object returned of type (or interface) ``type_or_iface`` + will itself be used as a response object. + + ``adapter`` and ``type_or_interface`` may be Python objects or + strings representing dotted names to importable Python global + objects. + + See :ref:`using_iresponse` for more information.""" + adapter = self.maybe_dotted(adapter) + type_or_iface = self.maybe_dotted(type_or_iface) + def register(): + reg = self.registry + if adapter is None: + reg.registerSelfAdapter((type_or_iface,), IResponse) + else: + reg.registerAdapter(adapter, (type_or_iface,), IResponse) + self.action((IResponse, type_or_iface), register) + def add_settings(self, settings=None, **kw): """Augment the ``settings`` argument passed in to the Configurator constructor with one or more 'setting' key/value pairs. A setting is @@ -1978,7 +2038,8 @@ class Configurator(object): def bwcompat_view(context, request): context = getattr(request, 'context', None) return view(context, request) - return self.add_view(bwcompat_view, context=Forbidden, wrapper=wrapper) + return self.add_view(bwcompat_view, context=HTTPForbidden, + wrapper=wrapper) @action_method def set_notfound_view(self, view=None, attr=None, renderer=None, @@ -2018,7 +2079,8 @@ class Configurator(object): def bwcompat_view(context, request): context = getattr(request, 'context', None) return view(context, request) - return self.add_view(bwcompat_view, context=NotFound, wrapper=wrapper) + return self.add_view(bwcompat_view, context=HTTPNotFound, + wrapper=wrapper) @action_method def set_request_factory(self, factory): @@ -2826,7 +2888,7 @@ class ViewDeriver(object): return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg, result) + raise HTTPForbidden(msg, result=result) _secured_view.__call_permissive__ = view _secured_view.__permitted__ = _permitted _secured_view.__permission__ = permission @@ -2875,7 +2937,8 @@ class ViewDeriver(object): def predicate_wrapper(context, request): if all((predicate(context, request) for predicate in predicates)): return view(context, request) - raise PredicateMismatch('predicate mismatch for view %s' % view) + raise PredicateMismatch( + 'predicate mismatch for view %s' % view) def checker(context, request): return all((predicate(context, request) for predicate in predicates)) @@ -2920,22 +2983,24 @@ class ViewDeriver(object): def _rendered_view(context, request): renderer = static_renderer - response = wrapped_view(context, request) - if not is_response(response): + result = wrapped_view(context, request) + registry = self.kw['registry'] + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: attrs = getattr(request, '__dict__', {}) if 'override_renderer' in attrs: # renderer overridden by newrequest event or other renderer_name = attrs.pop('override_renderer') renderer = RendererHelper(name=renderer_name, package=self.kw.get('package'), - registry = self.kw['registry']) + registry = registry) if '__view__' in attrs: view_inst = attrs.pop('__view__') else: view_inst = getattr(wrapped_view, '__original_view__', wrapped_view) - return renderer.render_view(request, response, view_inst, - context) + response = renderer.render_view(request, result, view_inst, + context) return response return _rendered_view diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 771d71b88..151fc241f 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,84 +1,12 @@ from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import implements -from pyramid.decorator import reify -from pyramid.interfaces import IExceptionResponse -import cgi +from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import HTTPForbidden -class ExceptionResponse(Exception): - """ Abstract class to support behaving as a WSGI response object """ - implements(IExceptionResponse) - status = None +NotFound = HTTPNotFound # bw compat +Forbidden = HTTPForbidden # bw compat - def __init__(self, message=''): - Exception.__init__(self, message) # B / C - self.message = message - - @reify # defer execution until asked explicitly - def app_iter(self): - return [ - """ - <html> - <title>%s</title> - <body> - <h1>%s</h1> - <code>%s</code> - </body> - </html> - """ % (self.status, self.status, cgi.escape(self.message)) - ] - - @reify # defer execution until asked explicitly - def headerlist(self): - return [ - ('Content-Length', str(len(self.app_iter[0]))), - ('Content-Type', 'text/html') - ] - - -class Forbidden(ExceptionResponse): - """ - Raise this exception within :term:`view` code to immediately return the - :term:`forbidden view` to the invoking user. Usually this is a basic - ``403`` page, but the forbidden view can be customized as necessary. See - :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be - the ``context`` of a :term:`Forbidden View`. - - This exception's constructor accepts two arguments. The first argument, - ``message``, should be a string. The value of this string will be used - as the ``message`` attribute of the exception object. The second - argument, ``result`` is usually an instance of - :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` - each of which indicates a reason for the forbidden error. However, - ``result`` is also permitted to be just a plain boolean ``False`` object. - The ``result`` value will be used as the ``result`` attribute of the - exception object. - - The :term:`Forbidden View` can use the attributes of a Forbidden - exception as necessary to provide extended information in an error - report shown to a user. - """ - status = '403 Forbidden' - def __init__(self, message='', result=None): - ExceptionResponse.__init__(self, message) - self.message = message - self.result = result - -class NotFound(ExceptionResponse): - """ - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. - - This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be available as the - ``message`` attribute of this exception, for availability to the - :term:`Not Found View`. - """ - status = '404 Not Found' - -class PredicateMismatch(NotFound): +class PredicateMismatch(HTTPNotFound): """ Internal exception (not an API) raised by multiviews when no view matches. This exception subclasses the ``NotFound`` @@ -102,3 +30,4 @@ class ConfigurationError(ZCE): """ Raised when inappropriate input values are supplied to an API method of a :term:`Configurator`""" + diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index f56910b53..6d689988e 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -1,50 +1,1005 @@ -from webob.exc import __doc__ -from webob.exc import status_map - -# Parent classes -from webob.exc import HTTPException -from webob.exc import WSGIHTTPException -from webob.exc import HTTPOk -from webob.exc import HTTPRedirection -from webob.exc import HTTPError -from webob.exc import HTTPClientError -from webob.exc import HTTPServerError - -# Child classes -from webob.exc import HTTPCreated -from webob.exc import HTTPAccepted -from webob.exc import HTTPNonAuthoritativeInformation -from webob.exc import HTTPNoContent -from webob.exc import HTTPResetContent -from webob.exc import HTTPPartialContent -from webob.exc import HTTPMultipleChoices -from webob.exc import HTTPMovedPermanently -from webob.exc import HTTPFound -from webob.exc import HTTPSeeOther -from webob.exc import HTTPNotModified -from webob.exc import HTTPUseProxy -from webob.exc import HTTPTemporaryRedirect -from webob.exc import HTTPBadRequest -from webob.exc import HTTPUnauthorized -from webob.exc import HTTPPaymentRequired -from webob.exc import HTTPForbidden -from webob.exc import HTTPNotFound -from webob.exc import HTTPMethodNotAllowed -from webob.exc import HTTPNotAcceptable -from webob.exc import HTTPProxyAuthenticationRequired -from webob.exc import HTTPRequestTimeout -from webob.exc import HTTPConflict -from webob.exc import HTTPGone -from webob.exc import HTTPLengthRequired -from webob.exc import HTTPPreconditionFailed -from webob.exc import HTTPRequestEntityTooLarge -from webob.exc import HTTPRequestURITooLong -from webob.exc import HTTPUnsupportedMediaType -from webob.exc import HTTPRequestRangeNotSatisfiable -from webob.exc import HTTPExpectationFailed -from webob.exc import HTTPInternalServerError -from webob.exc import HTTPNotImplemented -from webob.exc import HTTPBadGateway -from webob.exc import HTTPServiceUnavailable -from webob.exc import HTTPGatewayTimeout -from webob.exc import HTTPVersionNotSupported +""" +HTTP Exceptions +--------------- + +This module contains Pyramid HTTP exception classes. Each class relates to a +single HTTP status code. Each class is a subclass of the +:class:`~HTTPException`. Each exception class is also a :term:`response` +object. + +Each exception class has a status code according to `RFC 2068 +<http://www.ietf.org/rfc/rfc2068.txt>`_: codes with 100-300 are not really +errors; 400s are client errors, and 500s are server errors. + +Exception + HTTPException + HTTPOk + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 306 - Unused (not implemented, obviously) + * 307 - HTTPTemporaryRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + +Each HTTP exception has the following attributes: + + ``code`` + the HTTP status code for the exception + + ``title`` + remainder of the status line (stuff after the code) + + ``explanation`` + a plain-text explanation of the error message that is + not subject to environment or header substitutions; + it is accessible in the template via ${explanation} + + ``detail`` + a plain-text message customization that is not subject + to environment or header substitutions; accessible in + the template via ${detail} + + ``body_template`` + a ``String.template``-format content fragment used for environment + and header substitution; the default template includes both + the explanation and further detail provided in the + message. + +Each HTTP exception accepts the following parameters: + + ``detail`` + a plain-text override of the default ``detail`` + + ``headers`` + a list of (k,v) header pairs + + ``comment`` + a plain-text additional information which is + usually stripped/hidden for end-users + + ``body_template`` + a ``string.Template`` object containing a content fragment in HTML + that frames the explanation and further detail + +Substitution of response headers into template values is always performed. +Substitution of WSGI environment values is performed if a ``request`` is +passed to the exception's constructor. + +The subclasses of :class:`~_HTTPMove` +(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, +:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and +:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` +field. Reflecting this, these subclasses have one additional keyword argument: +``location``, which indicates the location to which to redirect. +""" + +import types +from string import Template + +from zope.interface import implements + +from webob import html_escape as _html_escape + +from pyramid.interfaces import IExceptionResponse +from pyramid.response import Response + +def _no_escape(value): + if value is None: + return '' + if not isinstance(value, basestring): + if hasattr(value, '__unicode__'): + value = unicode(value) + else: + value = str(value) + return value + +class HTTPException(Exception): # bw compat + """ Base class for all :term:`exception response` objects.""" + +class WSGIHTTPException(Response, HTTPException): + implements(IExceptionResponse) + + ## You should set in subclasses: + # code = 200 + # title = 'OK' + # explanation = 'why this happens' + # body_template_obj = Template('response template') + + # differences from webob.exc.WSGIHTTPException: + # + # - bases plaintext vs. html result on self.content_type rather than + # on request accept header + # + # - doesn't use "strip_tags" (${br} placeholder for <br/>, no other html + # in default body template) + # + # - sets a default app_iter onto self during __call__ using a template if + # no body, app_iter, or unicode_body is set onto the response (instead of + # the replaced version's "generate_response") + # + # - explicitly sets self.message = detail to prevent whining by Python + # 2.6.5+ access of Exception.message + # + # - its base class of HTTPException is no longer a Python 2.4 compatibility + # shim; it's purely a base class that inherits from Exception. This + # implies that this class' ``exception`` property always returns + # ``self`` (only for bw compat at this point). + # + # - documentation improvements (Pyramid-specific docstrings where necessary) + # + code = None + title = None + explanation = '' + body_template_obj = Template('''\ +${explanation}${br}${br} +${detail} +${html_comment} +''') + + plain_template_obj = Template('''\ +${status} + +${body}''') + + html_template_obj = Template('''\ +<html> + <head> + <title>${status}</title> + </head> + <body> + <h1>${status}</h1> + ${body} + </body> +</html>''') + + ## Set this to True for responses that should have no request body + empty_body = False + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, **kw): + status = '%s %s' % (self.code, self.title) + Response.__init__(self, status=status, **kw) + Exception.__init__(self, detail) + self.detail = self.message = detail + if headers: + self.headers.extend(headers) + self.comment = comment + if body_template is not None: + self.body_template = body_template + self.body_template_obj = Template(body_template) + + if self.empty_body: + del self.content_type + del self.content_length + + def __str__(self): + return self.detail or self.explanation + + def _default_app_iter(self, environ): + html_comment = '' + comment = self.comment or '' + content_type = self.content_type or '' + if 'html' in content_type: + escape = _html_escape + page_template = self.html_template_obj + br = '<br/>' + if comment: + html_comment = '<!-- %s -->' % escape(comment) + else: + escape = _no_escape + page_template = self.plain_template_obj + br = '\n' + if comment: + html_comment = escape(comment) + args = { + 'br':br, + 'explanation': escape(self.explanation), + 'detail': escape(self.detail or ''), + 'comment': escape(comment), + 'html_comment':html_comment, + } + body_tmpl = self.body_template_obj + if WSGIHTTPException.body_template_obj is not body_tmpl: + # Custom template; add headers to args + for k, v in environ.items(): + args[k] = escape(v) + for k, v in self.headers.items(): + args[k.lower()] = escape(v) + body = body_tmpl.substitute(args) + page = page_template.substitute(status=self.status, body=body) + if isinstance(page, unicode): + page = page.encode(self.charset) + return [page] + + @property + def wsgi_response(self): + # bw compat only + return self + + exception = wsgi_response # bw compat only + + def __call__(self, environ, start_response): + if not self.body and not self.empty_body: + self.app_iter = self._default_app_iter(environ) + return Response.__call__(self, environ, start_response) + +class HTTPError(WSGIHTTPException): + """ + base class for exceptions with status codes in the 400s and 500s + + This is an exception which indicates that an error has occurred, + and that any work in progress should not be committed. + """ + +class HTTPRedirection(WSGIHTTPException): + """ + base class for exceptions with status codes in the 300s (redirections) + + This is an abstract base class for 3xx redirection. It indicates + that further action needs to be taken by the user agent in order + to fulfill the request. It does not necessarly signal an error + condition. + """ + +class HTTPOk(WSGIHTTPException): + """ + Base class for exceptions with status codes in the 200s (successful + responses) + + code: 200, title: OK + """ + code = 200 + title = 'OK' + +############################################################ +## 2xx success +############################################################ + +class HTTPCreated(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that request has been fulfilled and resulted in a new + resource being created. + + code: 201, title: Created + """ + code = 201 + title = 'Created' + +class HTTPAccepted(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the request has been accepted for processing, but the + processing has not been completed. + + code: 202, title: Accepted + """ + code = 202 + title = 'Accepted' + explanation = 'The request is accepted for processing.' + +class HTTPNonAuthoritativeInformation(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the returned metainformation in the entity-header is + not the definitive set as available from the origin server, but is + gathered from a local or a third-party copy. + + code: 203, title: Non-Authoritative Information + """ + code = 203 + title = 'Non-Authoritative Information' + +class HTTPNoContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the request but does + not need to return an entity-body, and might want to return updated + metainformation. + + code: 204, title: No Content + """ + code = 204 + title = 'No Content' + empty_body = True + +class HTTPResetContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the the server has fulfilled the request and + the user agent SHOULD reset the document view which caused the + request to be sent. + + code: 205, title: Reset Content + """ + code = 205 + title = 'Reset Content' + empty_body = True + +class HTTPPartialContent(HTTPOk): + """ + subclass of :class:`~HTTPOk` + + This indicates that the server has fulfilled the partial GET + request for the resource. + + code: 206, title: Partial Content + """ + code = 206 + title = 'Partial Content' + +## FIXME: add 207 Multi-Status (but it's complicated) + +############################################################ +## 3xx redirection +############################################################ + +class _HTTPMove(HTTPRedirection): + """ + redirections which require a Location field + + Since a 'Location' header is a required attribute of 301, 302, 303, + 305 and 307 (but not 304), this base class provides the mechanics to + make this easy. + + You must provide a ``location`` keyword argument. + """ + # differences from webob.exc._HTTPMove: + # + # - not a wsgi app + # + # - ${location} isn't wrapped in an <a> tag in body + # + # - location keyword arg defaults to '' + # + # - ``add_slash`` argument is no longer accepted: code that passes + # add_slash argument to the constructor will receive an exception. + explanation = 'The resource has been moved to' + body_template_obj = Template('''\ +${explanation} ${location}; +you should be redirected automatically. +${detail} +${html_comment}''') + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, location='', **kw): + super(_HTTPMove, self).__init__( + detail=detail, headers=headers, comment=comment, + body_template=body_template, location=location, **kw) + +class HTTPMultipleChoices(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource corresponds to any one + of a set of representations, each with its own specific location, + and agent-driven negotiation information is being provided so that + the user can select a preferred representation and redirect its + request to that location. + + code: 300, title: Multiple Choices + """ + code = 300 + title = 'Multiple Choices' + +class HTTPMovedPermanently(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource has been assigned a new + permanent URI and any future references to this resource SHOULD use + one of the returned URIs. + + code: 301, title: Moved Permanently + """ + code = 301 + title = 'Moved Permanently' + +class HTTPFound(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily under + a different URI. + + code: 302, title: Found + """ + code = 302 + title = 'Found' + explanation = 'The resource was found at' + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the response to the request can be found under + a different URI and SHOULD be retrieved using a GET method on that + resource. + + code: 303, title: See Other + """ + code = 303 + title = 'See Other' + +class HTTPNotModified(HTTPRedirection): + """ + subclass of :class:`~HTTPRedirection` + + This indicates that if the client has performed a conditional GET + request and access is allowed, but the document has not been + modified, the server SHOULD respond with this status code. + + code: 304, title: Not Modified + """ + # FIXME: this should include a date or etag header + code = 304 + title = 'Not Modified' + empty_body = True + +class HTTPUseProxy(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource MUST be accessed through + the proxy given by the Location field. + + code: 305, title: Use Proxy + """ + # Not a move, but looks a little like one + code = 305 + title = 'Use Proxy' + explanation = ( + 'The resource must be accessed through a proxy located at') + +class HTTPTemporaryRedirect(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily + under a different URI. + + code: 307, title: Temporary Redirect + """ + code = 307 + title = 'Temporary Redirect' + +############################################################ +## 4xx client error +############################################################ + +class HTTPClientError(HTTPError): + """ + base class for the 400s, where the client is in error + + This is an error condition in which the client is presumed to be + in-error. This is an expected problem, and thus is not considered + a bug. A server-side traceback is not warranted. Unless specialized, + this is a '400 Bad Request' + """ + code = 400 + title = 'Bad Request' + explanation = ('The server could not comply with the request since ' + 'it is either malformed or otherwise incorrect.') + +class HTTPBadRequest(HTTPClientError): + pass + +class HTTPUnauthorized(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request requires user authentication. + + code: 401, title: Unauthorized + """ + code = 401 + title = 'Unauthorized' + explanation = ( + 'This server could not verify that you are authorized to ' + 'access the document you requested. Either you supplied the ' + 'wrong credentials (e.g., bad password), or your browser ' + 'does not understand how to supply the credentials required.') + +class HTTPPaymentRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + code: 402, title: Payment Required + """ + code = 402 + title = 'Payment Required' + explanation = ('Access was denied for financial reasons.') + +class HTTPForbidden(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server understood the request, but is + refusing to fulfill it. + + code: 403, title: Forbidden + + Raise this exception within :term:`view` code to immediately return the + :term:`forbidden view` to the invoking user. Usually this is a basic + ``403`` page, but the forbidden view can be customized as necessary. See + :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be + the ``context`` of a :term:`Forbidden View`. + + This exception's constructor treats two arguments specially. The first + argument, ``detail``, should be a string. The value of this string will + be used as the ``message`` attribute of the exception object. The second + special keyword argument, ``result`` is usually an instance of + :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` + each of which indicates a reason for the forbidden error. However, + ``result`` is also permitted to be just a plain boolean ``False`` object + or ``None``. The ``result`` value will be used as the ``result`` + attribute of the exception object. It defaults to ``None``. + + The :term:`Forbidden View` can use the attributes of a Forbidden + exception as necessary to provide extended information in an error + report shown to a user. + """ + # differences from webob.exc.HTTPForbidden: + # + # - accepts a ``result`` keyword argument + # + # - overrides constructor to set ``self.result`` + # + # differences from older ``pyramid.exceptions.Forbidden``: + # + # - ``result`` must be passed as a keyword argument. + # + code = 403 + title = 'Forbidden' + explanation = ('Access was denied to this resource.') + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, result=None, **kw): + HTTPClientError.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + self.result = result + +class HTTPNotFound(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find anything matching the + Request-URI. + + code: 404, title: Not Found + + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found view` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found view can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a ``detail`` argument + (the first argument), which should be a string. The value of this + string will be available as the ``message`` attribute of this exception, + for availability to the :term:`Not Found View`. + """ + code = 404 + title = 'Not Found' + explanation = ('The resource could not be found.') + +class HTTPMethodNotAllowed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method specified in the Request-Line is + not allowed for the resource identified by the Request-URI. + + code: 405, title: Method Not Allowed + """ + # differences from webob.exc.HTTPMethodNotAllowed: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 405 + title = 'Method Not Allowed' + +class HTTPNotAcceptable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates the resource identified by the request is only + capable of generating response entities which have content + characteristics not acceptable according to the accept headers + sent in the request. + + code: 406, title: Not Acceptable + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # HTTP_ACCEPT) + code = 406 + title = 'Not Acceptable' + +class HTTPProxyAuthenticationRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This is similar to 401, but indicates that the client must first + authenticate itself with the proxy. + + code: 407, title: Proxy Authentication Required + """ + code = 407 + title = 'Proxy Authentication Required' + explanation = ('Authentication with a local proxy is needed.') + +class HTTPRequestTimeout(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the client did not produce a request within + the time that the server was prepared to wait. + + code: 408, title: Request Timeout + """ + code = 408 + title = 'Request Timeout' + explanation = ('The server has waited too long for the request to ' + 'be sent by the client.') + +class HTTPConflict(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request could not be completed due to a + conflict with the current state of the resource. + + code: 409, title: Conflict + """ + code = 409 + title = 'Conflict' + explanation = ('There was a conflict when trying to complete ' + 'your request.') + +class HTTPGone(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the requested resource is no longer available + at the server and no forwarding address is known. + + code: 410, title: Gone + """ + code = 410 + title = 'Gone' + explanation = ('This resource is no longer available. No forwarding ' + 'address is given.') + +class HTTPLengthRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the the server refuses to accept the request + without a defined Content-Length. + + code: 411, title: Length Required + """ + code = 411 + title = 'Length Required' + explanation = ('Content-Length header required.') + +class HTTPPreconditionFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the precondition given in one or more of the + request-header fields evaluated to false when it was tested on the + server. + + code: 412, title: Precondition Failed + """ + code = 412 + title = 'Precondition Failed' + explanation = ('Request precondition failed.') + +class HTTPRequestEntityTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to process a request + because the request entity is larger than the server is willing or + able to process. + + code: 413, title: Request Entity Too Large + """ + code = 413 + title = 'Request Entity Too Large' + explanation = ('The body of your request was too large for this server.') + +class HTTPRequestURITooLong(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the Request-URI is longer than the server is willing to + interpret. + + code: 414, title: Request-URI Too Long + """ + code = 414 + title = 'Request-URI Too Long' + explanation = ('The request URI was too long for this server.') + +class HTTPUnsupportedMediaType(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the entity of the request is in a format not supported by + the requested resource for the requested method. + + code: 415, title: Unsupported Media Type + """ + # differences from webob.exc.HTTPUnsupportedMediaType: + # + # - body_template_obj not overridden (it tried to use request environ's + # CONTENT_TYPE) + code = 415 + title = 'Unsupported Media Type' + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + The server SHOULD return a response with this status code if a + request included a Range request-header field, and none of the + range-specifier values in this field overlap the current extent + of the selected resource, and the request did not include an + If-Range request-header field. + + code: 416, title: Request Range Not Satisfiable + """ + code = 416 + title = 'Request Range Not Satisfiable' + explanation = ('The Range requested is not available.') + +class HTTPExpectationFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indidcates that the expectation given in an Expect + request-header field could not be met by this server. + + code: 417, title: Expectation Failed + """ + code = 417 + title = 'Expectation Failed' + explanation = ('Expectation failed.') + +class HTTPUnprocessableEntity(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unable to process the contained + instructions. Only for WebDAV. + + code: 422, title: Unprocessable Entity + """ + ## Note: from WebDAV + code = 422 + title = 'Unprocessable Entity' + explanation = 'Unable to process the contained instructions' + +class HTTPLocked(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the resource is locked. Only for WebDAV + + code: 423, title: Locked + """ + ## Note: from WebDAV + code = 423 + title = 'Locked' + explanation = ('The resource is locked') + +class HTTPFailedDependency(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method could not be performed because the + requested action depended on another action and that action failed. + Only for WebDAV. + + code: 424, title: Failed Dependency + """ + ## Note: from WebDAV + code = 424 + title = 'Failed Dependency' + explanation = ( + 'The method could not be performed because the requested ' + 'action dependended on another action and that action failed') + +############################################################ +## 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + +class HTTPServerError(HTTPError): + """ + base class for the 500s, where the server is in-error + + This is an error condition in which the server is presumed to be + in-error. Unless specialized, this is a '500 Internal Server Error'. + """ + code = 500 + title = 'Internal Server Error' + explanation = ( + 'The server has either erred or is incapable of performing ' + 'the requested operation.') + +class HTTPInternalServerError(HTTPServerError): + pass + +class HTTPNotImplemented(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support the functionality + required to fulfill the request. + + code: 501, title: Not Implemented + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - body_template_obj not overridden (it tried to use request environ's + # REQUEST_METHOD) + code = 501 + title = 'Not Implemented' + +class HTTPBadGateway(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + + code: 502, title: Bad Gateway + """ + code = 502 + title = 'Bad Gateway' + explanation = ('Bad gateway.') + +class HTTPServiceUnavailable(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server is currently unable to handle the + request due to a temporary overloading or maintenance of the server. + + code: 503, title: Service Unavailable + """ + code = 503 + title = 'Service Unavailable' + explanation = ('The server is currently unavailable. ' + 'Please try again at a later time.') + +class HTTPGatewayTimeout(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + did not receive a timely response from the upstream server specified + by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server + (e.g. DNS) it needed to access in attempting to complete the request. + + code: 504, title: Gateway Timeout + """ + code = 504 + title = 'Gateway Timeout' + explanation = ('The gateway has timed out.') + +class HTTPVersionNotSupported(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support, or refuses to + support, the HTTP protocol version that was used in the request + message. + + code: 505, title: HTTP Version Not Supported + """ + code = 505 + title = 'HTTP Version Not Supported' + explanation = ('The HTTP version is not supported.') + +class HTTPInsufficientStorage(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not have enough space to save + the resource. + + code: 507, title: Insufficient Storage + """ + code = 507 + title = 'Insufficient Storage' + explanation = ('There was not enough space to save the resource') + +def responsecode(status_code, **kw): + """Creates an HTTP exception based on a status code. Example:: + + raise responsecode(404) # raises an HTTPNotFound exception. + + The values passed as ``kw`` are provided to the exception's constructor. + """ + exc = status_map[status_code](**kw) + return exc + +def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + return context + +status_map={} +for name, value in globals().items(): + if (isinstance(value, (type, types.ClassType)) and + issubclass(value, HTTPException) + and not name.startswith('_')): + code = getattr(value, 'code', None) + if code: + status_map[code] = value +del name, value + + + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d200d15cf..fee8d549d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -46,21 +46,225 @@ class IApplicationCreated(Interface): IWSGIApplicationCreatedEvent = IApplicationCreated # b /c -class IResponse(Interface): # not an API - status = Attribute('WSGI status code of response') - headerlist = Attribute('List of response headers') - app_iter = Attribute('Iterable representing the response body') +class IResponse(Interface): + """ Represents a WSGI response using the WebOb response interface. Some + attribute and method documentation of this interface references `RFC 2616 + <http://www.w3.org/Protocols/rfc2616/>`_. + + This interface is most famously implemented by + :class:`pyramid.response.Response` and the HTTP exception classes in + :mod:`pyramid.httpexceptions`.""" + + RequestClass = Attribute( + """ Alias for :class:`pyramid.request.Request` """) + + def __call__(environ, start_response): + """ :term:`WSGI` call interface, should call the start_response + callback and should return an iterable""" + + accept_ranges = Attribute( + """Gets and sets and deletes the Accept-Ranges header. For more + information on Accept-Ranges see RFC 2616, section 14.5""") + + age = Attribute( + """Gets and sets and deletes the Age header. Converts using int. + For more information on Age see RFC 2616, section 14.6.""") + + allow = Attribute( + """Gets and sets and deletes the Allow header. Converts using + list. For more information on Allow see RFC 2616, Section 14.7.""") + + app_iter = Attribute( + """Returns the app_iter of the response. + + If body was set, this will create an app_iter from that body + (a single-item list)""") + + def app_iter_range(start, stop): + """ Return a new app_iter built from the response app_iter that + serves up only the given start:stop range. """ + + body = Attribute( + """The body of the response, as a str. This will read in the entire + app_iter if necessary.""") + + body_file = Attribute( + """A file-like object that can be used to write to the body. If you + passed in a list app_iter, that app_iter will be modified by writes.""") + + cache_control = Attribute( + """Get/set/modify the Cache-Control header (RFC 2616 section 14.9)""") + + cache_expires = Attribute( + """ Get/set the Cache-Control and Expires headers. This sets the + response to expire in the number of seconds passed when set. """) + + charset = Attribute( + """Get/set the charset (in the Content-Type)""") + + def conditional_response_app(environ, start_response): + """ Like the normal __call__ interface, but checks conditional + headers: + + - If-Modified-Since (304 Not Modified; only on GET, HEAD) + + - If-None-Match (304 Not Modified; only on GET, HEAD) + + - Range (406 Partial Content; only on GET, HEAD)""" + + content_disposition = Attribute( + """Gets and sets and deletes the Content-Disposition header. + For more information on Content-Disposition see RFC 2616 section + 19.5.1.""") + + content_encoding = Attribute( + """Gets and sets and deletes the Content-Encoding header. For more + information about Content-Encoding see RFC 2616 section 14.11.""") + + content_language = Attribute( + """Gets and sets and deletes the Content-Language header. Converts + using list. For more information about Content-Language see RFC 2616 + section 14.12.""") + + content_length = Attribute( + """Gets and sets and deletes the Content-Length header. For more + information on Content-Length see RFC 2616 section 14.17. + Converts using int. """) + + content_location = Attribute( + """Gets and sets and deletes the Content-Location header. For more + information on Content-Location see RFC 2616 section 14.14.""") + + content_md5 = Attribute( + """Gets and sets and deletes the Content-MD5 header. For more + information on Content-MD5 see RFC 2616 section 14.14.""") + + content_range = Attribute( + """Gets and sets and deletes the Content-Range header. For more + information on Content-Range see section 14.16. Converts using + ContentRange object.""") + + content_type = Attribute( + """Get/set the Content-Type header (or None), without the charset + or any parameters. If you include parameters (or ; at all) when + setting the content_type, any existing parameters will be deleted; + otherwise they will be preserved.""") + + content_type_params = Attribute( + """A dictionary of all the parameters in the content type. This is + not a view, set to change, modifications of the dict would not + be applied otherwise.""") + + def copy(): + """ Makes a copy of the response and returns the copy. """ + + date = Attribute( + """Gets and sets and deletes the Date header. For more information on + Date see RFC 2616 section 14.18. Converts using HTTP date.""") + + def delete_cookie(key, path='/', domain=None): + """ Delete a cookie from the client. Note that path and domain must + match how the cookie was originally set. This sets the cookie to the + empty string, and max_age=0 so that it should expire immediately. """ + + def encode_content(encoding='gzip', lazy=False): + """ Encode the content with the given encoding (only gzip and + identity are supported).""" + + environ = Attribute( + """Get/set the request environ associated with this response, + if any.""") + + etag = Attribute( + """ Gets and sets and deletes the ETag header. For more information + on ETag see RFC 2616 section 14.19. Converts using Entity tag.""") + + expires = Attribute( + """ Gets and sets and deletes the Expires header. For more + information on Expires see RFC 2616 section 14.21. Converts using + HTTP date.""") + + headerlist = Attribute( + """ The list of response headers. """) + + headers = Attribute( + """ The headers in a dictionary-like object """) + + last_modified = Attribute( + """ Gets and sets and deletes the Last-Modified header. For more + information on Last-Modified see RFC 2616 section 14.29. Converts + using HTTP date.""") + + location = Attribute( + """ Gets and sets and deletes the Location header. For more + information on Location see RFC 2616 section 14.30.""") + + def md5_etag(body=None, set_content_md5=False): + """ Generate an etag for the response object using an MD5 hash of the + body (the body parameter, or self.body if not given). Sets self.etag. + If set_content_md5 is True sets self.content_md5 as well """ + + def merge_cookies(resp): + """ Merge the cookies that were set on this response with the given + resp object (which can be any WSGI application). If the resp is a + webob.Response object, then the other object will be modified + in-place. """ + + pragma = Attribute( + """ Gets and sets and deletes the Pragma header. For more information + on Pragma see RFC 2616 section 14.32. """) + + request = Attribute( + """ Return the request associated with this response if any. """) + + retry_after = Attribute( + """ Gets and sets and deletes the Retry-After header. For more + information on Retry-After see RFC 2616 section 14.37. Converts + using HTTP date or delta seconds.""") + + server = Attribute( + """ Gets and sets and deletes the Server header. For more information + on Server see RFC216 section 14.38. """) + + def set_cookie(key, value='', max_age=None, path='/', domain=None, + secure=False, httponly=False, comment=None, expires=None, + overwrite=False): + """ Set (add) a cookie for the response """ + + status = Attribute( + """ The status string. """) + + status_int = Attribute( + """ The status as an integer """) + + unicode_body = Attribute( + """ Get/set the unicode value of the body (using the charset of + the Content-Type)""") + + def unset_cookie(key, strict=True): + """ Unset a cookie with the given name (remove it from the + response).""" + + vary = Attribute( + """Gets and sets and deletes the Vary header. For more information + on Vary see section 14.44. Converts using list.""") + + www_authenticate = Attribute( + """ Gets and sets and deletes the WWW-Authenticate header. For more + information on WWW-Authenticate see RFC 2616 section 14.47. Converts + using 'parse_auth' and 'serialize_auth'. """) class IException(Interface): # not an API """ An interface representing a generic exception """ class IExceptionResponse(IException, IResponse): - """ An interface representing a WSGI response which is also an - exception object. Register an exception view using this interface - as a ``context`` to apply the registered view for all exception - types raised by :app:`Pyramid` internally - (:class:`pyramid.exceptions.NotFound` and - :class:`pyramid.exceptions.Forbidden`).""" + """ An interface representing a WSGI response which is also an exception + object. Register an exception view using this interface as a ``context`` + to apply the registered view for all exception types raised by + :app:`Pyramid` internally (any exception that inherits from + :class:`pyramid.response.Response`, including + :class:`pyramid.httpexceptions.HTTPNotFound` and + :class:`pyramid.httpexceptions.HTTPForbidden`).""" class IBeforeRender(Interface): """ @@ -273,11 +477,7 @@ class IExceptionViewClassifier(Interface): class IView(Interface): def __call__(context, request): - """ Must return an object that implements IResponse. May - optionally raise ``pyramid.exceptions.Forbidden`` if an - authorization failure is detected during view execution or - ``pyramid.exceptions.NotFound`` if the not found page is - meant to be returned.""" + """ Must return an object that implements IResponse. """ class ISecuredView(IView): """ *Internal only* interface. Not an API. """ diff --git a/pyramid/registry.py b/pyramid/registry.py index 37e230dc3..5db0a11e2 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -1,4 +1,5 @@ from zope.component.registry import Components +from zope.interface import providedBy from pyramid.interfaces import ISettings @@ -28,6 +29,22 @@ class Registry(Components, dict): self.has_listeners = True return result + def registerSelfAdapter(self, required=None, provided=None, name=u'', + info=u'', event=True): + # registerAdapter analogue which always returns the object itself + # when required is matched + return self.registerAdapter(lambda x: x, required=required, + provided=provided, name=name, + info=info, event=event) + + def queryAdapterOrSelf(self, object, interface, default=None): + # queryAdapter analogue which returns the object if it implements + # the interface, otherwise it will return an adaptation to the + # interface + if not interface.providedBy(object): + return self.queryAdapter(object, interface, default=default) + return object + def registerHandler(self, *arg, **kw): result = Components.registerHandler(self, *arg, **kw) self.has_listeners = True diff --git a/pyramid/renderers.py b/pyramid/renderers.py index a6dce9b3a..6865067dd 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -316,9 +316,7 @@ class RendererHelper(object): 'context':context, 'request':request } - return self.render_to_response(response, system, - request=request) - + return self.render_to_response(response, system, request=request) def render(self, value, system_values, request=None): renderer = self.renderer diff --git a/pyramid/request.py b/pyramid/request.py index d387a0b2f..06dbddd29 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -5,12 +5,14 @@ from zope.interface.interface import InterfaceClass from webob import BaseRequest from pyramid.interfaces import IRequest +from pyramid.interfaces import IResponse from pyramid.interfaces import ISessionFactory from pyramid.interfaces import IResponseFactory from pyramid.exceptions import ConfigurationError from pyramid.decorator import reify from pyramid.response import Response +from pyramid.threadlocal import get_current_registry from pyramid.url import resource_url from pyramid.url import route_url from pyramid.url import static_url @@ -321,6 +323,15 @@ class Request(BaseRequest): default=Response) return response_factory() + def is_response(self, ob): + """ Return ``True`` if the object passed as ``ob`` is a valid + response object, ``False`` otherwise.""" + registry = self.registry + adapted = registry.queryAdapterOrSelf(ob, IResponse) + if adapted is None: + return False + return adapted is ob + # b/c dict interface for "root factory" code that expects a bare # environ. Explicitly omitted dict methods: clear (unnecessary), # copy (implemented by WebOb), fromkeys (unnecessary); deprecated diff --git a/pyramid/response.py b/pyramid/response.py index 26f27b142..68496e386 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,2 +1,7 @@ -from webob import Response -Response = Response # pyflakes +from webob import Response as _Response +from zope.interface import implements +from pyramid.interfaces import IResponse + +class Response(_Response): + implements(IResponse) + diff --git a/pyramid/router.py b/pyramid/router.py index b8a8639aa..8e33332df 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -12,11 +12,12 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import ITraverser from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import IResponse from pyramid.events import ContextFound from pyramid.events import NewRequest from pyramid.events import NewResponse -from pyramid.exceptions import NotFound +from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory @@ -153,9 +154,9 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg) + raise HTTPNotFound(msg) else: - response = view_callable(context, request) + result = view_callable(context, request) # handle exceptions raised during root finding and view-exec except Exception, why: @@ -177,30 +178,26 @@ class Router(object): # repoze.bfg.message docs-deprecated in Pyramid 1.0 environ['repoze.bfg.message'] = msg - response = view_callable(why, request) + result = view_callable(why, request) # process the response + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: + raise ValueError( + 'Could not convert view return value "%s" into a ' + 'response object' % (result,)) has_listeners and registry.notify(NewResponse(request,response)) if request.response_callbacks: request._process_response_callbacks(response) - try: - headers = response.headerlist - app_iter = response.app_iter - status = response.status - except AttributeError: - raise ValueError( - 'Non-response object returned from view named %s ' - '(and no renderer): %r' % (view_name, response)) - finally: if request is not None and request.finished_callbacks: request._process_finished_callbacks() - start_response(status, headers) - return app_iter - + return response(request.environ, start_response) + finally: manager.pop() + diff --git a/pyramid/session.py b/pyramid/session.py index 5772c80d0..bb3226a1e 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -8,8 +8,6 @@ try: except ImportError: # pragma: no cover import pickle -from webob import Response - import base64 import binascii import hmac @@ -213,17 +211,7 @@ def UnencryptedCookieSessionFactoryConfig( 'Cookie value is too long to store (%s bytes)' % len(cookieval) ) - if hasattr(response, 'set_cookie'): - # ``response`` is a "real" webob response - set_cookie = response.set_cookie - else: - # ``response`` is not a "real" webob response, cope - def set_cookie(*arg, **kw): - tmp_response = Response() - tmp_response.set_cookie(*arg, **kw) - response.headerlist.append( - tmp_response.headerlist[-1]) - set_cookie( + response.set_cookie( self._cookie_name, value=cookieval, max_age = self._cookie_max_age, diff --git a/pyramid/testing.py b/pyramid/testing.py index 36cc38830..86276df1e 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -17,7 +17,7 @@ from pyramid.interfaces import ISession from pyramid.config import Configurator from pyramid.decorator import reify -from pyramid.exceptions import Forbidden +from pyramid.httpexceptions import HTTPForbidden from pyramid.response import Response from pyramid.registry import Registry from pyramid.security import Authenticated @@ -217,7 +217,7 @@ def registerView(name, result='', view=None, for_=(Interface, Interface), else: def _secure(context, request): if not has_permission(permission, context, request): - raise Forbidden('no permission') + raise HTTPForbidden('no permission') else: return view(context, request) _secure.__call_permissive__ = view diff --git a/pyramid/tests/fixtureapp/views.py b/pyramid/tests/fixtureapp/views.py index 9ab985e32..cbfc5a574 100644 --- a/pyramid/tests/fixtureapp/views.py +++ b/pyramid/tests/fixtureapp/views.py @@ -1,6 +1,6 @@ from zope.interface import Interface from webob import Response -from pyramid.exceptions import Forbidden +from pyramid.httpexceptions import HTTPForbidden def fixture_view(context, request): """ """ @@ -16,7 +16,7 @@ def exception_view(context, request): def protected_view(context, request): """ """ - raise Forbidden() + raise HTTPForbidden() class IDummy(Interface): pass diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index ed9aa8357..7001b87f5 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,7 +1,5 @@ -from cgi import escape from webob import Response from pyramid.httpexceptions import HTTPForbidden -from pyramid.exceptions import Forbidden def x_view(request): # pragma: no cover return Response('this is private!') @@ -22,4 +20,4 @@ def includeme(config): config._set_authentication_policy(authn_policy) config._set_authorization_policy(authz_policy) config.add_view(x_view, name='x', permission='private') - config.add_view(forbidden_view, context=Forbidden) + config.add_view(forbidden_view, context=HTTPForbidden) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 97a93616d..cc4a037c2 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -50,8 +50,8 @@ class ConfiguratorTests(unittest.TestCase): return iface def _assertNotFound(self, wrapper, *arg): - from pyramid.exceptions import NotFound - self.assertRaises(NotFound, wrapper, *arg) + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, wrapper, *arg) def _registerEventListener(self, config, event_iface=None): if event_iface is None: # pragma: no cover @@ -203,6 +203,35 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IViewMapperFactory), mapper) + def test_ctor_httpexception_view_default(self): + from pyramid.interfaces import IExceptionResponse + from pyramid.httpexceptions import default_exceptionresponse_view + from pyramid.interfaces import IRequest + config = self._makeOne() + view = self._getViewCallable(config, + ctx_iface=IExceptionResponse, + request_iface=IRequest) + self.failUnless(view is default_exceptionresponse_view) + + def test_ctor_exceptionresponse_view_None(self): + from pyramid.interfaces import IExceptionResponse + from pyramid.interfaces import IRequest + config = self._makeOne(exceptionresponse_view=None) + view = self._getViewCallable(config, + ctx_iface=IExceptionResponse, + request_iface=IRequest) + self.failUnless(view is None) + + def test_ctor_exceptionresponse_view_custom(self): + from pyramid.interfaces import IExceptionResponse + from pyramid.interfaces import IRequest + def exceptionresponse_view(context, request): pass + config = self._makeOne(exceptionresponse_view=exceptionresponse_view) + view = self._getViewCallable(config, + ctx_iface=IExceptionResponse, + request_iface=IRequest) + self.failUnless(view is exceptionresponse_view) + def test_with_package_module(self): from pyramid.tests import test_configuration import pyramid.tests @@ -260,27 +289,58 @@ class ConfiguratorTests(unittest.TestCase): result = config.absolute_asset_spec('templates') self.assertEqual(result, 'pyramid.tests:templates') - def test_setup_registry_fixed(self): - class DummyRegistry(object): - def subscribers(self, events, name): - self.events = events - return events - def registerUtility(self, *arg, **kw): - pass + def test__fix_registry_has_listeners(self): reg = DummyRegistry() config = self._makeOne(reg) - config.add_view = lambda *arg, **kw: False - config.setup_registry() + config._fix_registry() self.assertEqual(reg.has_listeners, True) + + def test__fix_registry_notify(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() self.assertEqual(reg.notify(1), None) self.assertEqual(reg.events, (1,)) + def test__fix_registry_queryAdapterOrSelf(self): + from zope.interface import Interface + class IFoo(Interface): + pass + class Foo(object): + implements(IFoo) + class Bar(object): + pass + adaptation = () + foo = Foo() + bar = Bar() + reg = DummyRegistry(adaptation) + config = self._makeOne(reg) + config._fix_registry() + self.assertTrue(reg.queryAdapterOrSelf(foo, IFoo) is foo) + self.assertTrue(reg.queryAdapterOrSelf(bar, IFoo) is adaptation) + + def test__fix_registry_registerSelfAdapter(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + reg.registerSelfAdapter('required', 'provided', name='abc') + self.assertEqual(len(reg.adapters), 1) + args, kw = reg.adapters[0] + self.assertEqual(args[0]('abc'), 'abc') + self.assertEqual(kw, + {'info': u'', 'provided': 'provided', + 'required': 'required', 'name': 'abc', 'event': True}) + + def test_setup_registry_calls_fix_registry(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config.add_view = lambda *arg, **kw: False + config.setup_registry() + self.assertEqual(reg.has_listeners, True) + def test_setup_registry_registers_default_exceptionresponse_view(self): from pyramid.interfaces import IExceptionResponse from pyramid.view import default_exceptionresponse_view - class DummyRegistry(object): - def registerUtility(self, *arg, **kw): - pass reg = DummyRegistry() config = self._makeOne(reg) views = [] @@ -289,19 +349,29 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) + def test_setup_registry_registers_default_webob_iresponse_adapter(self): + from webob import Response + from pyramid.interfaces import IResponse + config = self._makeOne() + config.setup_registry() + response = Response() + self.assertTrue( + config.registry.queryAdapter(response, IResponse) is response) + def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.registry import Registry reg = Registry() config = self._makeOne(reg, autocommit=True) config.setup_registry() # registers IExceptionResponse default view def myview(context, request): return 'OK' - config.add_view(myview, context=NotFound) + config.add_view(myview, context=HTTPNotFound) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, 'OK') @@ -1665,14 +1735,14 @@ class ConfiguratorTests(unittest.TestCase): self._assertNotFound(wrapper, None, request) def test_add_view_with_header_val_missing(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound view = lambda *arg: 'OK' config = self._makeOne(autocommit=True) config.add_view(view=view, header=r'Host:\d') wrapper = self._getViewCallable(config) request = self._makeRequest(config) request.headers = {'NoHost':'1'} - self.assertRaises(NotFound, wrapper, None, request) + self.assertRaises(HTTPNotFound, wrapper, None, request) def test_add_view_with_accept_match(self): view = lambda *arg: 'OK' @@ -2199,12 +2269,13 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, (None, request)) @@ -2212,13 +2283,14 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) request = self._makeRequest(config) request.context = 'abc' - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, ('abc', request)) @@ -2227,7 +2299,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_notfound_view(view, @@ -2236,7 +2308,7 @@ class ConfiguratorTests(unittest.TestCase): try: # chameleon depends on being able to find a threadlocal registry request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(NotFound), + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) finally: @@ -2246,12 +2318,13 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: 'OK' config.set_forbidden_view(view) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(Forbidden), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) self.assertEqual(result, 'OK') @@ -2259,13 +2332,14 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_forbidden_view(view) request = self._makeRequest(config) request.context = 'abc' - view = self._getViewCallable(config, ctx_iface=implementedBy(Forbidden), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) self.assertEqual(result, ('abc', request)) @@ -2274,7 +2348,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_forbidden_view(view, @@ -2283,7 +2357,7 @@ class ConfiguratorTests(unittest.TestCase): try: # chameleon requires a threadlocal registry request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(Forbidden), + ctx_iface=implementedBy(HTTPForbidden), request_iface=IRequest) result = view(None, request) finally: @@ -2554,6 +2628,34 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), pyramid.tests) + def test_add_response_adapter(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + def __init__(self, other): + self.other = other + config.add_response_adapter(Adapter, str) + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.other, 'foo') + + def test_add_response_adapter_self(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + pass + config.add_response_adapter(None, Adapter) + adapter = Adapter() + result = config.registry.queryAdapter(adapter, IResponse) + self.assertTrue(result is adapter) + + def test_add_response_adapter_dottednames(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + config.add_response_adapter('pyramid.response.Response', + 'types.StringType') + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.body, 'foo') + def test_scan_integration(self): import os from zope.interface import alsoProvides @@ -3653,7 +3755,7 @@ class TestViewDeriver(unittest.TestCase): "None against context None): True") def test_debug_auth_permission_authpol_denied(self): - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden view = lambda *arg: 'OK' self.config.registry.settings = dict( debug_authorization=True, reload_templates=True) @@ -3668,7 +3770,7 @@ class TestViewDeriver(unittest.TestCase): request = self._makeRequest() request.view_name = 'view_name' request.url = 'url' - self.assertRaises(Forbidden, result, None, request) + self.assertRaises(HTTPForbidden, result, None, request) self.assertEqual(len(logger.messages), 1) self.assertEqual(logger.messages[0], "debug_authorization of url url (view name " @@ -3781,7 +3883,7 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(predicates, [True, True]) def test_with_predicates_notall(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound view = lambda *arg: 'OK' predicates = [] def predicate1(context, request): @@ -3794,7 +3896,7 @@ class TestViewDeriver(unittest.TestCase): result = deriver(view) request = self._makeRequest() request.method = 'POST' - self.assertRaises(NotFound, result, None, None) + self.assertRaises(HTTPNotFound, result, None, None) self.assertEqual(predicates, [True, True]) def test_with_wrapper_viewname(self): @@ -4589,14 +4691,14 @@ class TestMultiView(unittest.TestCase): self.assertEqual(mv.get_views(request), mv.views) def test_match_not_found(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.match, context, request) + self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_fails(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() def view(context, request): """ """ @@ -4604,7 +4706,7 @@ class TestMultiView(unittest.TestCase): mv.views = [(100, view, None)] context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.match, context, request) + self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_succeeds(self): mv = self._makeOne() @@ -4618,11 +4720,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, view) def test_permitted_no_views(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.__permitted__, context, request) + self.assertRaises(HTTPNotFound, mv.__permitted__, context, request) def test_permitted_no_match_with__permitted__(self): mv = self._makeOne() @@ -4645,11 +4747,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, False) def test__call__not_found(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call__intermediate_not_found(self): from pyramid.exceptions import PredicateMismatch @@ -4667,17 +4769,17 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test___call__raise_not_found_isnt_interpreted_as_pred_mismatch(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() request.view_name = '' def view1(context, request): - raise NotFound + raise HTTPNotFound def view2(context, request): """ """ mv.views = [(100, view1, None), (99, view2, None)] - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call__(self): mv = self._makeOne() @@ -4692,11 +4794,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test__call_permissive__not_found(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call_permissive_has_call_permissive(self): mv = self._makeOne() @@ -5100,3 +5202,17 @@ def dummy_extend(config, discrim): def dummy_extend2(config, discrim): config.action(discrim, None, config.registry) +class DummyRegistry(object): + def __init__(self, adaptation=None): + self.utilities = [] + self.adapters = [] + self.adaptation = adaptation + def subscribers(self, events, name): + self.events = events + return events + def registerUtility(self, *arg, **kw): + self.utilities.append((arg, kw)) + def registerAdapter(self, *arg, **kw): + self.adapters.append((arg, kw)) + def queryAdapter(self, *arg, **kw): + return self.adaptation diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 5d0fa1e1a..50182ee5c 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -1,26 +1,15 @@ import unittest -class TestExceptionResponse(unittest.TestCase): - def _makeOne(self, message): - from pyramid.exceptions import ExceptionResponse - return ExceptionResponse(message) - - def test_app_iter(self): - exc = self._makeOne('') - self.assertTrue('<code></code>' in exc.app_iter[0]) - - def test_headerlist(self): - exc = self._makeOne('') - headerlist = exc.headerlist - headerlist.sort() - app_iter = exc.app_iter - clen = str(len(app_iter[0])) - self.assertEqual(headerlist[0], ('Content-Length', clen)) - self.assertEqual(headerlist[1], ('Content-Type', 'text/html')) - - def test_withmessage(self): - exc = self._makeOne('abc&123') - self.assertTrue('<code>abc&123</code>' in exc.app_iter[0]) +class TestBWCompat(unittest.TestCase): + def test_bwcompat_notfound(self): + from pyramid.exceptions import NotFound as one + from pyramid.httpexceptions import HTTPNotFound as two + self.assertTrue(one is two) + + def test_bwcompat_forbidden(self): + from pyramid.exceptions import Forbidden as one + from pyramid.httpexceptions import HTTPForbidden as two + self.assertTrue(one is two) class TestNotFound(unittest.TestCase): def _makeOne(self, message): @@ -28,10 +17,16 @@ class TestNotFound(unittest.TestCase): return NotFound(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse + from pyramid.interfaces import IExceptionResponse e = self._makeOne('notfound') - self.assertTrue(isinstance(e, ExceptionResponse)) + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '404 Not Found') + self.assertEqual(e.message, 'notfound') + + def test_response_equivalence(self): + from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound + self.assertTrue(NotFound is HTTPNotFound) class TestForbidden(unittest.TestCase): def _makeOne(self, message): @@ -39,7 +34,14 @@ class TestForbidden(unittest.TestCase): return Forbidden(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse - e = self._makeOne('unauthorized') - self.assertTrue(isinstance(e, ExceptionResponse)) + from pyramid.interfaces import IExceptionResponse + e = self._makeOne('forbidden') + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '403 Forbidden') + self.assertEqual(e.message, 'forbidden') + + def test_response_equivalence(self): + from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden + self.assertTrue(Forbidden is HTTPForbidden) + diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py new file mode 100644 index 000000000..60bde460e --- /dev/null +++ b/pyramid/tests/test_httpexceptions.py @@ -0,0 +1,306 @@ +import unittest + +class Test_responsecode(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.httpexceptions import responsecode + return responsecode(*arg, **kw) + + def test_status_404(self): + from pyramid.httpexceptions import HTTPNotFound + self.assertEqual(self._callFUT(404).__class__, HTTPNotFound) + + def test_status_201(self): + from pyramid.httpexceptions import HTTPCreated + self.assertEqual(self._callFUT(201).__class__, HTTPCreated) + + def test_extra_kw(self): + resp = self._callFUT(404, headers=[('abc', 'def')]) + self.assertEqual(resp.headers['abc'], 'def') + +class Test_default_exceptionresponse_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.httpexceptions import default_exceptionresponse_view + return default_exceptionresponse_view(context, request) + + def test_call_with_exception(self): + context = Exception() + result = self._callFUT(context, None) + self.assertEqual(result, context) + + def test_call_with_nonexception(self): + request = DummyRequest() + context = Exception() + request.exception = context + result = self._callFUT(None, request) + self.assertEqual(result, context) + +class Test__no_escape(unittest.TestCase): + def _callFUT(self, val): + from pyramid.httpexceptions import _no_escape + return _no_escape(val) + + def test_null(self): + self.assertEqual(self._callFUT(None), '') + + def test_not_basestring(self): + self.assertEqual(self._callFUT(42), '42') + + def test_unicode(self): + class DummyUnicodeObject(object): + def __unicode__(self): + return u'42' + duo = DummyUnicodeObject() + self.assertEqual(self._callFUT(duo), u'42') + +class TestWSGIHTTPException(unittest.TestCase): + def _getTargetClass(self): + from pyramid.httpexceptions import WSGIHTTPException + return WSGIHTTPException + + def _getTargetSubclass(self, code='200', title='OK', + explanation='explanation', empty_body=False): + cls = self._getTargetClass() + class Subclass(cls): + pass + Subclass.empty_body = empty_body + Subclass.code = code + Subclass.title = title + Subclass.explanation = explanation + return Subclass + + def _makeOne(self, *arg, **kw): + cls = self._getTargetClass() + return cls(*arg, **kw) + + def test_implements_IResponse(self): + from pyramid.interfaces import IResponse + cls = self._getTargetClass() + self.failUnless(IResponse.implementedBy(cls)) + + def test_provides_IResponse(self): + from pyramid.interfaces import IResponse + inst = self._getTargetClass()() + self.failUnless(IResponse.providedBy(inst)) + + def test_implements_IExceptionResponse(self): + from pyramid.interfaces import IExceptionResponse + cls = self._getTargetClass() + self.failUnless(IExceptionResponse.implementedBy(cls)) + + def test_provides_IExceptionResponse(self): + from pyramid.interfaces import IExceptionResponse + inst = self._getTargetClass()() + self.failUnless(IExceptionResponse.providedBy(inst)) + + def test_ctor_sets_detail(self): + exc = self._makeOne('message') + self.assertEqual(exc.detail, 'message') + + def test_ctor_sets_comment(self): + exc = self._makeOne(comment='comment') + self.assertEqual(exc.comment, 'comment') + + def test_ctor_calls_Exception_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.message, 'message') + + def test_ctor_calls_Response_ctor(self): + exc = self._makeOne('message') + self.assertEqual(exc.status, 'None None') + + def test_ctor_extends_headers(self): + exc = self._makeOne(headers=[('X-Foo', 'foo')]) + self.assertEqual(exc.headers.get('X-Foo'), 'foo') + + def test_ctor_sets_body_template_obj(self): + exc = self._makeOne(body_template='${foo}') + self.assertEqual( + exc.body_template_obj.substitute({'foo':'foo'}), 'foo') + + def test_ctor_with_empty_body(self): + cls = self._getTargetSubclass(empty_body=True) + exc = cls() + self.assertEqual(exc.content_type, None) + self.assertEqual(exc.content_length, None) + + def test_ctor_with_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(body='123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_unicode_body_doesnt_set_default_app_iter(self): + exc = self._makeOne(unicode_body=u'123') + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_app_iter_doesnt_set_default_app_iter(self): + exc = self._makeOne(app_iter=['123']) + self.assertEqual(exc.app_iter, ['123']) + + def test_ctor_with_body_sets_default_app_iter_html(self): + cls = self._getTargetSubclass() + exc = cls('detail') + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertTrue(body.startswith('<html')) + self.assertTrue('200 OK' in body) + self.assertTrue('explanation' in body) + self.assertTrue('detail' in body) + + def test_ctor_with_body_sets_default_app_iter_text(self): + cls = self._getTargetSubclass() + exc = cls('detail') + exc.content_type = 'text/plain' + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, '200 OK\n\nexplanation\n\n\ndetail\n\n') + + def test__str__detail(self): + exc = self._makeOne() + exc.detail = 'abc' + self.assertEqual(str(exc), 'abc') + + def test__str__explanation(self): + exc = self._makeOne() + exc.explanation = 'def' + self.assertEqual(str(exc), 'def') + + def test_wsgi_response(self): + exc = self._makeOne() + self.assertTrue(exc is exc.wsgi_response) + + def test_exception(self): + exc = self._makeOne() + self.assertTrue(exc is exc.exception) + + def test__calls_start_response(self): + cls = self._getTargetSubclass() + exc = cls() + exc.content_type = 'text/plain' + environ = _makeEnviron() + start_response = DummyStartResponse() + exc(environ, start_response) + self.assertTrue(start_response.headerlist) + self.assertEqual(start_response.status, '200 OK') + + def test__default_app_iter_no_comment_plain(self): + cls = self._getTargetSubclass() + exc = cls() + exc.content_type = 'text/plain' + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, '200 OK\n\nexplanation\n\n\n\n\n') + + def test__default_app_iter_with_comment_plain(self): + cls = self._getTargetSubclass() + exc = cls(comment='comment') + exc.content_type = 'text/plain' + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, '200 OK\n\nexplanation\n\n\n\ncomment\n') + + def test__default_app_iter_no_comment_html(self): + cls = self._getTargetSubclass() + exc = cls() + exc.content_type = 'text/html' + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertFalse('<!-- ' in body) + + def test__default_app_iter_with_comment_html(self): + cls = self._getTargetSubclass() + exc = cls(comment='comment & comment') + exc.content_type = 'text/html' + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertTrue('<!-- comment & comment -->' in body) + + def test_custom_body_template(self): + cls = self._getTargetSubclass() + exc = cls(body_template='${REQUEST_METHOD}') + exc.content_type = 'text/plain' + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, '200 OK\n\nGET') + + def test_body_template_unicode(self): + cls = self._getTargetSubclass() + la = unicode('/La Pe\xc3\xb1a', 'utf-8') + environ = _makeEnviron(unicodeval=la) + exc = cls(body_template='${unicodeval}') + exc.content_type = 'text/plain' + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') + +class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): + def _doit(self, content_type): + from pyramid.httpexceptions import status_map + L = [] + self.assertTrue(status_map) + for v in status_map.values(): + environ = _makeEnviron() + start_response = DummyStartResponse() + exc = v() + exc.content_type = content_type + result = list(exc(environ, start_response))[0] + if exc.empty_body: + self.assertEqual(result, '') + else: + self.assertTrue(exc.status in result) + L.append(result) + self.assertEqual(len(L), len(status_map)) + + def test_it_plain(self): + self._doit('text/plain') + + def test_it_html(self): + self._doit('text/html') + +class Test_HTTPMove(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import _HTTPMove + return _HTTPMove(*arg, **kw) + + def test_it_location_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.location, '') + + def test_it_location_passed(self): + exc = self._makeOne(location='foo') + self.assertEqual(exc.location, 'foo') + +class TestHTTPForbidden(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import HTTPForbidden + return HTTPForbidden(*arg, **kw) + + def test_it_result_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.result, None) + + def test_it_result_passed(self): + exc = self._makeOne(result='foo') + self.assertEqual(exc.result, 'foo') + +class DummyRequest(object): + exception = None + +class DummyStartResponse(object): + def __call__(self, status, headerlist): + self.status = status + self.headerlist = headerlist + +def _makeEnviron(**kw): + environ = {'REQUEST_METHOD':'GET', + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'80'} + environ.update(kw) + return environ + diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index e35856ce0..90c55b0f0 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -203,7 +203,35 @@ class TestRequest(unittest.TestCase): self.assertEqual(result, 'abc') self.assertEqual(info.args, ('pyramid.tests:static/foo.css', request, {}) ) + + def test_is_response_false(self): + request = self._makeOne({}) + request.registry = self.config.registry + self.assertEqual(request.is_response('abc'), False) + + def test_is_response_false_adapter_is_not_self(self): + from pyramid.interfaces import IResponse + request = self._makeOne({}) + request.registry = self.config.registry + def adapter(ob): + return object() + class Foo(object): + pass + foo = Foo() + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), False) + def test_is_response_adapter_true(self): + from pyramid.interfaces import IResponse + request = self._makeOne({}) + request.registry = self.config.registry + class Foo(object): + pass + foo = Foo() + def adapter(ob): + return ob + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), True) class TestRequestDeprecatedMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py new file mode 100644 index 000000000..46eb298d1 --- /dev/null +++ b/pyramid/tests/test_response.py @@ -0,0 +1,17 @@ +import unittest + +class TestResponse(unittest.TestCase): + def _getTargetClass(self): + from pyramid.response import Response + return Response + + def test_implements_IResponse(self): + from pyramid.interfaces import IResponse + cls = self._getTargetClass() + self.failUnless(IResponse.implementedBy(cls)) + + def test_provides_IResponse(self): + from pyramid.interfaces import IResponse + inst = self._getTargetClass()() + self.failUnless(IResponse.providedBy(inst)) + diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index b869a3830..5fd2cf01e 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -136,69 +136,70 @@ class TestRouter(unittest.TestCase): self.assertEqual(router.request_factory, DummyRequestFactory) def test_call_traverser_default(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_traverser_raises_notfound_class(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=NotFound) + self._registerTraverserFactory(context, raise_error=HTTPNotFound) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) def test_traverser_raises_notfound_instance(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=NotFound('foo')) + self._registerTraverserFactory(context, raise_error=HTTPNotFound('foo')) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('foo' in why[0], why) def test_traverser_raises_forbidden_class(self): - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=Forbidden) + self._registerTraverserFactory(context, raise_error=HTTPForbidden) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(Forbidden, router, environ, start_response) + self.assertRaises(HTTPForbidden, router, environ, start_response) def test_traverser_raises_forbidden_instance(self): - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=Forbidden('foo')) + self._registerTraverserFactory(context, + raise_error=HTTPForbidden('foo')) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(Forbidden, router, environ, start_response) + why = exc_raised(HTTPForbidden, router, environ, start_response) self.assertTrue('foo' in why[0], why) def test_call_no_view_registered_no_isettings(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_false(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -206,13 +207,13 @@ class TestRouter(unittest.TestCase): self._registerSettings(debug_notfound=False) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_true(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -220,7 +221,7 @@ class TestRouter(unittest.TestCase): logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue( "debug_notfound of url http://localhost:8080/; path_info: '/', " "context:" in why[0]) @@ -235,7 +236,7 @@ class TestRouter(unittest.TestCase): self.assertTrue("view_name: ''" in message) self.assertTrue("subpath: []" in message) - def test_call_view_returns_nonresponse(self): + def test_call_view_returns_non_iresponse(self): from pyramid.interfaces import IViewClassifier context = DummyContext() self._registerTraverserFactory(context) @@ -246,6 +247,24 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(ValueError, router, environ, start_response) + def test_call_view_returns_adapted_response(self): + from pyramid.response import Response + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponse + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron() + view = DummyView('abc') + self._registerView(view, '', IViewClassifier, None, None) + router = self._makeOne() + start_response = DummyStartResponse() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + app_iter = router(environ, start_response) + self.assertEqual(app_iter, ['abc']) + self.assertEqual(start_response.status, '200 OK') + def test_call_view_registered_nonspecific_default_path(self): from pyramid.interfaces import IViewClassifier context = DummyContext() @@ -323,7 +342,7 @@ class TestRouter(unittest.TestCase): def test_call_view_registered_specific_fail(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.interfaces import IViewClassifier class IContext(Interface): pass @@ -339,12 +358,12 @@ class TestRouter(unittest.TestCase): self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) def test_call_view_raises_forbidden(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden class IContext(Interface): pass from pyramid.interfaces import IRequest @@ -353,12 +372,13 @@ class TestRouter(unittest.TestCase): directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) response = DummyResponse() - view = DummyView(response, raise_exception=Forbidden("unauthorized")) + view = DummyView(response, + raise_exception=HTTPForbidden("unauthorized")) environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(Forbidden, router, environ, start_response) + why = exc_raised(HTTPForbidden, router, environ, start_response) self.assertEqual(why[0], 'unauthorized') def test_call_view_raises_notfound(self): @@ -368,17 +388,17 @@ class TestRouter(unittest.TestCase): pass from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound context = DummyContext() directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) response = DummyResponse() - view = DummyView(response, raise_exception=NotFound("notfound")) + view = DummyView(response, raise_exception=HTTPNotFound("notfound")) environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertEqual(why[0], 'notfound') def test_call_request_has_response_callbacks(self): @@ -566,7 +586,7 @@ class TestRouter(unittest.TestCase): "pattern: 'archives/:action/:article', ")) def test_call_route_match_miss_debug_routematch(self): - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound logger = self._registerLogger() self._registerSettings(debug_routematch=True) self._registerRouteRequest('foo') @@ -577,7 +597,7 @@ class TestRouter(unittest.TestCase): self._registerRootFactory(context) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) self.assertEqual(len(logger.messages), 1) self.assertEqual( @@ -627,11 +647,11 @@ class TestRouter(unittest.TestCase): def test_root_factory_raises_notfound(self): from pyramid.interfaces import IRootFactory - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): - raise NotFound('from root factory') + raise HTTPNotFound('from root factory') self.registry.registerUtility(rootfactory, IRootFactory) class IContext(Interface): pass @@ -640,16 +660,16 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('from root factory' in why[0]) def test_root_factory_raises_forbidden(self): from pyramid.interfaces import IRootFactory - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): - raise Forbidden('from root factory') + raise HTTPForbidden('from root factory') self.registry.registerUtility(rootfactory, IRootFactory) class IContext(Interface): pass @@ -658,7 +678,7 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(Forbidden, router, environ, start_response) + why = exc_raised(HTTPForbidden, router, environ, start_response) self.assertTrue('from root factory' in why[0]) def test_root_factory_exception_propagating(self): @@ -829,7 +849,7 @@ class TestRouter(unittest.TestCase): result = router(environ, start_response) self.assertEqual(result, ["Hello, world"]) - def test_exception_view_returns_non_response(self): + def test_exception_view_returns_non_iresponse(self): from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IExceptionViewClassifier @@ -1054,12 +1074,22 @@ class DummyStartResponse: def __call__(self, status, headers): self.status = status self.headers = headers - -class DummyResponse: + +from pyramid.interfaces import IResponse +from zope.interface import implements + +class DummyResponse(object): + implements(IResponse) headerlist = () app_iter = () + environ = None def __init__(self, status='200 OK'): self.status = status + + def __call__(self, environ, start_response): + self.environ = environ + start_response(self.status, self.headerlist) + return self.app_iter class DummyThreadLocalManager: def __init__(self): diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 17a437af6..5c6454a38 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -90,16 +90,8 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertEqual(session._set_cookie(response), True) self.assertEqual(response.headerlist[-1][0], 'Set-Cookie') - def test__set_cookie_other_kind_of_response(self): - request = testing.DummyRequest() - request.exception = None - session = self._makeOne(request) - session['abc'] = 'x' - response = DummyResponse() - self.assertEqual(session._set_cookie(response), True) - self.assertEqual(len(response.headerlist), 1) - def test__set_cookie_options(self): + from pyramid.response import Response request = testing.DummyRequest() request.exception = None session = self._makeOne(request, @@ -110,10 +102,9 @@ class TestUnencryptedCookieSession(unittest.TestCase): cookie_httponly = True, ) session['abc'] = 'x' - response = DummyResponse() + response = Response() self.assertEqual(session._set_cookie(response), True) - self.assertEqual(len(response.headerlist), 1) - cookieval= response.headerlist[0][1] + cookieval= response.headerlist[-1][1] val, domain, path, secure, httponly = [x.strip() for x in cookieval.split(';')] self.assertTrue(val.startswith('abc=')) diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 58ca2b7d9..159a88ebd 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -150,7 +150,7 @@ class Test_registerView(TestBase): def test_registerView_with_permission_denying(self): from pyramid import testing - from pyramid.exceptions import Forbidden + from pyramid.httpexceptions import HTTPForbidden def view(context, request): """ """ view = testing.registerView('moo.html', view=view, permission='bar') @@ -160,7 +160,7 @@ class Test_registerView(TestBase): from pyramid.view import render_view_to_response request = DummyRequest() request.registry = self.registry - self.assertRaises(Forbidden, render_view_to_response, + self.assertRaises(HTTPForbidden, render_view_to_response, None, request, 'moo.html') def test_registerView_with_permission_denying2(self): diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index b1d48b98b..b42224d4c 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -120,6 +120,19 @@ class RenderViewToIterableTests(BaseTest, unittest.TestCase): secure=True) self.assertEqual(iterable, ()) + def test_call_view_returns_iresponse_adaptable(self): + from pyramid.response import Response + request = self._makeRequest() + context = self._makeContext() + view = make_view('123') + self._registerView(request.registry, view, 'registered') + def str_response(s): + return Response(s) + request.registry.registerAdapter(str_response, (str,), IResponse) + iterable = self._callFUT(context, request, name='registered', + secure=True) + self.assertEqual(iterable, ['123']) + def test_call_view_registered_insecure_no_call_permissive(self): context = self._makeContext() request = self._makeRequest() @@ -185,6 +198,14 @@ class RenderViewTests(BaseTest, unittest.TestCase): self.assertEqual(s, 'anotherview') class TestIsResponse(unittest.TestCase): + def setUp(self): + from zope.deprecation import __show__ + __show__.off() + + def tearDown(self): + from zope.deprecation import __show__ + __show__.on() + def _callFUT(self, *arg, **kw): from pyramid.view import is_response return is_response(*arg, **kw) @@ -536,9 +557,15 @@ def make_view(response): class DummyRequest: exception = None -class DummyResponse: - status = '200 OK' +from pyramid.interfaces import IResponse +from zope.interface import implements + +class DummyResponse(object): + implements(IResponse) headerlist = () + app_iter = () + status = '200 OK' + environ = None def __init__(self, body=None): if body is None: self.app_iter = () diff --git a/pyramid/view.py b/pyramid/view.py index 2563f1e43..afa10fd0f 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -4,11 +4,13 @@ import venusian from zope.interface import providedBy from zope.deprecation import deprecated +from pyramid.interfaces import IResponse from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier from pyramid.httpexceptions import HTTPFound +from pyramid.httpexceptions import default_exceptionresponse_view from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -44,12 +46,12 @@ def render_view_to_response(context, request, name='', secure=True): ``name`` / ``context`` / and ``request``). If `secure`` is ``True``, and the :term:`view callable` found is - protected by a permission, the permission will be checked before - calling the view function. If the permission check disallows view - execution (based on the current :term:`authorization policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised. - The exception's ``args`` attribute explains why the view access - was disallowed. + protected by a permission, the permission will be checked before calling + the view function. If the permission check disallows view execution + (based on the current :term:`authorization policy`), a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised. + The exception's ``args`` attribute explains why the view access was + disallowed. If ``secure`` is ``False``, no permission checking is done.""" provides = [IViewClassifier] + map(providedBy, (request, context)) @@ -87,19 +89,23 @@ def render_view_to_iterable(context, request, name='', secure=True): of this function by calling ``''.join(iterable)``, or just use :func:`pyramid.view.render_view` instead. - If ``secure`` is ``True``, and the view is protected by a - permission, the permission will be checked before the view - function is invoked. If the permission check disallows view - execution (based on the current :term:`authentication policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised; - its ``args`` attribute explains why the view access was - disallowed. + If ``secure`` is ``True``, and the view is protected by a permission, the + permission will be checked before the view function is invoked. If the + permission check disallows view execution (based on the current + :term:`authentication policy`), a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised; its + ``args`` attribute explains why the view access was disallowed. If ``secure`` is ``False``, no permission checking is done.""" response = render_view_to_response(context, request, name, secure) if response is None: return None + try: + reg = request.registry + except AttributeError: + reg = get_current_registry() + response = reg.queryAdapterOrSelf(response, IResponse) return response.app_iter def render_view(context, request, name='', secure=True): @@ -116,12 +122,11 @@ def render_view(context, request, name='', secure=True): ``app_iter`` attribute. This function will return ``None`` if a corresponding view cannot be found. - If ``secure`` is ``True``, and the view is protected by a - permission, the permission will be checked before the view is - invoked. If the permission check disallows view execution (based - on the current :term:`authorization policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised; - its ``args`` attribute explains why the view access was + If ``secure`` is ``True``, and the view is protected by a permission, the + permission will be checked before the view is invoked. If the permission + check disallows view execution (based on the current :term:`authorization + policy`), a :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be + raised; its ``args`` attribute explains why the view access was disallowed. If ``secure`` is ``False``, no permission checking is done.""" @@ -130,21 +135,6 @@ def render_view(context, request, name='', secure=True): return None return ''.join(iterable) -def is_response(ob): - """ Return ``True`` if ``ob`` implements the interface implied by - :ref:`the_response`. ``False`` if not. - - .. note:: This isn't a true interface or subclass check. Instead, it's a - duck-typing check, as response objects are not obligated to be of a - particular class or provide any particular Zope interface.""" - - # response objects aren't obligated to implement a Zope interface, - # so we do it the hard way - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False - class view_config(object): """ A function, class or method :term:`decorator` which allows a developer to create view registrations nearer to a :term:`view @@ -243,14 +233,6 @@ deprecated( 'pyramid.view.view_config instead (API-compat, simple ' 'rename).') -def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - return context - class AppendSlashNotFoundViewFactory(object): """ There can only be one :term:`Not Found view` in any :app:`Pyramid` application. Even if you use @@ -271,14 +253,13 @@ class AppendSlashNotFoundViewFactory(object): .. code-block:: python - from pyramid.exceptions import NotFound - from pyramid.view import AppendSlashNotFoundViewFactory from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): return HTTPNotFound('nope') custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=NotFound) + config.add_view(custom_append_slash, context=HTTPNotFound) The ``notfound_view`` supplied must adhere to the two-argument view callable calling convention of ``(context, request)`` @@ -294,7 +275,7 @@ class AppendSlashNotFoundViewFactory(object): 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 = request.exception + context = getattr(request, 'exception', None) or context path = request.path registry = request.registry mapper = registry.queryUtility(IRoutesMapper) @@ -325,12 +306,27 @@ routes are not considered when attempting to find a matching route. Use the :meth:`pyramid.config.Configurator.add_view` method to configure this view as the Not Found view:: - from pyramid.exceptions import NotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import append_slash_notfound_view - config.add_view(append_slash_notfound_view, context=NotFound) + config.add_view(append_slash_notfound_view, context=HTTPNotFound) See also :ref:`changing_the_notfound_view`. """ +def is_response(ob): + """ Return ``True`` if ``ob`` implements the interface implied by + :ref:`the_response`. ``False`` if not. + + .. warning:: This function is deprecated as of :app:`Pyramid` 1.1. New + code should not use it. Instead, new code should use the + :func:`pyramid.request.Request.is_response` method.""" + if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and + hasattr(ob, 'status') ): + return True + return False +deprecated( + 'is_response', + 'pyramid.view.is_response is deprecated as of Pyramid 1.1. Use ' + 'pyramid.request.Request.is_response instead.') |
