diff options
| author | Chris McDonough <chrism@plope.com> | 2011-06-11 05:35:27 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-06-11 05:35:27 -0400 |
| commit | 99edc51a3b05309c7f5d98ff96289ec51b1d7660 (patch) | |
| tree | c8ddaa62b21c54eb996f5e375abd5bf9f5198806 | |
| parent | df15ed98612e7962e3122da52d8d5f5b9d8882b2 (diff) | |
| download | pyramid-99edc51a3b05309c7f5d98ff96289ec51b1d7660.tar.gz pyramid-99edc51a3b05309c7f5d98ff96289ec51b1d7660.tar.bz2 pyramid-99edc51a3b05309c7f5d98ff96289ec51b1d7660.zip | |
- Pyramid now expects Response objects to have a __call__
method which implements the WSGI application interface
instead of the three webob attrs status, headerlist
and app_iter. Backwards compatibility exists for
code which returns response objects that do not
have a __call__.
- pyramid.response.Response is no longer an exception
(and therefore cannot be raised in order to generate
a response).
- Changed my mind about moving stuff from pyramid.httpexceptions
to pyramid.response. The stuff I moved over has been moved
back to pyramid.httpexceptions.
39 files changed, 1632 insertions, 1595 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 7840bc525..e413f0657 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -37,12 +37,10 @@ Documentation - Added "What's New in Pyramid 1.1" to HTML rendering of documentation. -- Added API docs for ``pyramid.httpexceptions.abort`` and - ``pyramid.httpexceptions.redirect``. +- Added API docs for ``pyramid.httpexceptions.responsecode``. - Added "HTTP Exceptions" section to Views narrative chapter including a - description of ``pyramid.httpexceptions.abort``; adjusted redirect section - to note ``pyramid.httpexceptions.redirect``. + description of ``pyramid.httpexceptions.responsecode``. Features -------- @@ -105,18 +103,15 @@ Features more information. - A default exception view for the context - ``pyramid.interfaces.IExceptionResponse`` (aka - ``pyramid.response.Response`` or ``pyramid.httpexceptions.HTTPException``) - 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. - -- New functions named ``pyramid.httpexceptions.abort`` and - ``pyramid.httpexceptions.redirect`` perform the equivalent of their Pylons - brethren when an HTTP exception handler is registered. These functions - take advantage of the newly registered exception view for - ``webob.exc.HTTPException``. + ``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 @@ -135,28 +130,13 @@ Features - It is now possible to control how the Pyramid router calls the WSGI ``start_response`` callable and obtains the WSGI ``app_iter`` based on adapting the response object to the new ``pyramid.interfaces.IResponder`` - interface. The default ``IResponder`` uses Pyramid 1.0's logic to do this. - To override the responder:: - - from pyramid.interfaces import IResponder - from pyramid.response import Response - from myapp import MyResponder - - config.registry.registerAdapter(MyResponder, (Response,), - IResponder, name='') - - This makes it possible to reuse response object implementations which have, - for example, their own ``__call__`` expected to be used as a WSGI - application (like ``pyramid.response.Response``), e.g.: + interface. See the section in the Hooks chapter of the documentation + entitled "Changing How Pyramid Treats Response Objects". - class MyResponder(object): - def __init__(self, response): - """ Obtain a reference to the response """ - self.response = response - def __call__(self, request, start_response): - """ Call start_response and return an app_iter """ - app_iter = self.response(request.environ, start_response) - return app_iter +- 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. Bug Fixes --------- @@ -291,7 +271,7 @@ Deprecations Behavior Changes ---------------- -- 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 @@ -323,10 +303,25 @@ Behavior Changes result. - ``pyramid.response.Response`` is now a *subclass* of - ``webob.response.Response``. It also inherits from the built-in Python - ``Exception`` class and implements the - ``pyramid.interfaces.IExceptionResponse`` class so it can be raised as an - exception from view code. + ``webob.response.Response`` (in order to directly implement the + ``pyramid.interfaces.IResponse`` interface). + +- The ``pyramid.interfaces.IResponse`` interface now includes a ``__call__`` + method which has the WSGI application call signature (and which expects an + iterable as a result). + +- The Pyramid router now, by default, expects response objects returned from + views to implement the WSGI application interface (a ``__call__`` method + that accepts ``environ`` and ``start_response``, and which returns an + ``app_iter`` iterable). If such a method exists, Pyramid will now call it + in order to satisfy the WSGI request. Backwards compatibility code in the + default responder exists which will fall back to the older behavior, but + Pyramid will raise a deprecation warning if it is reached. See the section + in the Hooks chapter of the documentation entitled "Changing How Pyramid + Treats Response Objects" to default back to the older behavior, where the + ``app_iter``, ``headerlist``, and ``status`` attributes of the object were + consulted directly (without any indirection through ``__call__``) to + silence the deprecation warnings. Dependencies ------------ @@ -1,6 +1,12 @@ Pyramid TODOs ============= +Must-Have +--------- + +- Depend on only __call__ interface or only 3-attr interface in builtin code + that deals with response objects. + Should-Have ----------- diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 73da4126b..325d5af03 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -5,16 +5,14 @@ .. automodule:: pyramid.httpexceptions - .. autofunction:: abort - - .. autofunction:: redirect - .. attribute:: status_map A mapping of integer status code to exception class (eg. the integer "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). + .. autofunction:: responsecode + .. autoclass:: HTTPException .. autoclass:: HTTPOk 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/glossary.rst b/docs/glossary.rst index dbab331c1..079a069b4 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -594,7 +594,7 @@ Glossary Not Found view An :term:`exception view` invoked by :app:`Pyramid` when the - developer explicitly raises a ``pyramid.response.HTTPNotFound`` + 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.response.HTTPForbidden`` 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. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index aa151d281..b6a781417 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.response.HTTPNotFound` 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,7 +31,7 @@ method to register an "exception view": .. code-block:: python :linenos: - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from helloworld.views import notfound_view config.add_view(notfound_view, context=HTTPNotFound) @@ -42,22 +42,22 @@ 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.response.HTTPNotFound` 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: .. code-block:: python :linenos: - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound def notfound_view(request): 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.response.HTTPNotFound` exception that + 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 @@ -67,8 +67,9 @@ Here's some sample code that implements a minimal NotFound view callable: .. 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.response.HTTPNotFound` 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 "not found" view consists -only of naming the :exc:`pyramid.response.HTTPForbidden` 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,7 +97,7 @@ view": :linenos: from helloworld.views import forbidden_view - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden config.add_view(forbidden_view, context=HTTPForbidden) Replace ``helloworld.views.forbidden_view`` with a reference to the Python @@ -122,8 +123,8 @@ Here's some sample code that implements a minimal forbidden view: .. 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.response.HTTPForbidden` exception that - caused the forbidden view to be called. The value of + 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 @@ -532,10 +533,10 @@ Changing How Pyramid Treats Response Objects It is possible to control how the Pyramid :term:`router` calls the WSGI ``start_response`` callable and obtains the WSGI ``app_iter`` based on adapting the response object to the :class: `pyramid.interfaces.IResponder` -interface. The default ``IResponder`` uses the three attributes ``status``, -``headerlist``, and ``app_iter`` attached to the response object, and calls -``start_response`` with the status and headerlist, returning the -``app_iter``. To override the responder:: +interface. The default responder uses the ``__call__`` method of a response +object, passing it the WSGI environ and the WSGI ``start_response`` callable +(the response is assumed to be a WSGI application). To override the +responder:: from pyramid.interfaces import IResponder from pyramid.response import Response @@ -545,8 +546,9 @@ interface. The default ``IResponder`` uses the three attributes ``status``, IResponder, name='') Overriding makes it possible to reuse response object implementations which -have, for example, their own ``__call__`` expected to be used as a WSGI -application (like :class:`pyramid.response.Response`), e.g.: +have, for example, the ``app_iter``, ``headerlist`` and ``status`` attributes +of an object returned as a response instead of trying to use the object's +``__call__`` method:: class MyResponder(object): def __init__(self, response): @@ -554,8 +556,8 @@ application (like :class:`pyramid.response.Response`), e.g.: self.response = response def __call__(self, request, start_response): """ Call start_response and return an app_iter """ - app_iter = self.response(request.environ, start_response) - return app_iter + start_response(self.response.status, self.response.headerlist) + return self.response.app_iter .. index:: single: view mapper diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index c7a3d7837..99ee14908 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -11,7 +11,6 @@ Response interface, :app:`Pyramid` will attempt to use a .. code-block:: python :linenos: - from pyramid.response import Response from pyramid.view import view_config @view_config(renderer='json') @@ -77,39 +76,52 @@ 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 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`. +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.response.HTTPFound` class as a response, no renderer will be -employed. +:class:`pyramid.response.Response` class as a response, no renderer +will be employed. .. code-block:: python :linenos: - from pyramid.response import HTTPFound + from pyramid.response import Response + from pyramid.view import view_config + @view_config(renderer='json') def view(request): - return HTTPFound(location='http://example.com') # any renderer avoided + return Response('OK') # json renderer avoided -Likewise for a "plain old response": +Likewise for an :term:`HTTP exception` response: .. code-block:: python :linenos: - from pyramid.response import Response + from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import view_config + @view_config(renderer='json') def view(request): - return Response('OK') # any renderer avoided + return HTTPFound(location='http://example.com') # json renderer avoided -Mutations to ``request.response`` in views which return a Response object -like this directly (unless that response *is* ``request.response``) will be -ignored. +You can of course also return the ``request.response`` attribute instead to +avoid rendering: + +.. 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) @@ -377,6 +389,31 @@ 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`. diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 44fa9835b..30d54767e 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -82,8 +82,8 @@ processing? 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.response.HTTPNotFound` exception, which is meant to be - caught by a surrounding exception handler. + :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. @@ -95,13 +95,13 @@ processing? 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.response.HTTPForbidden` exception, which is meant to be - called by a surrounding exception handler. + :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.response.HTTPNotFound` or - :class:`~pyramid.response.HTTPForbidden`), the router catches the + (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 diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 862eda9f0..05e851fde 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -191,7 +191,7 @@ function. :linenos: from pyramid.security import has_permission - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden def view_fn(request): if not has_permission('edit', request.context, request): @@ -230,7 +230,7 @@ without needing to invoke the actual application configuration implied by its testing.tearDown() def test_view_fn_forbidden(self): - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=False) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index e5228b81e..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.response.HTTPNotFound') + 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,7 +945,7 @@ view as the first argument to its constructor. For instance: .. code-block:: python :linenos: - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 73a7c2e2a..990828f80 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -230,29 +230,29 @@ 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.response.HTTPFound` is also a valid response object -(see :ref:`http_exceptions` and ref:`http_redirect`). A view can actually -return any object that has the following attributes. - -status - The HTTP status code (including the name) for the response as a string. - E.g. ``200 OK`` or ``401 Unauthorized``. - -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')]`` - -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. - -These attributes form the structure of the "Pyramid Response interface". +You don't need to use :class:`~pyramid.response.Response` to represent a +response. A view can actually return any object that has a ``__call__`` +method that implements the :term:`WSGI` application call interface. For +example, an instance of the following class could be successfully returned by +a view callable as a response object: + +.. code-block:: python + :linenos: + + class SimpleResponse(object): + def __call__(self, environ, start_response): + """ Call the ``start_response`` callback and return + an iterable """ + body = 'Hello World!' + headers = [('Content-Type', 'text/plain'), + ('Content-Length', str(len(body)))] + start_response('200 OK', headers) + return [body] + +: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_exceptions` and ref:`http_redirect`). .. index:: single: view exceptions @@ -269,40 +269,8 @@ logged there. 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. Two categories of special exceptions -exist: internal exceptions and HTTP exceptions. - -Internal Exceptions -~~~~~~~~~~~~~~~~~~~ - -:exc:`pyramid.response.HTTPNotFound` and -:exc:`pyramid.response.HTTPForbidden` are exceptions often raised by Pyramid -itself when it (respectively) cannot find a view to service a request or when -authorization was forbidden by a security policy. However, they can also be -raised by application developers. - -If :exc:`~pyramid.response.HTTPNotFound` is raised within view code, the -result of the :term:`Not Found View` will be returned to the user agent which -performed the request. - -If :exc:`~pyramid.response.HTTPForbidden` is raised within view code, the -result of the :term:`Forbidden View` will be returned to the user agent which -performed the request. - -Both are exception classes which accept a single positional constructor -argument: a ``message``. 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]``. - -An example: - -.. code-block:: python - :linenos: - - from pyramid.response import HTTPNotFound - - def aview(request): - raise HTTPNotFound('not found!') +:app:`Pyramid` to generate a response. These are known as :term:`HTTP +exception` objects. .. index:: single: HTTP exceptions @@ -312,54 +280,77 @@ An example: HTTP Exceptions ~~~~~~~~~~~~~~~ -All classes documented in the :mod:`pyramid.response` module as inheriting -from the :class:`pryamid.response.Response` object implement the -:term:`Response` interface; an instance of any of these classes can be -returned or raised from within a view. The instance will be used as as the -view's response. +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. -For example, the :class:`pyramid.response.HTTPUnauthorized` exception +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.response import HTTPUnauthorized + from pyramid.httpexceptions import HTTPUnauthorized def aview(request): raise HTTPUnauthorized() -A shortcut for importing and raising an HTTP exception is the -:func:`pyramid.response.abort` function. This function accepts an HTTP -status code and raises the corresponding HTTP exception. For example, to -raise HTTPUnauthorized, instead of the above, you could do: +An HTTP exception, instead of being raised, can alternately be *returned* +(HTTP exceptions are also valid response objects): .. code-block:: python :linenos: - from pyramid.response import abort + from pyramid.httpexceptions import HTTPUnauthorized def aview(request): - abort(401) - -This is the case because ``401`` is the HTTP status code for "HTTP -Unauthorized". Therefore, ``abort(401)`` is functionally equivalent to -``raise HTTPUnauthorized()``. Other exceptions in -:mod:`pyramid.response` can be raised via -:func:`pyramid.response.abort` as well, as long as the status code -associated with the exception is provided to the function. + return HTTPUnauthorized() -An HTTP exception, instead of being raised, can alternately be *returned* -(HTTP exceptions are also valid response objects): +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. .. code-block:: python :linenos: - from pyramid.response import HTTPUnauthorized + from pyramid.httpexceptions import responsecode def aview(request): - return HTTPUnauthorized() + 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`. + +How Pyramid Uses HTTP Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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.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. + +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 @@ -369,11 +360,10 @@ An HTTP exception, instead of being raised, can alternately be *returned* Custom Exception Views ---------------------- -The machinery which allows :exc:`~pyramid.response.HTTPNotFound`, -:exc:`~pyramid.response.HTTPForbidden` and other responses to be used as -exceptions 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. +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 @@ -409,8 +399,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: @@ -458,57 +448,34 @@ Exception views can be configured with any view registration mechanism: Using a View Callable to Do an HTTP Redirect -------------------------------------------- -Two methods exist to redirect to another URL from within a view callable: a -short form and a long form. The short form should be preferred when -possible. - -Short Form -~~~~~~~~~~ - -You can issue an HTTP redirect from within a view callable by using the -:func:`pyramid.response.redirect` function. This function raises an -:class:`pyramid.response.HTTPFound` exception (a "302"), which is caught by -the default exception response handler and turned into a response. - -.. code-block:: python - :linenos: - - from pyramid.response import redirect - - def myview(request): - redirect('http://example.com') - -Long Form -~~~~~~~~~ - -You can issue an HTTP redirect from within a view "by hand" instead of -relying on the :func:`pyramid.response.redirect` function to do it for -you. +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.response.HTTPFound` +To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` instance. .. code-block:: python :linenos: - from pyramid.response import HTTPFound + from pyramid.httpexceptions import HTTPFound def myview(request): return HTTPFound(location='http://example.com') -Or, alternately, you can *raise* an HTTPFound exception instead of returning -one. +Alternately, you can *raise* an HTTPFound exception instead of returning one. .. code-block:: python :linenos: - from pyramid.response import HTTPFound + from pyramid.httpexceptions import HTTPFound def myview(request): raise HTTPFound(location='http://example.com') -The above form of generating a response by raising HTTPFound is completely -equivalent to ``redirect('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 6cd9418ce..70ab5eea8 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -362,20 +362,21 @@ 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.response` module. This import location contains subclasses and -replacements that mirror those in the original ``webob.exc``. +:mod:`pyramid.httpexceptions` module. This import location contains +subclasses and replacements that mirror those in the original ``webob.exc``. -Each class is named ``pyramid.response.HTTP*``, where ``*`` is the reason for -the error. For instance, :class:`pyramid.response.HTTPNotFound`. It -subclasses :class:`pyramid.Response`, so you can manipulate the instances in -the same way. A typical example is: +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 :linenos: - from pyramid.response import HTTPNotFound - from pyramid.response import HTTPMovedPermanently + from pyramid.httpexceptions import HTTPNotFound + from pyramid.httpexceptions import HTTPMovedPermanently response = HTTPNotFound('There is no such resource') # or: @@ -385,7 +386,7 @@ 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.response` 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 3b102958e..de5c9486d 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -131,17 +131,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.response.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. +``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 ea8842294..b6c083bbf 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -83,10 +83,10 @@ 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.response.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 +: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 ``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 822b19b9e..334115880 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/login.py @@ -1,4 +1,4 @@ -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.security import remember from pyramid.security import forget @@ -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.response.HTTPForbidden', +@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/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 67550d58e..a83e17de4 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -1,7 +1,7 @@ from docutils.core import publish_parts import re -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.url import resource_url from pyramid.view import view_config from pyramid.security import authenticated_userid diff --git a/docs/tutorials/wiki/src/views/tutorial/views.py b/docs/tutorials/wiki/src/views/tutorial/views.py index d72cbd3fd..42420f2fe 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views.py +++ b/docs/tutorials/wiki/src/views/tutorial/views.py @@ -1,7 +1,7 @@ from docutils.core import publish_parts import re -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.url import resource_url from pyramid.view import view_config diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 32e3c0b24..832f90b92 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -90,8 +90,8 @@ path to our "FrontPage". :language: python The ``view_wiki`` function returns an instance of the -:class:`pyramid.response.HTTPFound` class (instances of which implement the -WebOb :term:`response` interface), It will use the +:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement +the WebOb :term:`response` interface), 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 42013622c..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.response.HTTPForbidden', + context='pyramid.httpexceptions.HTTPForbidden', renderer='tutorial:templates/login.pt') return config.make_wsgi_app() diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/login.py b/docs/tutorials/wiki2/src/authorization/tutorial/login.py index 2bc8a7201..7a1d1f663 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/login.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/login.py @@ -1,4 +1,4 @@ -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.security import remember from pyramid.security import forget from pyramid.url import route_url diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index ed441295c..5abd8391e 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.security import authenticated_userid from pyramid.url import route_url diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index 80d817d99..b8896abe7 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -2,7 +2,7 @@ import re from docutils.core import publish_parts -from pyramid.response import HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.url import route_url from tutorial.models import DBSession diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 488328519..533ae3637 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -63,12 +63,6 @@ Default HTTP Exception View from within view code; when raised, this exception view will render the exception to a response. - New convenience functions named :func:`pyramid.httpexceptions.abort` and - :func:`pyramid.httpexceptions.redirect` perform the equivalent of their - Pylons brethren when an HTTP exception handler is registered. These - functions take advantage of the newly registered exception view for - :exc:`webob.exc.HTTPException`. - 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 @@ -81,6 +75,10 @@ Default HTTP Exception View Minor Feature Additions ----------------------- +- 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, @@ -177,7 +175,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 diff --git a/pyramid/config.py b/pyramid/config.py index ab1729c06..91ba414b3 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -56,10 +56,10 @@ from pyramid.compat import md5 from pyramid.compat import any from pyramid.events import ApplicationCreated from pyramid.exceptions import ConfigurationError -from pyramid.response import default_exceptionresponse_view -from pyramid.response import HTTPForbidden -from pyramid.response import HTTPNotFound 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 diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 2484f94a3..151fc241f 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,12 +1,12 @@ from zope.configuration.exceptions import ConfigurationError as ZCE -from pyramid.response import HTTPNotFound -from pyramid.response import HTTPForbidden +from pyramid.httpexceptions import HTTPNotFound +from pyramid.httpexceptions import HTTPForbidden NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat -class PredicateMismatch(NotFound): +class PredicateMismatch(HTTPNotFound): """ Internal exception (not an API) raised by multiviews when no view matches. This exception subclasses the ``NotFound`` diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index dbb530b4a..a692380f8 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -110,7 +110,907 @@ field. Reflecting this, these subclasses have one additional keyword argument: ``location``, which indicates the location to which to redirect. """ -from pyramid.response import * # API +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 + pass + +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: + # - not a WSGI application (just a response) + # + # as a result: + # + # - bases plaintext vs. html result on self.content_type rather than + # on request accept header + # + # - doesn't add request.environ keys to template substitutions unless + # 'request' is passed as a constructor keyword argument. + # + # - doesn't use "strip_tags" (${br} placeholder for <br/>, no other html + # in default body template) + # + # - sets a default app_iter if no body, app_iter, or unicode_body is + # passed using a template (ala 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 + elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): + self.app_iter = self._default_app_iter() + + def __str__(self): + return self.detail or self.explanation + + def _default_app_iter(self): + # This is a generator which defers the creation of the response page + # body; we use a generator because we want to ensure that if + # attributes of this response are changed after it is constructed, we + # use the changed values rather than the values at time of construction + # (e.g. self.content_type or self.charset). + 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 + environ = self.environ + if environ is not None: + 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) + yield page + raise StopIteration + + @property + def exception(self): + # bw compat only + return self + wsgi_response = exception # bw compat only + +class HTTPError(WSGIHTTPException): + """ + base class for status codes in the 400's and 500's + + This is an exception which indicates that an error has occurred, + and that any work in progress should not be committed. These are + typically results in the 400's and 500's. + """ + +class HTTPRedirection(WSGIHTTPException): + """ + base class for 300's status code (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 the 200's status code (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 400's, 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 500's, where the server is in-error + + This is an error condition in which the server is presumed to be + in-error. This is usually unexpected, and thus requires a traceback; + ideally, opening a support ticket for the customer. 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 d5d382492..b8ff2d4c9 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -46,11 +46,15 @@ class IApplicationCreated(Interface): IWSGIApplicationCreatedEvent = IApplicationCreated # b /c -class IResponse(Interface): # not an API +class IResponse(Interface): status = Attribute('WSGI status code of response') headerlist = Attribute('List of response headers') app_iter = Attribute('Iterable representing the response body') + def __call__(environ, start_response): + """ WSGI call interface, should call the start_response callback + and should return an iterable """ + class IException(Interface): # not an API """ An interface representing a generic exception """ @@ -60,8 +64,8 @@ class IExceptionResponse(IException, IResponse): 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.response.HTTPNotFound` and - :class:`pyramid.response.HTTPForbidden`).""" + :class:`pyramid.httpexceptions.HTTPNotFound` and + :class:`pyramid.httpexceptions.HTTPForbidden`).""" class IBeforeRender(Interface): """ @@ -282,11 +286,7 @@ class IExceptionViewClassifier(Interface): class IView(Interface): def __call__(context, request): - """ Must return an object that implements IResponse. May - optionally raise ``pyramid.response.HTTPForbidden`` if an - authorization failure is detected during view execution or - ``pyramid.response.HTTPNotFound`` 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/response.py b/pyramid/response.py index 6e6af32c8..1d2ef296f 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,947 +1,7 @@ -import types -from string import Template - from webob import Response as _Response -from webob import html_escape as _html_escape from zope.interface import implements -from zope.configuration.exceptions import ConfigurationError as ZCE - -from pyramid.interfaces import IExceptionResponse - -class Response(_Response, Exception): - implements(IExceptionResponse) - -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 - pass - -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: - # - not a WSGI application (just a response) - # - # as a result: - # - # - bases plaintext vs. html result on self.content_type rather than - # on request accept header - # - # - doesn't add request.environ keys to template substitutions unless - # 'request' is passed as a constructor keyword argument. - # - # - doesn't use "strip_tags" (${br} placeholder for <br/>, no other html - # in default body template) - # - # - sets a default app_iter if no body, app_iter, or unicode_body is - # passed using a template (ala 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). - 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 - elif not ('unicode_body' in kw or 'body' in kw or 'app_iter' in kw): - self.app_iter = self._default_app_iter() - - def __str__(self): - return self.detail or self.explanation - - def _default_app_iter(self): - # This is a generator which defers the creation of the response page - # body; we use a generator because we want to ensure that if - # attributes of this response are changed after it is constructed, we - # use the changed values rather than the values at time of construction - # (e.g. self.content_type or self.charset). - 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 - environ = self.environ - if environ is not None: - 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) - yield page - raise StopIteration - - @property - def exception(self): - # bw compat only - return self - wsgi_response = exception # bw compat only - -class HTTPError(WSGIHTTPException): - """ - base class for status codes in the 400's and 500's - - This is an exception which indicates that an error has occurred, - and that any work in progress should not be committed. These are - typically results in the 400's and 500's. - """ - -class HTTPRedirection(WSGIHTTPException): - """ - base class for 300's status code (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 the 200's status code (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 400's, 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 +from pyramid.interfaces import IResponse - 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. +class Response(_Response): + implements(IResponse) - 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 500's, where the server is in-error - - This is an error condition in which the server is presumed to be - in-error. This is usually unexpected, and thus requires a traceback; - ideally, opening a support ticket for the customer. 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') - -NotFound = HTTPNotFound # bw compat -Forbidden = HTTPForbidden # bw compat - -class PredicateMismatch(NotFound): - """ - Internal exception (not an API) raised by multiviews when no - view matches. This exception subclasses the ``NotFound`` - exception only one reason: if it reaches the main exception - handler, it should be treated like a ``NotFound`` by any exception - view registrations. - """ - -class URLDecodeError(UnicodeDecodeError): - """ - This exception is raised when :app:`Pyramid` cannot - successfully decode a URL or a URL path segment. This exception - it behaves just like the Python builtin - :exc:`UnicodeDecodeError`. It is a subclass of the builtin - :exc:`UnicodeDecodeError` exception only for identity purposes, - mostly so an exception view can be registered when a URL cannot be - decoded. - """ - -class ConfigurationError(ZCE): - """ Raised when inappropriate input values are supplied to an API - method of a :term:`Configurator`""" - - -def abort(status_code, **kw): - """Aborts the request immediately by raising an HTTP exception based on a - status code. Example:: - - abort(404) # raises an HTTPNotFound exception. - - The values passed as ``kw`` are provided to the exception's constructor. - """ - exc = status_map[status_code](**kw) - raise exc - - -def redirect(url, code=302, **kw): - """Raises an :class:`~HTTPFound` (302) redirect exception to the - URL specified by ``url``. - - Optionally, a code variable may be passed with the status code of - the redirect, ie:: - - redirect(route_url('foo', request), code=303) - - The values passed as ``kw`` are provided to the exception constructor. - - """ - exc = status_map[code] - raise exc(location=url, **kw) - -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/router.py b/pyramid/router.py index 92c6cc920..4d2750efb 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,3 +1,5 @@ +import warnings + from zope.interface import implements from zope.interface import providedBy @@ -17,7 +19,7 @@ from pyramid.interfaces import IResponder from pyramid.events import ContextFound from pyramid.events import NewRequest from pyramid.events import NewResponse -from pyramid.response import HTTPNotFound +from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory @@ -203,6 +205,11 @@ class Router(object): def default_responder(response): def inner(request, start_response): + # __call__ is default 1.1 response API + call = getattr(response, '__call__', None) + if call is not None: + return call(request.environ, start_response) + # start 1.0 bw compat (use headerlist, app_iter, status) try: headers = response.headerlist app_iter = response.app_iter @@ -212,6 +219,14 @@ def default_responder(response): 'Non-response object returned from view ' '(and no renderer): %r' % (response)) start_response(status, headers) + warnings.warn( + 'As of Pyramid 1.1, an object used as a response object is ' + 'required to have a "__call__" method if an IResponder adapter is ' + 'not registered for its type. See "Deprecations" in "What\'s New ' + 'in Pyramid 1.1" within the general Pyramid documentation for ' + 'further details.', + DeprecationWarning, + 3) return app_iter return inner diff --git a/pyramid/testing.py b/pyramid/testing.py index 4d7dd252a..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.response import HTTPForbidden +from pyramid.httpexceptions import HTTPForbidden from pyramid.response import Response from pyramid.registry import Registry from pyramid.security import Authenticated diff --git a/pyramid/tests/fixtureapp/views.py b/pyramid/tests/fixtureapp/views.py index 3125c972f..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.response import HTTPForbidden +from pyramid.httpexceptions import HTTPForbidden def fixture_view(context, request): """ """ diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index 9ad2dc801..7001b87f5 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,5 +1,5 @@ from webob import Response -from pyramid.response import HTTPForbidden +from pyramid.httpexceptions import HTTPForbidden def x_view(request): # pragma: no cover return Response('this is private!') diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 6817c5936..703a2577c 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -50,7 +50,7 @@ class ConfiguratorTests(unittest.TestCase): return iface def _assertNotFound(self, wrapper, *arg): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound self.assertRaises(HTTPNotFound, wrapper, *arg) def _registerEventListener(self, config, event_iface=None): @@ -205,7 +205,7 @@ class ConfiguratorTests(unittest.TestCase): def test_ctor_httpexception_view_default(self): from pyramid.interfaces import IExceptionResponse - from pyramid.response import default_exceptionresponse_view + from pyramid.httpexceptions import default_exceptionresponse_view from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable(config, @@ -321,7 +321,7 @@ class ConfiguratorTests(unittest.TestCase): def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.registry import Registry reg = Registry() config = self._makeOne(reg, autocommit=True) @@ -1695,7 +1695,7 @@ class ConfiguratorTests(unittest.TestCase): self._assertNotFound(wrapper, None, request) def test_add_view_with_header_val_missing(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound view = lambda *arg: 'OK' config = self._makeOne(autocommit=True) config.add_view(view=view, header=r'Host:\d') @@ -2229,7 +2229,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) @@ -2243,7 +2243,7 @@ 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.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) @@ -2259,7 +2259,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.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_notfound_view(view, @@ -2278,12 +2278,13 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.response 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') @@ -2291,13 +2292,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.response 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)) @@ -2306,7 +2308,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.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_forbidden_view(view, @@ -2315,7 +2317,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: @@ -3685,7 +3687,7 @@ class TestViewDeriver(unittest.TestCase): "None against context None): True") def test_debug_auth_permission_authpol_denied(self): - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden view = lambda *arg: 'OK' self.config.registry.settings = dict( debug_authorization=True, reload_templates=True) @@ -3700,7 +3702,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 " @@ -3813,7 +3815,7 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(predicates, [True, True]) def test_with_predicates_notall(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound view = lambda *arg: 'OK' predicates = [] def predicate1(context, request): @@ -4621,14 +4623,14 @@ class TestMultiView(unittest.TestCase): self.assertEqual(mv.get_views(request), mv.views) def test_match_not_found(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_fails(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() def view(context, request): """ """ @@ -4650,7 +4652,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, view) def test_permitted_no_views(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() @@ -4677,7 +4679,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, False) def test__call__not_found(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() @@ -4699,7 +4701,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test___call__raise_not_found_isnt_interpreted_as_pred_mismatch(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() @@ -4724,7 +4726,7 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test__call_permissive__not_found(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 673fb6712..50182ee5c 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -1,5 +1,16 @@ import unittest +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): from pyramid.exceptions import NotFound @@ -14,7 +25,7 @@ class TestNotFound(unittest.TestCase): def test_response_equivalence(self): from pyramid.exceptions import NotFound - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound self.assertTrue(NotFound is HTTPNotFound) class TestForbidden(unittest.TestCase): @@ -31,6 +42,6 @@ class TestForbidden(unittest.TestCase): def test_response_equivalence(self): from pyramid.exceptions import Forbidden - from pyramid.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden self.assertTrue(Forbidden is HTTPForbidden) diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 28adc9d3d..629bbe225 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -1,9 +1,277 @@ import unittest -class TestIt(unittest.TestCase): - def test_bwcompat_imports(self): - from pyramid.httpexceptions import HTTPNotFound as one - from pyramid.response import HTTPNotFound as two - self.assertTrue(one is two) +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') + body = list(exc.app_iter)[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' + body = list(exc.app_iter)[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__default_app_iter_no_comment_plain(self): + cls = self._getTargetSubclass() + exc = cls() + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[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' + body = list(exc._default_app_iter())[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' + body = list(exc._default_app_iter())[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' + body = list(exc._default_app_iter())[0] + self.assertTrue('<!-- comment & comment -->' in body) + + def test_custom_body_template_no_environ(self): + cls = self._getTargetSubclass() + exc = cls(body_template='${location}', location='foo') + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nfoo') + + def test_custom_body_template_with_environ(self): + cls = self._getTargetSubclass() + from pyramid.request import Request + request = Request.blank('/') + exc = cls(body_template='${REQUEST_METHOD}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[0] + self.assertEqual(body, '200 OK\n\nGET') + + def test_body_template_unicode(self): + from pyramid.request import Request + cls = self._getTargetSubclass() + la = unicode('/La Pe\xc3\xb1a', 'utf-8') + request = Request.blank('/') + request.environ['unicodeval'] = la + exc = cls(body_template='${unicodeval}', request=request) + exc.content_type = 'text/plain' + body = list(exc._default_app_iter())[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(): + exc = v() + exc.content_type = content_type + result = list(exc.app_iter)[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 diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py index 6cc87fc0a..46eb298d1 100644 --- a/pyramid/tests/test_response.py +++ b/pyramid/tests/test_response.py @@ -5,304 +5,13 @@ class TestResponse(unittest.TestCase): from pyramid.response import Response return Response - def test_implements_IExceptionResponse(self): - from pyramid.interfaces import IExceptionResponse - Response = self._getTargetClass() - self.failUnless(IExceptionResponse.implementedBy(Response)) - - def test_provides_IExceptionResponse(self): - from pyramid.interfaces import IExceptionResponse - response = self._getTargetClass()() - self.failUnless(IExceptionResponse.providedBy(response)) - -class Test_abort(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.response import abort - return abort(*arg, **kw) - - def test_status_404(self): - from pyramid.response import HTTPNotFound - self.assertRaises(HTTPNotFound, self._callFUT, 404) - - def test_status_201(self): - from pyramid.response import HTTPCreated - self.assertRaises(HTTPCreated, self._callFUT, 201) - - def test_extra_kw(self): - from pyramid.response import HTTPNotFound - try: - self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound, exc: - self.assertEqual(exc.headers['abc'], 'def') - else: # pragma: no cover - raise AssertionError - -class Test_redirect(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.response import redirect - return redirect(*arg, **kw) - - def test_default(self): - from pyramid.response import HTTPFound - try: - self._callFUT('http://example.com') - except HTTPFound, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - - def test_custom_code(self): - from pyramid.response import HTTPMovedPermanently - try: - self._callFUT('http://example.com', 301) - except HTTPMovedPermanently, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '301 Moved Permanently') - - def test_extra_kw(self): - from pyramid.response import HTTPFound - try: - self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - self.assertEqual(exc.headers['abc'], 'def') - - -class Test_default_exceptionresponse_view(unittest.TestCase): - def _callFUT(self, context, request): - from pyramid.response 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.response 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.response import WSGIHTTPException - return WSGIHTTPException - - def _getTargetSubclass(self, code='200', title='OK', - explanation='explanation', empty_body=False): + def test_implements_IResponse(self): + from pyramid.interfaces import IResponse 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_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') + self.failUnless(IResponse.implementedBy(cls)) - 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') - body = list(exc.app_iter)[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' - body = list(exc.app_iter)[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__default_app_iter_no_comment_plain(self): - cls = self._getTargetSubclass() - exc = cls() - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[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' - body = list(exc._default_app_iter())[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' - body = list(exc._default_app_iter())[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' - body = list(exc._default_app_iter())[0] - self.assertTrue('<!-- comment & comment -->' in body) - - def test_custom_body_template_no_environ(self): - cls = self._getTargetSubclass() - exc = cls(body_template='${location}', location='foo') - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\nfoo') - - def test_custom_body_template_with_environ(self): - cls = self._getTargetSubclass() - from pyramid.request import Request - request = Request.blank('/') - exc = cls(body_template='${REQUEST_METHOD}', request=request) - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\nGET') - - def test_body_template_unicode(self): - from pyramid.request import Request - cls = self._getTargetSubclass() - la = unicode('/La Pe\xc3\xb1a', 'utf-8') - request = Request.blank('/') - request.environ['unicodeval'] = la - exc = cls(body_template='${unicodeval}', request=request) - exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] - self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') - -class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): - def _doit(self, content_type): - from pyramid.response import status_map - L = [] - self.assertTrue(status_map) - for v in status_map.values(): - exc = v() - exc.content_type = content_type - result = list(exc.app_iter)[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.response 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.response 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 + 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 a89de7a36..765a26751 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -2,6 +2,19 @@ import unittest from pyramid import testing +def hide_warnings(wrapped): + import warnings + def wrapper(*arg, **kw): + warnings.filterwarnings('ignore') + try: + wrapped(*arg, **kw) + finally: + warnings.resetwarnings() + wrapper.__name__ = wrapped.__name__ + wrapper.__doc__ = wrapped.__doc__ + return wrapper + + class TestRouter(unittest.TestCase): def setUp(self): testing.setUp() @@ -136,7 +149,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(router.request_factory, DummyRequestFactory) def test_call_traverser_default(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() logger = self._registerLogger() router = self._makeOne() @@ -147,7 +160,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(len(logger.messages), 0) def test_traverser_raises_notfound_class(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=HTTPNotFound) @@ -156,7 +169,7 @@ class TestRouter(unittest.TestCase): self.assertRaises(HTTPNotFound, router, environ, start_response) def test_traverser_raises_notfound_instance(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=HTTPNotFound('foo')) @@ -166,26 +179,27 @@ class TestRouter(unittest.TestCase): self.assertTrue('foo' in why[0], why) def test_traverser_raises_forbidden_class(self): - from pyramid.response 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.response 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.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -198,7 +212,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_false(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -212,7 +226,7 @@ class TestRouter(unittest.TestCase): self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_true(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -323,7 +337,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.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.interfaces import IViewClassifier class IContext(Interface): pass @@ -344,7 +358,7 @@ class TestRouter(unittest.TestCase): def test_call_view_raises_forbidden(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.response import Forbidden + from pyramid.httpexceptions import HTTPForbidden class IContext(Interface): pass from pyramid.interfaces import IRequest @@ -353,12 +367,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,7 +383,7 @@ class TestRouter(unittest.TestCase): pass from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound context = DummyContext() directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) @@ -597,7 +612,7 @@ class TestRouter(unittest.TestCase): "pattern: 'archives/:action/:article', ")) def test_call_route_match_miss_debug_routematch(self): - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound logger = self._registerLogger() self._registerSettings(debug_routematch=True) self._registerRouteRequest('foo') @@ -658,7 +673,7 @@ class TestRouter(unittest.TestCase): def test_root_factory_raises_notfound(self): from pyramid.interfaces import IRootFactory - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): @@ -676,11 +691,11 @@ class TestRouter(unittest.TestCase): def test_root_factory_raises_forbidden(self): from pyramid.interfaces import IRootFactory - from pyramid.response 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 @@ -689,7 +704,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): @@ -1057,6 +1072,52 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) +class Test_default_responder(unittest.TestCase): + def _makeOne(self, response): + from pyramid.router import default_responder + return default_responder(response) + + def test_has_call(self): + response = DummyResponse() + response.app_iter = ['123'] + response.headerlist = [('a', '1')] + responder = self._makeOne(response) + request = DummyRequest({'a':'1'}) + start_response = DummyStartResponse() + app_iter = responder(request, start_response) + self.assertEqual(app_iter, response.app_iter) + self.assertEqual(start_response.status, response.status) + self.assertEqual(start_response.headers, response.headerlist) + self.assertEqual(response.environ, request.environ) + + @hide_warnings + def test_without_call_success(self): + response = DummyResponseWithoutCall() + response.app_iter = ['123'] + response.headerlist = [('a', '1')] + responder = self._makeOne(response) + request = DummyRequest({'a':'1'}) + start_response = DummyStartResponse() + app_iter = responder(request, start_response) + self.assertEqual(app_iter, response.app_iter) + self.assertEqual(start_response.status, response.status) + self.assertEqual(start_response.headers, response.headerlist) + + @hide_warnings + def test_without_call_exception(self): + response = DummyResponseWithoutCall() + del response.status + responder = self._makeOne(response) + request = DummyRequest({'a':'1'}) + start_response = DummyStartResponse() + self.assertRaises(ValueError, responder, request, start_response) + + +class DummyRequest(object): + def __init__(self, environ=None): + if environ is None: environ = {} + self.environ = environ + class DummyContext: pass @@ -1085,12 +1146,20 @@ class DummyStartResponse: def __call__(self, status, headers): self.status = status self.headers = headers - -class DummyResponse: + +class DummyResponseWithoutCall: headerlist = () app_iter = () def __init__(self, status='200 OK'): self.status = status + +class DummyResponse(DummyResponseWithoutCall): + environ = None + + 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_testing.py b/pyramid/tests/test_testing.py index 0288884b7..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.response import HTTPForbidden + from pyramid.httpexceptions import HTTPForbidden def view(context, request): """ """ view = testing.registerView('moo.html', view=view, permission='bar') diff --git a/pyramid/view.py b/pyramid/view.py index 0b5c7cdc9..9a4be7580 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -8,8 +8,8 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.response import HTTPFound -from pyramid.response import default_exceptionresponse_view +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 @@ -45,12 +45,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.response.HTTPForbidden` 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)) @@ -88,13 +88,12 @@ 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.response.HTTPForbidden` 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.""" @@ -117,12 +116,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.response.HTTPForbidden` 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.""" @@ -249,7 +247,7 @@ class AppendSlashNotFoundViewFactory(object): .. code-block:: python - from pyramid.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): return HTTPNotFound('nope') @@ -302,7 +300,7 @@ 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.response import HTTPNotFound + from pyramid.httpexceptions import HTTPNotFound from pyramid.view import append_slash_notfound_view config.add_view(append_slash_notfound_view, context=HTTPNotFound) |
