From 1ffb8e3cc21603b29ccd78152f82cca7f61a09b1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 16 May 2011 02:11:56 -0400 Subject: - Added API docs for ``pyramid.httpexceptions.abort`` and ``pyramid.httpexceptions.redirect``. - Added "HTTP Exceptions" section to Views narrative chapter including a description of ``pyramid.httpexceptions.abort``; adjusted redirect section to note ``pyramid.httpexceptions.redirect``. - A default exception view for the context ``webob.exc.HTTPException`` (aka ``pyramid.httpexceptions.HTTPException``) is now registered by default. This means that an instance of any exception 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``. - The Configurator now accepts an additional keyword argument named ``httpexception_view``. By default, this argument is populated with a default exception view function that will be used when an HTTP exception is raised. When ``None`` is passed for this value, an exception view for HTTP exceptions will not be registered. Passing ``None`` returns the behavior of raising an HTTP exception to that of Pyramid 1.0 (the exception will propagate to middleware and to the WSGI server). --- CHANGES.txt | 28 ++++ TODO.txt | 4 - docs/api/httpexceptions.rst | 4 + docs/glossary.rst | 6 + docs/narr/views.rst | 249 +++++++++++++++++++++++++---------- docs/whatsnew-1.1.rst | 35 +++++ pyramid/config.py | 25 +++- pyramid/httpexceptions.py | 33 +++++ pyramid/tests/test_config.py | 46 +++++++ pyramid/tests/test_httpexceptions.py | 79 +++++++++++ 10 files changed, 432 insertions(+), 77 deletions(-) create mode 100644 pyramid/tests/test_httpexceptions.py diff --git a/CHANGES.txt b/CHANGES.txt index 0992af9ef..756d1345c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -37,6 +37,13 @@ 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 "HTTP Exceptions" section to Views narrative chapter including a + description of ``pyramid.httpexceptions.abort``; adjusted redirect section + to note ``pyramid.httpexceptions.redirect``. + Features -------- @@ -97,6 +104,27 @@ Features section entitled "Static Routes" in the URL Dispatch narrative chapter for more information. +- A default exception view for the context ``webob.exc.HTTPException`` (aka + ``pyramid.httpexceptions.HTTPException``) is now registered by default. + This means that an instance of any exception 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``. + +- The Configurator now accepts an additional keyword argument named + ``httpexception_view``. By default, this argument is populated with a + default exception view function that will be used when an HTTP exception is + raised. When ``None`` is passed for this value, an exception view for HTTP + exceptions will not be registered. Passing ``None`` returns the behavior + of raising an HTTP exception to that of Pyramid 1.0 (the exception will + propagate to middleware and to the WSGI server). + Bug Fixes --------- diff --git a/TODO.txt b/TODO.txt index 0f7d6342c..d85f3b7f0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,10 +4,6 @@ Pyramid TODOs Should-Have ----------- -- Consider adding a default exception view for HTTPException and attendant - ``redirect`` and ``abort`` functions ala Pylons (promised Mike I'd enable - this in 1.1). - - Add narrative docs for wsgiapp and wsgiapp2. Nice-to-Have diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 57ca8092c..73da4126b 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -5,6 +5,10 @@ .. automodule:: pyramid.httpexceptions + .. autofunction:: abort + + .. autofunction:: redirect + .. attribute:: status_map A mapping of integer status code to exception class (eg. the diff --git a/docs/glossary.rst b/docs/glossary.rst index e1e9e76a9..797343e5e 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -618,6 +618,12 @@ Glossary request processing. See :ref:`exception_views` for more information. + HTTP Exception + The set of exception classes defined in :mod:`pyramid.httpexceptions`. + These can be used to generate responses with various status codes when + raised or returned from a :term:`view callable`. See also + :ref:`http_exceptions`. + thread local A thread-local variable is one which is essentially a global variable in terms of how it is accessed and treated, however, each `thread diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 5c9bd91af..465cd3c0d 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -51,14 +51,14 @@ the request object contains everything your application needs to know about the specific HTTP request being made. A view callable's ultimate responsibility is to create a :mod:`Pyramid` -:term:`Response` object. This can be done by creating the response -object in the view callable code and returning it directly, as we will -be doing in this chapter. However, if a view callable does not return a -response itself, it can be configured to use a :term:`renderer` that -converts its return value into a :term:`Response` object. Using -renderers is the common way that templates are used with view callables -to generate markup. See the :ref:`renderers_chapter` chapter for -details. +:term:`Response` object. This can be done by creating the response object in +the view callable code and returning it directly, as we will be doing in this +chapter. However, if a view callable does not return a response itself, it +can be configured to use a :term:`renderer` that converts its return value +into a :term:`Response` object. Using renderers is the common way that +templates are used with view callables to generate markup: see the +:ref:`renderers_chapter` chapter for details. In some cases, a response may +also be generated by raising an exception within a view callable. .. index:: single: view calling convention @@ -234,8 +234,8 @@ You don't need to always use :class:`~pyramid.response.Response` to represent a response. :app:`Pyramid` provides a range of different "exception" classes which can act as response objects too. For example, an instance of the class :class:`pyramid.httpexceptions.HTTPFound` is also a valid response object -(see :ref:`http_redirect`). A view can actually return any object that has -the following attributes. +(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. @@ -254,46 +254,6 @@ app_iter These attributes form the structure of the "Pyramid Response interface". -.. index:: - single: view http redirect - single: http redirect (from a view) - -.. _http_redirect: - -Using a View Callable to Do an HTTP Redirect --------------------------------------------- - -You can issue an HTTP redirect from within a view by returning a particular -kind of response. - -.. code-block:: python - :linenos: - - from pyramid.httpexceptions import HTTPFound - - def myview(request): - return HTTPFound(location='http://example.com') - -All exception types from the :mod:`pyramid.httpexceptions` module implement -the :term:`Response` interface; any can be returned as the response from a -view. See :mod:`pyramid.httpexceptions` for the documentation for the -``HTTPFound`` exception; it also includes other response types that imply -other HTTP response codes, such as ``HTTPUnauthorized`` for ``401 -Unauthorized``. - -.. note:: - - Although exception types from the :mod:`pyramid.httpexceptions` module are - in fact bona fide Python :class:`Exception` types, the :app:`Pyramid` view - machinery expects them to be *returned* by a view callable rather than - *raised*. - - It is possible, however, in Python 2.5 and above, to configure an - *exception view* to catch these exceptions, and return an appropriate - :class:`~pyramid.response.Response`. The simplest such view could just - catch and return the original exception. See :ref:`exception_views` for - more details. - .. index:: single: view exceptions @@ -304,13 +264,21 @@ Using Special Exceptions In View Callables Usually when a Python exception is raised within a view callable, :app:`Pyramid` allows the exception to propagate all the way out to the -:term:`WSGI` server which invoked the application. +:term:`WSGI` server which invoked the application. It is usually caught and +logged there. -However, for convenience, two special exceptions exist which are always -handled by :app:`Pyramid` itself. These are -:exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden`. -Both are exception classes which accept a single positional constructor -argument: a ``message``. +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.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden` +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.exceptions.NotFound` is raised within view code, the result of the :term:`Not Found View` will be returned to the user agent which @@ -320,22 +288,100 @@ If :exc:`~pyramid.exceptions.Forbidden` is raised within view code, the result of the :term:`Forbidden View` will be returned to the user agent which performed the request. -In all cases, the message provided to the exception constructor is made -available to the view which :app:`Pyramid` invokes as +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.exceptions import NotFound + + def aview(request): + raise NotFound('not found!') + +Internal exceptions may not be *returned* in order to generate a response, +they must always be *raised*. + +.. index:: + single: HTTP exceptions + +.. _http_exceptions: + +HTTP Exceptions +~~~~~~~~~~~~~~~ + +All exception classes documented in the :mod:`pyramid.httpexceptions` module +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. + +For example, the :class:`pyramid.httpexceptions.HTTPUnauthorized` exception +can be raised. This will cause a response to be generated with a ``401 +Unauthorized`` status: + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPUnauthorized + + def aview(request): + raise HTTPUnauthorized() + +A shortcut for importing and raising an HTTP exception is the +:func:`pyramid.httpexceptions.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: + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import abort + + 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.httpexceptions` can be raised via +:func:`pyramid.httpexceptions.abort` as well, as long as the status code +associated with the exception is provided to the function. + +An HTTP exception, instead of being raised, can alternately be *returned* +(HTTP exceptions are also valid response objects): + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPUnauthorized + + def aview(request): + return HTTPUnauthorized() + +Note that :class:`pyramid.exceptions.NotFound` is *not* the same as +:class:`pyramid.httpexceptions.HTTPNotFound`. If the latter is raised, the +:term:`Not Found view` will *not* be called automatically. Likewise, +:class:`pyramid.exceptions.Foribdden` is not the same exception as +:class:`pyramid.httpexceptions.HTTPForbidden`. If the latter is raised, the +:term:`Forbidden view` will not be called automatically. + .. index:: single: exception views .. _exception_views: -Exception Views ---------------- +Custom Exception Views +---------------------- -The machinery which allows the special :exc:`~pyramid.exceptions.NotFound` and -:exc:`~pyramid.exceptions.Forbidden` exceptions to be caught by specialized -views as described in :ref:`special_exceptions_in_callables` can also be used -by application developers to convert arbitrary exceptions to responses. +The machinery which allows :exc:`~pyramid.exceptions.NotFound`, +:exc:`~pyramid.exceptions.Forbidden` and HTTP exceptions to be caught by +specialized views as described in :ref:`special_exceptions_in_callables` can +also be used by application developers to convert arbitrary exceptions to +responses. To register a view that should be called whenever a particular exception is raised from with :app:`Pyramid` view code, use the exception class or one of @@ -359,6 +405,7 @@ raises a ``helloworld.exceptions.ValidationFailure`` exception: .. code-block:: python :linenos: + from pyramid.view import view_config from helloworld.exceptions import ValidationFailure @view_config(context=ValidationFailure) @@ -380,12 +427,13 @@ exception view registration: :linenos: from pyramid.view import view_config - from pyramid.exceptions import NotFound - from pyramid.httpexceptions import HTTPNotFound + from helloworld.exceptions import ValidationFailure - @view_config(context=NotFound, route_name='home') - def notfound_view(request): - return HTTPNotFound() + @view_config(context=ValidationFailure, route_name='home') + def failed_validation(exc, request): + response = Response('Failed validation: %s' % exc.msg) + response.status_int = 500 + return response The above exception view names the ``route_name`` of ``home``, meaning that it will only be called when the route matched has a name of ``home``. You @@ -407,7 +455,68 @@ exception views which have a name will be ignored. can use an exception as ``context`` for a normal view. Exception views can be configured with any view registration mechanism: -``@view_config`` decorator, ZCML, or imperative ``add_view`` styles. +``@view_config`` decorator or imperative ``add_view`` styles. + +.. index:: + single: view http redirect + single: http redirect (from a view) + +.. _http_redirect: + +Using a View Callable to Do an HTTP Redirect +-------------------------------------------- + +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.httpexceptions.redirect` function. This function raises an +:class:`pyramid.httpexceptions.HTTPFound` exception (a "302"), which is +caught by an exception handler and turned into a response. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions 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.httpexceptions.redirect` function to do it for +you. + +To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` +instance. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + return HTTPFound(location='http://example.com') + +Or, alternately, you can *raise* an HTTPFound exception instead of returning +one. + +.. code-block:: python + :linenos: + + from pyramid.httpexceptions import HTTPFound + + def myview(request): + raise HTTPFound(location='http://example.com') + +The above form of generating a response by raising HTTPFound is completely +equivalent to ``redirect('http://example.com')``. .. index:: single: unicode, views, and forms diff --git a/docs/whatsnew-1.1.rst b/docs/whatsnew-1.1.rst index 992e0b637..488328519 100644 --- a/docs/whatsnew-1.1.rst +++ b/docs/whatsnew-1.1.rst @@ -18,6 +18,9 @@ The major feature additions in Pyramid 1.1 are: - Support for "static" routes. +- Default HTTP exception view and associated ``redirect`` and ``abort`` + convenience functions. + ``request.response`` ~~~~~~~~~~~~~~~~~~~~ @@ -50,6 +53,31 @@ Static Routes be useful for URL generation via ``route_url`` and ``route_path``. See the section entitled :ref:`static_route_narr` for more information. +Default HTTP Exception View +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- A default exception view for the context :exc:`webob.exc.HTTPException` + (aka :class:`pyramid.httpexceptions.HTTPException`) is now registered by + default. This means that an instance of any exception class imported from + :mod:`pyramid.httpexceptions` (such as ``HTTPFound``) can now be raised + from within view code; when raised, this exception view will render the + exception to a response. + + 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 + that will be used when an HTTP exception is raised. When ``None`` is + passed for this value, an exception view for HTTP exceptions will not be + registered. Passing ``None`` returns the behavior of raising an HTTP + exception to that of Pyramid 1.0 (the exception will propagate to + middleware and to the WSGI server). + Minor Feature Additions ----------------------- @@ -222,3 +250,10 @@ Documentation Enhancements - Added a section to the "URL Dispatch" narrative chapter regarding the new "static" route feature entitled :ref:`static_route_narr`. + +- Added API docs for :func:`pyramid.httpexceptions.abort` and + :func:`pyramid.httpexceptions.redirect`. + +- Added :ref:`http_exceptions` section to Views narrative chapter including a + description of :func:`pyramid.httpexceptions.abort`` and + :func:`pyramid.httpexceptions.redirect`. diff --git a/pyramid/config.py b/pyramid/config.py index 4e06a9b2e..85a66a837 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -59,6 +59,8 @@ from pyramid.exceptions import ConfigurationError from pyramid.exceptions import Forbidden from pyramid.exceptions import NotFound from pyramid.exceptions import PredicateMismatch +from pyramid.httpexceptions import HTTPException +from pyramid.httpexceptions import default_httpexception_view from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger from pyramid.mako_templating import renderer_factory as mako_renderer_factory @@ -139,7 +141,8 @@ class Configurator(object): ``package``, ``settings``, ``root_factory``, ``authentication_policy``, ``authorization_policy``, ``renderers`` ``debug_logger``, ``locale_negotiator``, ``request_factory``, ``renderer_globals_factory``, - ``default_permission``, ``session_factory``, and ``autocommit``. + ``default_permission``, ``session_factory``, ``default_view_mapper``, + ``autocommit``, and ``httpexception_view``. If the ``registry`` argument is passed as a non-``None`` value, it must be an instance of the :class:`pyramid.registry.Registry` @@ -254,7 +257,17 @@ class Configurator(object): :term:`view mapper` factory for view configurations that don't otherwise specify one (see :class:`pyramid.interfaces.IViewMapperFactory`). If a default_view_mapper is not passed, a superdefault view mapper will be - used. """ + used. + + If ``httpexception_view`` is passed, it must be a :term:`view callable` + or ``None``. If it is a view callable, it will be used as an exception + view callable when an :term:`HTTP exception` is raised (any named + exception from the ``pyramid.httpexceptions`` module) by + :func:`pyramid.httpexceptions.abort`, + :func:`pyramid.httpexceptions.redirect` or 'by hand'. If it is ``None``, + no httpexception view will be registered. By default, the + ``pyramid.httpexceptions.default_httpexception_view`` function is + used. This behavior is new in Pyramid 1.1. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -277,6 +290,7 @@ class Configurator(object): session_factory=None, default_view_mapper=None, autocommit=False, + httpexception_view=default_httpexception_view, ): if package is None: package = caller_package() @@ -302,6 +316,7 @@ class Configurator(object): default_permission=default_permission, session_factory=session_factory, default_view_mapper=default_view_mapper, + httpexception_view=httpexception_view, ) def _set_settings(self, mapping): @@ -658,7 +673,8 @@ class Configurator(object): renderers=DEFAULT_RENDERERS, debug_logger=None, locale_negotiator=None, request_factory=None, renderer_globals_factory=None, default_permission=None, - session_factory=None, default_view_mapper=None): + session_factory=None, default_view_mapper=None, + httpexception_view=default_httpexception_view): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial 'setup' is performed against the registry. This is because the registry you pass in may @@ -690,6 +706,9 @@ class Configurator(object): self.add_renderer(name, renderer) self.add_view(default_exceptionresponse_view, context=IExceptionResponse) + if httpexception_view is not None: + httpexception_view = self.maybe_dotted(httpexception_view) + self.add_view(httpexception_view, context=HTTPException) if locale_negotiator: locale_negotiator = self.maybe_dotted(locale_negotiator) registry.registerUtility(locale_negotiator, ILocaleNegotiator) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index f56910b53..cbd87520b 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -48,3 +48,36 @@ from webob.exc import HTTPBadGateway from webob.exc import HTTPServiceUnavailable from webob.exc import HTTPGatewayTimeout from webob.exc import HTTPVersionNotSupported + +from webob.response import Response + +def abort(status_code, **kw): + """Aborts the request immediately by raising an HTTP exception. The + values in ``*kw`` will be passed to the HTTP exception constructor. + Example:: + + abort(404) # raises an HTTPNotFound exception. + """ + exc = status_map[status_code](**kw) + raise exc.exception + + +def redirect(url, code=302, **kw): + """Raises a redirect exception to the specified URL. + + Optionally, a code variable may be passed with the status code of + the redirect, ie:: + + redirect(route_url('foo', request), code=303) + + """ + exc = status_map[code] + raise exc(location=url, **kw).exception + +def default_httpexception_view(context, request): + if isinstance(context, Response): + # WSGIHTTPException, a Response (2.5+) + return context + # HTTPException, a WSGI app (2.4) + return request.get_response(context) + diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 97a93616d..9c8f4875b 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -203,6 +203,38 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IViewMapperFactory), mapper) + def test_ctor_httpexception_view_default(self): + from zope.interface import implementedBy + from pyramid.httpexceptions import HTTPException + from pyramid.httpexceptions import default_httpexception_view + from pyramid.interfaces import IRequest + config = self._makeOne() + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPException), + request_iface=IRequest) + self.failUnless(view is default_httpexception_view) + + def test_ctor_httpexception_view_None(self): + from zope.interface import implementedBy + from pyramid.httpexceptions import HTTPException + from pyramid.interfaces import IRequest + config = self._makeOne(httpexception_view=None) + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPException), + request_iface=IRequest) + self.failUnless(view is None) + + def test_ctor_httpexception_view_custom(self): + from zope.interface import implementedBy + from pyramid.httpexceptions import HTTPException + from pyramid.interfaces import IRequest + def httpexception_view(context, request): pass + config = self._makeOne(httpexception_view=httpexception_view) + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPException), + request_iface=IRequest) + self.failUnless(view is httpexception_view) + def test_with_package_module(self): from pyramid.tests import test_configuration import pyramid.tests @@ -289,6 +321,20 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) + def test_setup_registry_registers_default_httpexception_view(self): + from pyramid.httpexceptions import HTTPException + from pyramid.httpexceptions import default_httpexception_view + class DummyRegistry(object): + def registerUtility(self, *arg, **kw): + pass + reg = DummyRegistry() + config = self._makeOne(reg) + views = [] + config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config.setup_registry() + self.assertEqual(views[1], ((default_httpexception_view,), + {'context':HTTPException})) + def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py new file mode 100644 index 000000000..843f9485a --- /dev/null +++ b/pyramid/tests/test_httpexceptions.py @@ -0,0 +1,79 @@ +import unittest + +class Test_abort(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.httpexceptions import abort + return abort(*arg, **kw) + + def test_status_404(self): + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound().exception.__class__, + self._callFUT, 404) + + def test_status_201(self): + from pyramid.httpexceptions import HTTPCreated + self.assertRaises(HTTPCreated().exception.__class__, + self._callFUT, 201) + + def test_extra_kw(self): + from pyramid.httpexceptions import HTTPNotFound + try: + self._callFUT(404, headers=[('abc', 'def')]) + except HTTPNotFound().exception.__class__, 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.httpexceptions import redirect + return redirect(*arg, **kw) + + def test_default(self): + from pyramid.httpexceptions import HTTPFound + try: + self._callFUT('http://example.com') + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + + def test_custom_code(self): + from pyramid.httpexceptions import HTTPMovedPermanently + try: + self._callFUT('http://example.com', 301) + except HTTPMovedPermanently().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '301 Moved Permanently') + + def test_extra_kw(self): + from pyramid.httpexceptions import HTTPFound + try: + self._callFUT('http://example.com', headers=[('abc', 'def')]) + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + self.assertEqual(exc.headers['abc'], 'def') + + +class Test_default_httpexception_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.httpexceptions import default_httpexception_view + return default_httpexception_view(context, request) + + def test_call_with_response(self): + from pyramid.response import Response + r = Response() + result = self._callFUT(r, None) + self.assertEqual(result, r) + + def test_call_with_nonresponse(self): + request = DummyRequest() + result = self._callFUT(None, request) + self.assertEqual(result, 'response') + +class DummyRequest(object): + def get_response(self, context): + return 'response' + + + -- cgit v1.2.3 From e1e0df9ff85d5d31b355527549e0b5654cce3af9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 16 May 2011 02:19:37 -0400 Subject: typo --- docs/narr/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 465cd3c0d..66e9919e2 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -365,7 +365,7 @@ An HTTP exception, instead of being raised, can alternately be *returned* Note that :class:`pyramid.exceptions.NotFound` is *not* the same as :class:`pyramid.httpexceptions.HTTPNotFound`. If the latter is raised, the :term:`Not Found view` will *not* be called automatically. Likewise, -:class:`pyramid.exceptions.Foribdden` is not the same exception as +:class:`pyramid.exceptions.Forbidden` is not the same exception as :class:`pyramid.httpexceptions.HTTPForbidden`. If the latter is raised, the :term:`Forbidden view` will not be called automatically. -- cgit v1.2.3 From 8c2a9e6d8d4f089222db7b30324774e94279b0e4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 26 May 2011 14:13:45 -0400 Subject: work towards unifying NotFound/HTTPNotFound and Forbidden/HTTPForbidden; 2 tests fail --- pyramid/config.py | 52 ++++---- pyramid/exceptions.py | 215 ++++++++++++++++++++++++--------- pyramid/httpexceptions.py | 83 +------------ pyramid/router.py | 2 +- pyramid/testing.py | 2 +- pyramid/tests/forbiddenapp/__init__.py | 4 +- pyramid/tests/test_config.py | 45 +++---- pyramid/tests/test_exceptions.py | 114 ++++++++++++----- pyramid/tests/test_httpexceptions.py | 80 +----------- pyramid/view.py | 31 +---- 10 files changed, 302 insertions(+), 326 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 85a66a837..a20f93a90 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -56,11 +56,10 @@ from pyramid.compat import md5 from pyramid.compat import any from pyramid.events import ApplicationCreated from pyramid.exceptions import ConfigurationError +from pyramid.exceptions import default_exceptionresponse_view from pyramid.exceptions import Forbidden from pyramid.exceptions import NotFound from pyramid.exceptions import PredicateMismatch -from pyramid.httpexceptions import HTTPException -from pyramid.httpexceptions import default_httpexception_view from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger from pyramid.mako_templating import renderer_factory as mako_renderer_factory @@ -82,7 +81,6 @@ from pyramid.traversal import find_interface from pyramid.traversal import traversal_path from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver -from pyramid.view import default_exceptionresponse_view from pyramid.view import render_view_to_response from pyramid.view import is_response @@ -142,7 +140,7 @@ class Configurator(object): ``authorization_policy``, ``renderers`` ``debug_logger``, ``locale_negotiator``, ``request_factory``, ``renderer_globals_factory``, ``default_permission``, ``session_factory``, ``default_view_mapper``, - ``autocommit``, and ``httpexception_view``. + ``autocommit``, and ``exceptionresponse_view``. If the ``registry`` argument is passed as a non-``None`` value, it must be an instance of the :class:`pyramid.registry.Registry` @@ -259,15 +257,18 @@ class Configurator(object): default_view_mapper is not passed, a superdefault view mapper will be used. - If ``httpexception_view`` is passed, it must be a :term:`view callable` - or ``None``. If it is a view callable, it will be used as an exception - view callable when an :term:`HTTP exception` is raised (any named - exception from the ``pyramid.httpexceptions`` module) by - :func:`pyramid.httpexceptions.abort`, - :func:`pyramid.httpexceptions.redirect` or 'by hand'. If it is ``None``, - no httpexception view will be registered. By default, the - ``pyramid.httpexceptions.default_httpexception_view`` function is - used. This behavior is new in Pyramid 1.1. """ + If ``exceptionresponse_view`` is passed, it must be a :term:`view + callable` or ``None``. If it is a view callable, it will be used as an + exception view callable when an :term:`exception response` is raised (any + named exception from the ``pyramid.exceptions`` module that begins with + ``HTTP`` as well as the ``NotFound`` and ``Forbidden`` exceptions) as + well as exceptions raised via :func:`pyramid.exceptions.abort`, + :func:`pyramid.exceptions.redirect`. If ``exceptionresponse_view`` is + ``None``, no exception response view will be registered, and all + raised exception responses will be bubbled up to Pyramid's caller. By + default, the ``pyramid.exceptions.default_exceptionresponse_view`` + function is used as the ``exceptionresponse_view``. This argument is new + in Pyramid 1.1. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -290,7 +291,7 @@ class Configurator(object): session_factory=None, default_view_mapper=None, autocommit=False, - httpexception_view=default_httpexception_view, + exceptionresponse_view=default_exceptionresponse_view, ): if package is None: package = caller_package() @@ -316,7 +317,7 @@ class Configurator(object): default_permission=default_permission, session_factory=session_factory, default_view_mapper=default_view_mapper, - httpexception_view=httpexception_view, + exceptionresponse_view=exceptionresponse_view, ) def _set_settings(self, mapping): @@ -674,7 +675,7 @@ class Configurator(object): locale_negotiator=None, request_factory=None, renderer_globals_factory=None, default_permission=None, session_factory=None, default_view_mapper=None, - httpexception_view=default_httpexception_view): + exceptionresponse_view=default_exceptionresponse_view): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial 'setup' is performed against the registry. This is because the registry you pass in may @@ -704,11 +705,9 @@ class Configurator(object): authorization_policy) for name, renderer in renderers: self.add_renderer(name, renderer) - self.add_view(default_exceptionresponse_view, - context=IExceptionResponse) - if httpexception_view is not None: - httpexception_view = self.maybe_dotted(httpexception_view) - self.add_view(httpexception_view, context=HTTPException) + if exceptionresponse_view is not None: + exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) + self.add_view(exceptionresponse_view, context=IExceptionResponse) if locale_negotiator: locale_negotiator = self.maybe_dotted(locale_negotiator) registry.registerUtility(locale_negotiator, ILocaleNegotiator) @@ -724,7 +723,7 @@ class Configurator(object): if session_factory is not None: self.set_session_factory(session_factory) # commit before adding default_view_mapper, as the - # default_exceptionresponse_view above requires the superdefault view + # exceptionresponse_view above requires the superdefault view # mapper self.commit() if default_view_mapper is not None: @@ -2703,7 +2702,7 @@ class MultiView(object): return view if view.__predicated__(context, request): return view - raise PredicateMismatch(self.name) + raise PredicateMismatch(self.name).exception def __permitted__(self, context, request): view = self.match(context, request) @@ -2722,7 +2721,7 @@ class MultiView(object): return view(context, request) except PredicateMismatch: continue - raise PredicateMismatch(self.name) + raise PredicateMismatch(self.name).exception def wraps_view(wrapped): def inner(self, view): @@ -2845,7 +2844,7 @@ class ViewDeriver(object): return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg, result) + raise Forbidden(msg, result=result).exception _secured_view.__call_permissive__ = view _secured_view.__permitted__ = _permitted _secured_view.__permission__ = permission @@ -2894,7 +2893,8 @@ class ViewDeriver(object): def predicate_wrapper(context, request): if all((predicate(context, request) for predicate in predicates)): return view(context, request) - raise PredicateMismatch('predicate mismatch for view %s' % view) + raise PredicateMismatch( + 'predicate mismatch for view %s' % view).exception def checker(context, request): return all((predicate(context, request) for predicate in predicates)) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 771d71b88..60e3c7b9b 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,42 +1,91 @@ from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import implements - -from pyramid.decorator import reify +from zope.interface import classImplements from pyramid.interfaces import IExceptionResponse -import cgi - -class ExceptionResponse(Exception): - """ Abstract class to support behaving as a WSGI response object """ - implements(IExceptionResponse) - status = None - - def __init__(self, message=''): - Exception.__init__(self, message) # B / C - self.message = message - - @reify # defer execution until asked explicitly - def app_iter(self): - return [ - """ - - %s - -

%s

- %s - - - """ % (self.status, self.status, cgi.escape(self.message)) - ] - - @reify # defer execution until asked explicitly - def headerlist(self): - return [ - ('Content-Length', str(len(self.app_iter[0]))), - ('Content-Type', 'text/html') - ] - - -class Forbidden(ExceptionResponse): +from webob.response import Response + +# Documentation proxy import +from webob.exc import __doc__ + +# API: status_map +from webob.exc import status_map +status_map = status_map.copy() # we mutate it + +# API: parent classes +from webob.exc import HTTPException +from webob.exc import WSGIHTTPException +from webob.exc import HTTPOk +from webob.exc import HTTPRedirection +from webob.exc import HTTPError +from webob.exc import HTTPClientError +from webob.exc import HTTPServerError + +# slightly nasty import-time side effect to provide WSGIHTTPException +# with IExceptionResponse interface (used during config.py exception view +# registration) +classImplements(WSGIHTTPException, IExceptionResponse) + +# API: Child classes +from webob.exc import HTTPCreated +from webob.exc import HTTPAccepted +from webob.exc import HTTPNonAuthoritativeInformation +from webob.exc import HTTPNoContent +from webob.exc import HTTPResetContent +from webob.exc import HTTPPartialContent +from webob.exc import HTTPMultipleChoices +from webob.exc import HTTPMovedPermanently +from webob.exc import HTTPFound +from webob.exc import HTTPSeeOther +from webob.exc import HTTPNotModified +from webob.exc import HTTPUseProxy +from webob.exc import HTTPTemporaryRedirect +from webob.exc import HTTPBadRequest +from webob.exc import HTTPUnauthorized +from webob.exc import HTTPPaymentRequired +from webob.exc import HTTPMethodNotAllowed +from webob.exc import HTTPNotAcceptable +from webob.exc import HTTPProxyAuthenticationRequired +from webob.exc import HTTPRequestTimeout +from webob.exc import HTTPConflict +from webob.exc import HTTPGone +from webob.exc import HTTPLengthRequired +from webob.exc import HTTPPreconditionFailed +from webob.exc import HTTPRequestEntityTooLarge +from webob.exc import HTTPRequestURITooLong +from webob.exc import HTTPUnsupportedMediaType +from webob.exc import HTTPRequestRangeNotSatisfiable +from webob.exc import HTTPExpectationFailed +from webob.exc import HTTPInternalServerError +from webob.exc import HTTPNotImplemented +from webob.exc import HTTPBadGateway +from webob.exc import HTTPServiceUnavailable +from webob.exc import HTTPGatewayTimeout +from webob.exc import HTTPVersionNotSupported + +# API: HTTPNotFound and HTTPForbidden (redefined for bw compat) + +from webob.exc import HTTPForbidden as _HTTPForbidden +from webob.exc import HTTPNotFound as _HTTPNotFound + +class HTTPNotFound(_HTTPNotFound): + """ + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found view` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found view can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a single positional argument, which + should be a string. The value of this string will be available as the + ``message`` attribute of this exception, for availability to the + :term:`Not Found View`. + """ + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, **kw): + self.message = detail # prevent 2.6.X whining + _HTTPNotFound.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + +class HTTPForbidden(_HTTPForbidden): """ Raise this exception within :term:`view` code to immediately return the :term:`forbidden view` to the invoking user. Usually this is a basic @@ -58,25 +107,20 @@ class Forbidden(ExceptionResponse): exception as necessary to provide extended information in an error report shown to a user. """ - status = '403 Forbidden' - def __init__(self, message='', result=None): - ExceptionResponse.__init__(self, message) - self.message = message - self.result = result + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, result=None, **kw): + self.message = detail # prevent 2.6.X whining + self.result = result # bw compat + _HTTPForbidden.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) -class NotFound(ExceptionResponse): - """ - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. +NotFound = HTTPNotFound # bw compat +Forbidden = HTTPForbidden # bw compat - This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be available as the - ``message`` attribute of this exception, for availability to the - :term:`Not Found View`. - """ - status = '404 Not Found' +# patch our status map with subclasses +status_map[403] = HTTPForbidden +status_map[404] = HTTPNotFound class PredicateMismatch(NotFound): """ @@ -102,3 +146,66 @@ 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. The + values in ``*kw`` will be passed to the HTTP exception constructor. + Example:: + + abort(404) # raises an HTTPNotFound exception. + """ + exc = status_map[status_code](**kw) + raise exc.exception + + +def redirect(url, code=302, **kw): + """Raises a redirect exception to the specified URL. + + Optionally, a code variable may be passed with the status code of + the redirect, ie:: + + redirect(route_url('foo', request), code=303) + + """ + exc = status_map[code] + raise exc(location=url, **kw).exception + +def is_response(ob): + """ Return ``True`` if ``ob`` implements the interface implied by + :ref:`the_response`. ``False`` if not. + + .. note:: This isn't a true interface or subclass check. Instead, it's a + duck-typing check, as response objects are not obligated to be of a + particular class or provide any particular Zope interface.""" + + # response objects aren't obligated to implement a Zope interface, + # so we do it the hard way + if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and + hasattr(ob, 'status') ): + return True + return False + +newstyle_exceptions = issubclass(Exception, object) + +if newstyle_exceptions: + # webob exceptions will be Response objects (Py 2.5+) + 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 + # WSGIHTTPException, a Response (2.5+) + return context + +else: + # webob exceptions will not be Response objects (Py 2.4) + 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 + # HTTPException, not a Response (2.4) + get_response = getattr(request, 'get_response', lambda c: c) # testing + return get_response(context) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index cbd87520b..8b2a012cc 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -1,83 +1,2 @@ -from webob.exc import __doc__ -from webob.exc import status_map - -# Parent classes -from webob.exc import HTTPException -from webob.exc import WSGIHTTPException -from webob.exc import HTTPOk -from webob.exc import HTTPRedirection -from webob.exc import HTTPError -from webob.exc import HTTPClientError -from webob.exc import HTTPServerError - -# Child classes -from webob.exc import HTTPCreated -from webob.exc import HTTPAccepted -from webob.exc import HTTPNonAuthoritativeInformation -from webob.exc import HTTPNoContent -from webob.exc import HTTPResetContent -from webob.exc import HTTPPartialContent -from webob.exc import HTTPMultipleChoices -from webob.exc import HTTPMovedPermanently -from webob.exc import HTTPFound -from webob.exc import HTTPSeeOther -from webob.exc import HTTPNotModified -from webob.exc import HTTPUseProxy -from webob.exc import HTTPTemporaryRedirect -from webob.exc import HTTPBadRequest -from webob.exc import HTTPUnauthorized -from webob.exc import HTTPPaymentRequired -from webob.exc import HTTPForbidden -from webob.exc import HTTPNotFound -from webob.exc import HTTPMethodNotAllowed -from webob.exc import HTTPNotAcceptable -from webob.exc import HTTPProxyAuthenticationRequired -from webob.exc import HTTPRequestTimeout -from webob.exc import HTTPConflict -from webob.exc import HTTPGone -from webob.exc import HTTPLengthRequired -from webob.exc import HTTPPreconditionFailed -from webob.exc import HTTPRequestEntityTooLarge -from webob.exc import HTTPRequestURITooLong -from webob.exc import HTTPUnsupportedMediaType -from webob.exc import HTTPRequestRangeNotSatisfiable -from webob.exc import HTTPExpectationFailed -from webob.exc import HTTPInternalServerError -from webob.exc import HTTPNotImplemented -from webob.exc import HTTPBadGateway -from webob.exc import HTTPServiceUnavailable -from webob.exc import HTTPGatewayTimeout -from webob.exc import HTTPVersionNotSupported - -from webob.response import Response - -def abort(status_code, **kw): - """Aborts the request immediately by raising an HTTP exception. The - values in ``*kw`` will be passed to the HTTP exception constructor. - Example:: - - abort(404) # raises an HTTPNotFound exception. - """ - exc = status_map[status_code](**kw) - raise exc.exception - - -def redirect(url, code=302, **kw): - """Raises a redirect exception to the specified URL. - - Optionally, a code variable may be passed with the status code of - the redirect, ie:: - - redirect(route_url('foo', request), code=303) - - """ - exc = status_map[code] - raise exc(location=url, **kw).exception - -def default_httpexception_view(context, request): - if isinstance(context, Response): - # WSGIHTTPException, a Response (2.5+) - return context - # HTTPException, a WSGI app (2.4) - return request.get_response(context) +from pyramid.exceptions import * # bw compat diff --git a/pyramid/router.py b/pyramid/router.py index b8a8639aa..069db52bc 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -153,7 +153,7 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg) + raise NotFound(msg).exception else: response = view_callable(context, request) diff --git a/pyramid/testing.py b/pyramid/testing.py index 36cc38830..a512ede4b 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -217,7 +217,7 @@ def registerView(name, result='', view=None, for_=(Interface, Interface), else: def _secure(context, request): if not has_permission(permission, context, request): - raise Forbidden('no permission') + raise Forbidden('no permission').exception else: return view(context, request) _secure.__call_permissive__ = view diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index ed9aa8357..614aff037 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,6 +1,4 @@ -from cgi import escape from webob import Response -from pyramid.httpexceptions import HTTPForbidden from pyramid.exceptions import Forbidden def x_view(request): # pragma: no cover @@ -10,7 +8,7 @@ def forbidden_view(context, request): msg = context.message result = context.result message = msg + '\n' + str(result) - resp = HTTPForbidden() + resp = Forbidden() resp.body = message return resp diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 9c8f4875b..7c6389253 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -204,36 +204,33 @@ class ConfiguratorTests(unittest.TestCase): mapper) def test_ctor_httpexception_view_default(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import default_httpexception_view + from pyramid.interfaces import IExceptionResponse + from pyramid.exceptions import default_exceptionresponse_view from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is default_httpexception_view) + self.failUnless(view is default_exceptionresponse_view) - def test_ctor_httpexception_view_None(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException + def test_ctor_exceptionresponse_view_None(self): + from pyramid.interfaces import IExceptionResponse from pyramid.interfaces import IRequest - config = self._makeOne(httpexception_view=None) + config = self._makeOne(exceptionresponse_view=None) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) self.failUnless(view is None) - def test_ctor_httpexception_view_custom(self): - from zope.interface import implementedBy - from pyramid.httpexceptions import HTTPException + def test_ctor_exceptionresponse_view_custom(self): + from pyramid.interfaces import IExceptionResponse from pyramid.interfaces import IRequest - def httpexception_view(context, request): pass - config = self._makeOne(httpexception_view=httpexception_view) + def exceptionresponse_view(context, request): pass + config = self._makeOne(exceptionresponse_view=exceptionresponse_view) view = self._getViewCallable(config, - ctx_iface=implementedBy(HTTPException), + ctx_iface=IExceptionResponse, request_iface=IRequest) - self.failUnless(view is httpexception_view) + self.failUnless(view is exceptionresponse_view) def test_with_package_module(self): from pyramid.tests import test_configuration @@ -321,20 +318,6 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) - def test_setup_registry_registers_default_httpexception_view(self): - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import default_httpexception_view - class DummyRegistry(object): - def registerUtility(self, *arg, **kw): - pass - reg = DummyRegistry() - config = self._makeOne(reg) - views = [] - config.add_view = lambda *arg, **kw: views.append((arg, kw)) - config.setup_registry() - self.assertEqual(views[1], ((default_httpexception_view,), - {'context':HTTPException})) - def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 5d0fa1e1a..2e9279f66 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -1,37 +1,16 @@ import unittest -class TestExceptionResponse(unittest.TestCase): - def _makeOne(self, message): - from pyramid.exceptions import ExceptionResponse - return ExceptionResponse(message) - - def test_app_iter(self): - exc = self._makeOne('') - self.assertTrue('' in exc.app_iter[0]) - - def test_headerlist(self): - exc = self._makeOne('') - headerlist = exc.headerlist - headerlist.sort() - app_iter = exc.app_iter - clen = str(len(app_iter[0])) - self.assertEqual(headerlist[0], ('Content-Length', clen)) - self.assertEqual(headerlist[1], ('Content-Type', 'text/html')) - - def test_withmessage(self): - exc = self._makeOne('abc&123') - self.assertTrue('abc&123' in exc.app_iter[0]) - class TestNotFound(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import NotFound return NotFound(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse + from pyramid.interfaces import IExceptionResponse e = self._makeOne('notfound') - self.assertTrue(isinstance(e, ExceptionResponse)) + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '404 Not Found') + self.assertEqual(e.message, 'notfound') class TestForbidden(unittest.TestCase): def _makeOne(self, message): @@ -39,7 +18,88 @@ class TestForbidden(unittest.TestCase): return Forbidden(message) def test_it(self): - from pyramid.exceptions import ExceptionResponse - e = self._makeOne('unauthorized') - self.assertTrue(isinstance(e, ExceptionResponse)) + from pyramid.interfaces import IExceptionResponse + e = self._makeOne('forbidden') + self.assertTrue(IExceptionResponse.providedBy(e)) self.assertEqual(e.status, '403 Forbidden') + self.assertEqual(e.message, 'forbidden') + +class Test_abort(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.exceptions import abort + return abort(*arg, **kw) + + def test_status_404(self): + from pyramid.exceptions import HTTPNotFound + self.assertRaises(HTTPNotFound().exception.__class__, + self._callFUT, 404) + + def test_status_201(self): + from pyramid.exceptions import HTTPCreated + self.assertRaises(HTTPCreated().exception.__class__, + self._callFUT, 201) + + def test_extra_kw(self): + from pyramid.exceptions import HTTPNotFound + try: + self._callFUT(404, headers=[('abc', 'def')]) + except HTTPNotFound().exception.__class__, 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.exceptions import redirect + return redirect(*arg, **kw) + + def test_default(self): + from pyramid.exceptions import HTTPFound + try: + self._callFUT('http://example.com') + except HTTPFound().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '302 Found') + + def test_custom_code(self): + from pyramid.exceptions import HTTPMovedPermanently + try: + self._callFUT('http://example.com', 301) + except HTTPMovedPermanently().exception.__class__, exc: + self.assertEqual(exc.location, 'http://example.com') + self.assertEqual(exc.status, '301 Moved Permanently') + + def test_extra_kw(self): + from pyramid.exceptions import HTTPFound + try: + self._callFUT('http://example.com', headers=[('abc', 'def')]) + except HTTPFound().exception.__class__, 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.exceptions 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 DummyRequest(object): + exception = None + def get_response(self, context): + return 'response' + + + diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 843f9485a..afa5a94de 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -1,79 +1,9 @@ import unittest -class Test_abort(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.httpexceptions import abort - return abort(*arg, **kw) - - def test_status_404(self): - from pyramid.httpexceptions import HTTPNotFound - self.assertRaises(HTTPNotFound().exception.__class__, - self._callFUT, 404) - - def test_status_201(self): - from pyramid.httpexceptions import HTTPCreated - self.assertRaises(HTTPCreated().exception.__class__, - self._callFUT, 201) - - def test_extra_kw(self): - from pyramid.httpexceptions import HTTPNotFound - try: - self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound().exception.__class__, 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.httpexceptions import redirect - return redirect(*arg, **kw) - - def test_default(self): - from pyramid.httpexceptions import HTTPFound - try: - self._callFUT('http://example.com') - except HTTPFound().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - - def test_custom_code(self): - from pyramid.httpexceptions import HTTPMovedPermanently - try: - self._callFUT('http://example.com', 301) - except HTTPMovedPermanently().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '301 Moved Permanently') +class TestIt(unittest.TestCase): + def test_bwcompat_imports(self): + from pyramid.httpexceptions import HTTPNotFound as one + from pyramid.exceptions import HTTPNotFound as two + self.assertTrue(one is two) - def test_extra_kw(self): - from pyramid.httpexceptions import HTTPFound - try: - self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound().exception.__class__, exc: - self.assertEqual(exc.location, 'http://example.com') - self.assertEqual(exc.status, '302 Found') - self.assertEqual(exc.headers['abc'], 'def') - - -class Test_default_httpexception_view(unittest.TestCase): - def _callFUT(self, context, request): - from pyramid.httpexceptions import default_httpexception_view - return default_httpexception_view(context, request) - - def test_call_with_response(self): - from pyramid.response import Response - r = Response() - result = self._callFUT(r, None) - self.assertEqual(result, r) - def test_call_with_nonresponse(self): - request = DummyRequest() - result = self._callFUT(None, request) - self.assertEqual(result, 'response') - -class DummyRequest(object): - def get_response(self, context): - return 'response' - - - diff --git a/pyramid/view.py b/pyramid/view.py index 2563f1e43..d6b666cf2 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -8,7 +8,9 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.httpexceptions import HTTPFound +from pyramid.exceptions import HTTPFound +from pyramid.exceptions import default_exceptionresponse_view +from pyramid.exceptions import is_response # API from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -130,21 +132,6 @@ def render_view(context, request, name='', secure=True): return None return ''.join(iterable) -def is_response(ob): - """ Return ``True`` if ``ob`` implements the interface implied by - :ref:`the_response`. ``False`` if not. - - .. note:: This isn't a true interface or subclass check. Instead, it's a - duck-typing check, as response objects are not obligated to be of a - particular class or provide any particular Zope interface.""" - - # response objects aren't obligated to implement a Zope interface, - # so we do it the hard way - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False - class view_config(object): """ A function, class or method :term:`decorator` which allows a developer to create view registrations nearer to a :term:`view @@ -243,14 +230,6 @@ deprecated( 'pyramid.view.view_config instead (API-compat, simple ' 'rename).') -def default_exceptionresponse_view(context, request): - if not isinstance(context, Exception): - # backwards compat for an exception response view registered via - # config.set_notfound_view or config.set_forbidden_view - # instead of as a proper exception view - context = request.exception or context - return context - class AppendSlashNotFoundViewFactory(object): """ There can only be one :term:`Not Found view` in any :app:`Pyramid` application. Even if you use @@ -273,7 +252,7 @@ class AppendSlashNotFoundViewFactory(object): from pyramid.exceptions import NotFound from pyramid.view import AppendSlashNotFoundViewFactory - from pyramid.httpexceptions import HTTPNotFound + from pyramid.exceptions import HTTPNotFound def notfound_view(context, request): return HTTPNotFound('nope') @@ -294,7 +273,7 @@ class AppendSlashNotFoundViewFactory(object): if not isinstance(context, Exception): # backwards compat for an append_notslash_view registered via # config.set_notfound_view instead of as a proper exception view - context = request.exception + context = getattr(request, 'exception', None) or context path = request.path registry = request.registry mapper = registry.queryUtility(IRoutesMapper) -- cgit v1.2.3 From f77703d25056236d027a1a61bf63fab3a7c1b2c2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 00:13:06 -0400 Subject: horrid workaround for no app_iter initialized --- pyramid/exceptions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 60e3c7b9b..e9718c6ab 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -84,6 +84,12 @@ class HTTPNotFound(_HTTPNotFound): _HTTPNotFound.__init__(self, detail=detail, headers=headers, comment=comment, body_template=body_template, **kw) + if not ('body' in kw or 'app_iter' in kw): + if not self.empty_body: + body = self.html_body(self.environ) + if isinstance(body, unicode): + body = body.encode(self.charset) + self.body = body class HTTPForbidden(_HTTPForbidden): """ @@ -114,6 +120,12 @@ class HTTPForbidden(_HTTPForbidden): _HTTPForbidden.__init__(self, detail=detail, headers=headers, comment=comment, body_template=body_template, **kw) + if not ('body' in kw or 'app_iter' in kw): + if not self.empty_body: + body = self.html_body(self.environ) + if isinstance(body, unicode): + body = body.encode(self.charset) + self.body = body NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat -- cgit v1.2.3 From 4184d956514ada7dccf2f99ced09cbf07a721cc3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:22:13 -0400 Subject: bite the bullet and replace all webob.exc classes with ones of our own --- pyramid/exceptions.py | 1144 +++++++++++++++++++++++++++++++++----- pyramid/tests/test_exceptions.py | 299 +++++++++- 2 files changed, 1308 insertions(+), 135 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index e9718c6ab..53cb0e5a8 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,139 +1,1033 @@ +""" +HTTP Exceptions +--------------- + +This module contains Python exceptions that relate to HTTP status codes by +defining a set of classes, all subclasses of HTTPException. Each +exception, in addition to being a Python exception that can be raised and +caught, is also a ``Response`` object. + +This module defines exceptions according to RFC 2068 [1]_ : codes with +100-300 are not really errors; 400's are client errors, and 500's are +server errors. + +Exception + HTTPException + HTTPOk + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 306 - Unused (not implemented, obviously) + * 307 - HTTPTemporaryRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + +Each HTTP exception has the following attributes: + + ``code`` + the HTTP status code for the exception + + ``title`` + remainder of the status line (stuff after the code) + + ``explanation`` + a plain-text explanation of the error message that is + not subject to environment or header substitutions; + it is accessible in the template via %(explanation)s + + ``detail`` + a plain-text message customization that is not subject + to environment or header substitutions; accessible in + the template via %(detail)s + + ``body_template`` + a content fragment (in HTML) used for environment and + header substitution; the default template includes both + the explanation and further detail provided in the + message + +Each HTTP exception accepts the following parameters: + + ``detail`` + a plain-text override of the default ``detail`` + + ``headers`` + a list of (k,v) header pairs + + ``comment`` + a plain-text additional information which is + usually stripped/hidden for end-users + + ``body_template`` + a string.Template object containing a content fragment in HTML + that frames the explanation and further detail + +Substitution of environment variables and headers into template values is +performed if a ``request`` is passed to the exception constructor. + +The subclasses of :class:`~_HTTPMove` +(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, +:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and +:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` +field. Reflecting this, these subclasses have one additional keyword argument: +``location``, which indicates the location to which to redirect. + +References: + +.. [1] http://www.python.org/peps/pep-0333.html#error-handling +""" + +import types +from string import Template +from webob import Response +from webob import html_escape + from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import classImplements +from zope.interface import implements from pyramid.interfaces import IExceptionResponse -from webob.response import Response - -# Documentation proxy import -from webob.exc import __doc__ - -# API: status_map -from webob.exc import status_map -status_map = status_map.copy() # we mutate it - -# API: parent classes -from webob.exc import HTTPException -from webob.exc import WSGIHTTPException -from webob.exc import HTTPOk -from webob.exc import HTTPRedirection -from webob.exc import HTTPError -from webob.exc import HTTPClientError -from webob.exc import HTTPServerError - -# slightly nasty import-time side effect to provide WSGIHTTPException -# with IExceptionResponse interface (used during config.py exception view -# registration) -classImplements(WSGIHTTPException, IExceptionResponse) - -# API: Child classes -from webob.exc import HTTPCreated -from webob.exc import HTTPAccepted -from webob.exc import HTTPNonAuthoritativeInformation -from webob.exc import HTTPNoContent -from webob.exc import HTTPResetContent -from webob.exc import HTTPPartialContent -from webob.exc import HTTPMultipleChoices -from webob.exc import HTTPMovedPermanently -from webob.exc import HTTPFound -from webob.exc import HTTPSeeOther -from webob.exc import HTTPNotModified -from webob.exc import HTTPUseProxy -from webob.exc import HTTPTemporaryRedirect -from webob.exc import HTTPBadRequest -from webob.exc import HTTPUnauthorized -from webob.exc import HTTPPaymentRequired -from webob.exc import HTTPMethodNotAllowed -from webob.exc import HTTPNotAcceptable -from webob.exc import HTTPProxyAuthenticationRequired -from webob.exc import HTTPRequestTimeout -from webob.exc import HTTPConflict -from webob.exc import HTTPGone -from webob.exc import HTTPLengthRequired -from webob.exc import HTTPPreconditionFailed -from webob.exc import HTTPRequestEntityTooLarge -from webob.exc import HTTPRequestURITooLong -from webob.exc import HTTPUnsupportedMediaType -from webob.exc import HTTPRequestRangeNotSatisfiable -from webob.exc import HTTPExpectationFailed -from webob.exc import HTTPInternalServerError -from webob.exc import HTTPNotImplemented -from webob.exc import HTTPBadGateway -from webob.exc import HTTPServiceUnavailable -from webob.exc import HTTPGatewayTimeout -from webob.exc import HTTPVersionNotSupported - -# API: HTTPNotFound and HTTPForbidden (redefined for bw compat) - -from webob.exc import HTTPForbidden as _HTTPForbidden -from webob.exc import HTTPNotFound as _HTTPNotFound - -class HTTPNotFound(_HTTPNotFound): - """ - Raise this exception within :term:`view` code to immediately - return the :term:`Not Found view` to the invoking user. Usually - this is a basic ``404`` page, but the Not Found view can be - customized as necessary. See :ref:`changing_the_notfound_view`. - This exception's constructor accepts a single positional argument, which - should be a string. The value of this string will be available as the - ``message`` attribute of this exception, for availability to the - :term:`Not Found View`. +newstyle_exceptions = issubclass(Exception, object) + +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): + implements(IExceptionResponse) """ + Exception used on pre-Python-2.5, where new-style classes cannot be used as + an exception. + """ + + def __init__(self, message, wsgi_response): + self.message = message + Exception.__init__(self, message) + self.__dict__['wsgi_response'] = wsgi_response + + def exception(self): + return self + + exception = property(exception) + + # for old style exceptions + if not newstyle_exceptions: #pragma NO COVERAGE + def __getattr__(self, attr): + if not attr.startswith('_'): + return getattr(self.wsgi_response, attr) + else: + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if attr.startswith('_') or attr in ('args',): + self.__dict__[attr] = value + else: + setattr(self.wsgi_response, attr, value) + +class WSGIHTTPException(Response, HTTPException): + + ## 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 environ + # + # - doesn't add request.environ keys to template substitutions unless + # 'request' is passed as a keyword argument. + # + # - doesn't use "strip_tags" (${br} placeholder for
, no other html + # in default body template) + # + # - sets a default app_iter if no body, app_iter, or unicode_body is + # passed + # + # - explicitly sets self.message = detail to prevent whining by Python + # 2.6.5+ Exception.message + # + 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('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## Set this to True for responses that should have no request body + empty_body = False + _default_called = False + def __init__(self, detail=None, headers=None, comment=None, body_template=None, **kw): - self.message = detail # prevent 2.6.X whining - _HTTPNotFound.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - if not ('body' in kw or 'app_iter' in kw): - if not self.empty_body: - body = self.html_body(self.environ) - if isinstance(body, unicode): - body = body.encode(self.charset) - self.body = body - -class HTTPForbidden(_HTTPForbidden): + 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 = iter(self._default_app_iter, None) + + def __str__(self): + return self.detail or self.explanation + + def _default_app_iter(self): + if self._default_called: + return None + html_comment = '' + comment = self.comment or '' + if 'html' in self.content_type or '': + escape = html_escape + page_template = self.html_template_obj + br = '
' + if comment: + html_comment = '' % 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) + self._default_called = True + return page + + def wsgi_response(self): + return self + + wsgi_response = property(wsgi_response) + + def exception(self): + if newstyle_exceptions: + return self + else: + return HTTPException(self.detail, self) + + exception = property(exception) + +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 tag in body + # + # - location keyword arg defaults to '' + # + # - ``add_slash`` argument is no longer accepted: code that passes + # add_slash argument 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\r\n' + 'it is either malformed or otherwise incorrect.\r\n') + +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\r\n' + 'access the document you requested. Either you supplied the\r\n' + 'wrong credentials (e.g., bad password), or your browser\r\n' + 'does not understand how to supply the credentials required.\r\n') + +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 accepts two arguments. The first argument, - ``message``, should be a string. The value of this string will be used - as the ``message`` attribute of the exception object. The second - argument, ``result`` is usually an instance of + 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. The ``result`` value will be used as the ``result`` attribute of the - exception object. + 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): - self.message = detail # prevent 2.6.X whining - self.result = result # bw compat - _HTTPForbidden.__init__(self, detail=detail, headers=headers, - comment=comment, body_template=body_template, - **kw) - if not ('body' in kw or 'app_iter' in kw): - if not self.empty_body: - body = self.html_body(self.environ) - if isinstance(body, unicode): - body = body.encode(self.charset) - self.body = body + 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\r\n' + 'the requested operation.\r\n') + +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') + +__all__ = ['status_map'] +status_map={} +for name, value in globals().items(): + if (isinstance(value, (type, types.ClassType)) and + issubclass(value, HTTPException) + and not name.startswith('_')): + __all__.append(name) + if getattr(value, 'code', None): + status_map[value.code]=value + if hasattr(value, 'explanation'): + value.explanation = ' '.join(value.explanation.strip().split()) +del name, value NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat -# patch our status map with subclasses -status_map[403] = HTTPForbidden -status_map[404] = HTTPNotFound - class PredicateMismatch(NotFound): """ Internal exception (not an API) raised by multiviews when no @@ -197,27 +1091,15 @@ def is_response(ob): return True return False -newstyle_exceptions = issubclass(Exception, object) +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 + # WSGIHTTPException, a Response (2.5+) + return context -if newstyle_exceptions: - # webob exceptions will be Response objects (Py 2.5+) - 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 - # WSGIHTTPException, a Response (2.5+) - return context - -else: - # webob exceptions will not be Response objects (Py 2.4) - 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 - # HTTPException, not a Response (2.4) - get_response = getattr(request, 'get_response', lambda c: c) # testing - return get_response(context) +__all__.extend(['NotFound', 'Forbidden', 'PredicateMismatch', 'URLDecodeError', + 'ConfigurationError', 'abort', 'redirect', 'is_response', + 'default_exceptionresponse_view']) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 2e9279f66..3f303e3df 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -96,10 +96,301 @@ class Test_default_exceptionresponse_view(unittest.TestCase): result = self._callFUT(None, request) self.assertEqual(result, context) +class Test_no_escape(unittest.TestCase): + def _callFUT(self, val): + from pyramid.exceptions 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 DummyRequest(object): exception = None - def get_response(self, context): - return 'response' + +# from webob.request import Request +# from webob.exc import HTTPException +# from webob.exc import WSGIHTTPException +# from webob.exc import HTTPMethodNotAllowed +# from webob.exc import _HTTPMove +# from webob.exc import HTTPExceptionMiddleware + +# from nose.tools import eq_, ok_, assert_equal, assert_raises + +# def test_HTTPException(self): +# _called = [] +# _result = object() +# def _response(environ, start_response): +# _called.append((environ, start_response)) +# return _result +# environ = {} +# start_response = object() +# exc = HTTPException('testing', _response) +# ok_(exc.wsgi_response is _response) +# ok_(exc.exception is exc) +# result = exc(environ, start_response) +# ok_(result is result) +# assert_equal(_called, [(environ, start_response)]) + +# from webob.dec import wsgify +# @wsgify +# def method_not_allowed_app(req): +# if req.method != 'GET': +# raise HTTPMethodNotAllowed().exception +# return 'hello!' + + +# def test_exception_with_unicode_data(): +# req = Request.blank('/', method=u'POST') +# res = req.get_response(method_not_allowed_app) +# assert res.status_int == 405 + +# def test_WSGIHTTPException_headers(): +# exc = WSGIHTTPException(headers=[('Set-Cookie', 'a=1'), +# ('Set-Cookie', 'a=2')]) +# mixed = exc.headers.mixed() +# assert mixed['set-cookie'] == ['a=1', 'a=2'] + +# def test_WSGIHTTPException_w_body_template(): +# from string import Template +# TEMPLATE = '$foo: $bar' +# exc = WSGIHTTPException(body_template = TEMPLATE) +# assert_equal(exc.body_template, TEMPLATE) +# ok_(isinstance(exc.body_template_obj, Template)) +# eq_(exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}), +# 'FOO: BAR') + +# def test_WSGIHTTPException_w_empty_body(): +# class EmptyOnly(WSGIHTTPException): +# empty_body = True +# exc = EmptyOnly(content_type='text/plain', content_length=234) +# ok_('content_type' not in exc.__dict__) +# ok_('content_length' not in exc.__dict__) + +# def test_WSGIHTTPException___str__(): +# exc1 = WSGIHTTPException(detail='Detail') +# eq_(str(exc1), 'Detail') +# class Explain(WSGIHTTPException): +# explanation = 'Explanation' +# eq_(str(Explain()), 'Explanation') + +# def test_WSGIHTTPException_plain_body_no_comment(): +# class Explain(WSGIHTTPException): +# code = '999' +# title = 'Testing' +# explanation = 'Explanation' +# exc = Explain(detail='Detail') +# eq_(exc.plain_body({}), +# '999 Testing\n\nExplanation\n\n Detail ') + +# def test_WSGIHTTPException_html_body_w_comment(): +# class Explain(WSGIHTTPException): +# code = '999' +# title = 'Testing' +# explanation = 'Explanation' +# exc = Explain(detail='Detail', comment='Comment') +# eq_(exc.html_body({}), +# '\n' +# ' \n' +# ' 999 Testing\n' +# ' \n' +# ' \n' +# '

999 Testing

\n' +# ' Explanation

\n' +# 'Detail\n' +# '\n\n' +# ' \n' +# '' +# ) + +# def test_WSGIHTTPException_generate_response(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'PUT', +# 'HTTP_ACCEPT': 'text/html' +# } +# excep = WSGIHTTPException() +# assert_equal( excep(environ,start_response), [ +# '\n' +# ' \n' +# ' None None\n' +# ' \n' +# ' \n' +# '

None None

\n' +# '

\n' +# '\n' +# '\n\n' +# ' \n' +# '' ] +# ) + +# def test_WSGIHTTPException_call_w_body(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'PUT' +# } +# excep = WSGIHTTPException() +# excep.body = 'test' +# assert_equal( excep(environ,start_response), ['test'] ) + + +# def test_WSGIHTTPException_wsgi_response(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# assert_equal( excep.wsgi_response(environ,start_response), [] ) + +# def test_WSGIHTTPException_exception_newstyle(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# exc.newstyle_exceptions = True +# assert_equal( excep.exception(environ,start_response), [] ) + +# def test_WSGIHTTPException_exception_no_newstyle(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# excep = WSGIHTTPException() +# exc.newstyle_exceptions = False +# assert_equal( excep.exception(environ,start_response), [] ) + +# def test_HTTPMove(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_location_not_none(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove(location='http://example.com') +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_add_slash_and_location(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# assert_raises( TypeError, _HTTPMove, location='http://example.com', add_slash=True ) + +# def test_HTTPMove_call_add_slash(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# m.add_slash = True +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPMove_call_query_string(): +# def start_response(status, headers, exc_info=None): +# pass +# environ = { +# 'wsgi.url_scheme': 'HTTP', +# 'SERVER_NAME': 'localhost', +# 'SERVER_PORT': '80', +# 'REQUEST_METHOD': 'HEAD' +# } +# m = _HTTPMove() +# m.add_slash = True +# environ[ 'QUERY_STRING' ] = 'querystring' +# assert_equal( m( environ, start_response ), [] ) + +# def test_HTTPExceptionMiddleware_ok(): +# def app( environ, start_response ): +# return '123' +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# start_response = None +# res = m( environ, start_response ) +# assert_equal( res, '123' ) - - +# def test_HTTPExceptionMiddleware_exception(): +# def wsgi_response( environ, start_response): +# return '123' +# def app( environ, start_response ): +# raise HTTPException( None, wsgi_response ) +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# start_response = None +# res = m( environ, start_response ) +# assert_equal( res, '123' ) + +# def test_HTTPExceptionMiddleware_exception_exc_info_none(): +# class DummySys: +# def exc_info(self): +# return None +# def wsgi_response( environ, start_response): +# return start_response('200 OK', [], exc_info=None) +# def app( environ, start_response ): +# raise HTTPException( None, wsgi_response ) +# application = app +# m = HTTPExceptionMiddleware(application) +# environ = {} +# def start_response(status, headers, exc_info): +# pass +# try: +# from webob import exc +# old_sys = exc.sys +# sys = DummySys() +# res = m( environ, start_response ) +# assert_equal( res, None ) +# finally: +# exc.sys = old_sys -- cgit v1.2.3 From 7cac620d6834f2997bedbb1e6bbb10637f97a9b7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:44:41 -0400 Subject: use a generator; explain --- pyramid/exceptions.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 53cb0e5a8..e626efd5c 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -220,7 +220,6 @@ ${body}''') ## Set this to True for responses that should have no request body empty_body = False - _default_called = False def __init__(self, detail=None, headers=None, comment=None, body_template=None, **kw): @@ -239,14 +238,17 @@ ${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 = iter(self._default_app_iter, None) + self.app_iter = self._default_app_iter() def __str__(self): return self.detail or self.explanation def _default_app_iter(self): - if self._default_called: - return None + # 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). html_comment = '' comment = self.comment or '' if 'html' in self.content_type or '': @@ -281,8 +283,8 @@ ${body}''') page = page_template.substitute(status=self.status, body=body) if isinstance(page, unicode): page = page.encode(self.charset) - self._default_called = True - return page + yield page + raise StopIteration def wsgi_response(self): return self -- cgit v1.2.3 From e25be5271cf54dd409cacf8089e055b0d13b59c7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:45:38 -0400 Subject: explain better --- pyramid/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index e626efd5c..393fb376f 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -246,9 +246,9 @@ ${body}''') 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 + # 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). + # (e.g. self.content_type or self.charset). html_comment = '' comment = self.comment or '' if 'html' in self.content_type or '': -- cgit v1.2.3 From a90d3231d0820dec17c26f31e0045b6edd6ebbc3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 19:46:11 -0400 Subject: explain better --- pyramid/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 393fb376f..354679a22 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -182,7 +182,7 @@ class WSGIHTTPException(Response, HTTPException): # on request environ # # - doesn't add request.environ keys to template substitutions unless - # 'request' is passed as a keyword argument. + # 'request' is passed as a constructor keyword argument. # # - doesn't use "strip_tags" (${br} placeholder for
, no other html # in default body template) -- cgit v1.2.3 From ce9b9baba16799041e6a4e819eef894ae9ff516d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 28 May 2011 20:08:25 -0400 Subject: move is_response back to pyramid.view --- pyramid/exceptions.py | 17 +---------------- pyramid/view.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 354679a22..8705ed1fb 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1078,21 +1078,6 @@ def redirect(url, code=302, **kw): exc = status_map[code] raise exc(location=url, **kw).exception -def is_response(ob): - """ Return ``True`` if ``ob`` implements the interface implied by - :ref:`the_response`. ``False`` if not. - - .. note:: This isn't a true interface or subclass check. Instead, it's a - duck-typing check, as response objects are not obligated to be of a - particular class or provide any particular Zope interface.""" - - # response objects aren't obligated to implement a Zope interface, - # so we do it the hard way - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False - def default_exceptionresponse_view(context, request): if not isinstance(context, Exception): # backwards compat for an exception response view registered via @@ -1103,5 +1088,5 @@ def default_exceptionresponse_view(context, request): return context __all__.extend(['NotFound', 'Forbidden', 'PredicateMismatch', 'URLDecodeError', - 'ConfigurationError', 'abort', 'redirect', 'is_response', + 'ConfigurationError', 'abort', 'redirect', 'default_exceptionresponse_view']) diff --git a/pyramid/view.py b/pyramid/view.py index d6b666cf2..975464124 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -10,7 +10,6 @@ from pyramid.interfaces import IViewClassifier from pyramid.exceptions import HTTPFound from pyramid.exceptions import default_exceptionresponse_view -from pyramid.exceptions import is_response # API from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -312,4 +311,18 @@ See also :ref:`changing_the_notfound_view`. """ +def is_response(ob): + """ Return ``True`` if ``ob`` implements the interface implied by + :ref:`the_response`. ``False`` if not. + + .. note:: This isn't a true interface or subclass check. Instead, it's a + duck-typing check, as response objects are not obligated to be of a + particular class or provide any particular Zope interface.""" + + # response objects aren't obligated to implement a Zope interface, + # so we do it the hard way + if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and + hasattr(ob, 'status') ): + return True + return False -- cgit v1.2.3 From 3488ebdb257f2b0784def1aa90e56c6a6581bb9d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 01:24:43 -0400 Subject: preemptively drop 2.4 support --- pyramid/config.py | 8 +++---- pyramid/exceptions.py | 47 +++++++++------------------------------- pyramid/router.py | 2 +- pyramid/tests/test_exceptions.py | 14 +++++------- 4 files changed, 21 insertions(+), 50 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index a20f93a90..1013456ec 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2702,7 +2702,7 @@ class MultiView(object): return view if view.__predicated__(context, request): return view - raise PredicateMismatch(self.name).exception + raise PredicateMismatch(self.name) def __permitted__(self, context, request): view = self.match(context, request) @@ -2721,7 +2721,7 @@ class MultiView(object): return view(context, request) except PredicateMismatch: continue - raise PredicateMismatch(self.name).exception + raise PredicateMismatch(self.name) def wraps_view(wrapped): def inner(self, view): @@ -2844,7 +2844,7 @@ class ViewDeriver(object): return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg, result=result).exception + raise Forbidden(msg, result=result) _secured_view.__call_permissive__ = view _secured_view.__permitted__ = _permitted _secured_view.__permission__ = permission @@ -2894,7 +2894,7 @@ class ViewDeriver(object): if all((predicate(context, request) for predicate in predicates)): return view(context, request) raise PredicateMismatch( - 'predicate mismatch for view %s' % view).exception + 'predicate mismatch for view %s' % view) def checker(context, request): return all((predicate(context, request) for predicate in predicates)) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 8705ed1fb..168426a9c 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -122,8 +122,6 @@ from zope.configuration.exceptions import ConfigurationError as ZCE from zope.interface import implements from pyramid.interfaces import IExceptionResponse -newstyle_exceptions = issubclass(Exception, object) - def no_escape(value): if value is None: return '' @@ -135,37 +133,10 @@ def no_escape(value): return value class HTTPException(Exception): - implements(IExceptionResponse) - """ - Exception used on pre-Python-2.5, where new-style classes cannot be used as - an exception. - """ - - def __init__(self, message, wsgi_response): - self.message = message - Exception.__init__(self, message) - self.__dict__['wsgi_response'] = wsgi_response - - def exception(self): - return self - - exception = property(exception) - - # for old style exceptions - if not newstyle_exceptions: #pragma NO COVERAGE - def __getattr__(self, attr): - if not attr.startswith('_'): - return getattr(self.wsgi_response, attr) - else: - raise AttributeError(attr) - - def __setattr__(self, attr, value): - if attr.startswith('_') or attr in ('args',): - self.__dict__[attr] = value - else: - setattr(self.wsgi_response, attr, value) + pass class WSGIHTTPException(Response, HTTPException): + implements(IExceptionResponse) ## You should set in subclasses: # code = 200 @@ -193,6 +164,10 @@ class WSGIHTTPException(Response, HTTPException): # - explicitly sets self.message = detail to prevent whining by Python # 2.6.5+ 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 = '' @@ -292,10 +267,8 @@ ${body}''') wsgi_response = property(wsgi_response) def exception(self): - if newstyle_exceptions: - return self - else: - return HTTPException(self.detail, self) + # bw compat + return self exception = property(exception) @@ -1063,7 +1036,7 @@ def abort(status_code, **kw): abort(404) # raises an HTTPNotFound exception. """ exc = status_map[status_code](**kw) - raise exc.exception + raise exc def redirect(url, code=302, **kw): @@ -1076,7 +1049,7 @@ def redirect(url, code=302, **kw): """ exc = status_map[code] - raise exc(location=url, **kw).exception + raise exc(location=url, **kw) def default_exceptionresponse_view(context, request): if not isinstance(context, Exception): diff --git a/pyramid/router.py b/pyramid/router.py index 069db52bc..b8a8639aa 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -153,7 +153,7 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg).exception + raise NotFound(msg) else: response = view_callable(context, request) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index 3f303e3df..e3e44c9f2 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -31,19 +31,17 @@ class Test_abort(unittest.TestCase): def test_status_404(self): from pyramid.exceptions import HTTPNotFound - self.assertRaises(HTTPNotFound().exception.__class__, - self._callFUT, 404) + self.assertRaises(HTTPNotFound, self._callFUT, 404) def test_status_201(self): from pyramid.exceptions import HTTPCreated - self.assertRaises(HTTPCreated().exception.__class__, - self._callFUT, 201) + self.assertRaises(HTTPCreated, self._callFUT, 201) def test_extra_kw(self): from pyramid.exceptions import HTTPNotFound try: self._callFUT(404, headers=[('abc', 'def')]) - except HTTPNotFound().exception.__class__, exc: + except HTTPNotFound, exc: self.assertEqual(exc.headers['abc'], 'def') else: # pragma: no cover raise AssertionError @@ -57,7 +55,7 @@ class Test_redirect(unittest.TestCase): from pyramid.exceptions import HTTPFound try: self._callFUT('http://example.com') - except HTTPFound().exception.__class__, exc: + except HTTPFound, exc: self.assertEqual(exc.location, 'http://example.com') self.assertEqual(exc.status, '302 Found') @@ -65,7 +63,7 @@ class Test_redirect(unittest.TestCase): from pyramid.exceptions import HTTPMovedPermanently try: self._callFUT('http://example.com', 301) - except HTTPMovedPermanently().exception.__class__, exc: + except HTTPMovedPermanently, exc: self.assertEqual(exc.location, 'http://example.com') self.assertEqual(exc.status, '301 Moved Permanently') @@ -73,7 +71,7 @@ class Test_redirect(unittest.TestCase): from pyramid.exceptions import HTTPFound try: self._callFUT('http://example.com', headers=[('abc', 'def')]) - except HTTPFound().exception.__class__, exc: + except HTTPFound, exc: self.assertEqual(exc.location, 'http://example.com') self.assertEqual(exc.status, '302 Found') self.assertEqual(exc.headers['abc'], 'def') -- cgit v1.2.3 From 9b496b8b248911ca8bd6c2bce08b73ee894809c4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 02:19:05 -0400 Subject: change docs; simplify --- pyramid/exceptions.py | 91 ++++++++++++++++++++++----------------------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 168426a9c..c2db99627 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -2,14 +2,13 @@ HTTP Exceptions --------------- -This module contains Python exceptions that relate to HTTP status codes by -defining a set of classes, all subclasses of HTTPException. Each -exception, in addition to being a Python exception that can be raised and -caught, is also a ``Response`` object. +This module contains Pyramid HTTP exception classes. Each class relates to a +single HTTP status code. Each class is a subclass of the `~HTTPException`. +Each exception class is also a :term:`response` object. -This module defines exceptions according to RFC 2068 [1]_ : codes with -100-300 are not really errors; 400's are client errors, and 500's are -server errors. +Each exception class has a status code according to `RFC 2068 +`: codes with 100-300 are not really +errors; 400's are client errors, and 500's are server errors. Exception HTTPException @@ -69,12 +68,12 @@ Each HTTP exception has the following attributes: ``explanation`` a plain-text explanation of the error message that is not subject to environment or header substitutions; - it is accessible in the template via %(explanation)s + it is accessible in the template via ${explanation} ``detail`` a plain-text message customization that is not subject to environment or header substitutions; accessible in - the template via %(detail)s + the template via ${detail} ``body_template`` a content fragment (in HTML) used for environment and @@ -98,8 +97,9 @@ Each HTTP exception accepts the following parameters: a string.Template object containing a content fragment in HTML that frames the explanation and further detail -Substitution of environment variables and headers into template values is -performed if a ``request`` is passed to the exception constructor. +Substitution of response headers into template values is always performed. +Substitution of WSGI environment values is performed if a ``request`` is +passed to the exception's constructor. The subclasses of :class:`~_HTTPMove` (:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, @@ -107,22 +107,18 @@ The subclasses of :class:`~_HTTPMove` :class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` field. Reflecting this, these subclasses have one additional keyword argument: ``location``, which indicates the location to which to redirect. - -References: - -.. [1] http://www.python.org/peps/pep-0333.html#error-handling """ import types from string import Template from webob import Response -from webob import html_escape +from webob import html_escape as _html_escape from zope.configuration.exceptions import ConfigurationError as ZCE from zope.interface import implements from pyramid.interfaces import IExceptionResponse -def no_escape(value): +def _no_escape(value): if value is None: return '' if not isinstance(value, basestring): @@ -227,15 +223,15 @@ ${body}''') html_comment = '' comment = self.comment or '' if 'html' in self.content_type or '': - escape = html_escape + escape = _html_escape page_template = self.html_template_obj br = '
' if comment: html_comment = '' % escape(comment) else: - escape = no_escape + escape = _no_escape page_template = self.plain_template_obj - br = '\n' + br = '\r\n' if comment: html_comment = escape(comment) args = { @@ -262,14 +258,13 @@ ${body}''') raise StopIteration def wsgi_response(self): + # bw compat only return self - wsgi_response = property(wsgi_response) def exception(self): - # bw compat + # bw compat only return self - exception = property(exception) class HTTPError(WSGIHTTPException): @@ -407,7 +402,7 @@ class _HTTPMove(HTTPRedirection): # - location keyword arg defaults to '' # # - ``add_slash`` argument is no longer accepted: code that passes - # add_slash argument will receive an exception. + # add_slash argument to the constructor will receive an exception. explanation = 'The resource has been moved to' body_template_obj = Template('''\ ${explanation} ${location}; @@ -534,8 +529,8 @@ class HTTPClientError(HTTPError): """ code = 400 title = 'Bad Request' - explanation = ('The server could not comply with the request since\r\n' - 'it is either malformed or otherwise incorrect.\r\n') + explanation = ('The server could not comply with the request since ' + 'it is either malformed or otherwise incorrect.') class HTTPBadRequest(HTTPClientError): pass @@ -551,10 +546,10 @@ class HTTPUnauthorized(HTTPClientError): code = 401 title = 'Unauthorized' explanation = ( - 'This server could not verify that you are authorized to\r\n' - 'access the document you requested. Either you supplied the\r\n' - 'wrong credentials (e.g., bad password), or your browser\r\n' - 'does not understand how to supply the credentials required.\r\n') + '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): """ @@ -587,9 +582,9 @@ class HTTPForbidden(HTTPClientError): 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. - The ``result`` value will be used as the ``result`` attribute of the - exception object. It defaults to ``None``. + ``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 @@ -895,8 +890,8 @@ class HTTPServerError(HTTPError): code = 500 title = 'Internal Server Error' explanation = ( - 'The server has either erred or is incapable of performing\r\n' - 'the requested operation.\r\n') + 'The server has either erred or is incapable of performing ' + 'the requested operation.') class HTTPInternalServerError(HTTPServerError): pass @@ -987,19 +982,6 @@ class HTTPInsufficientStorage(HTTPServerError): title = 'Insufficient Storage' explanation = ('There was not enough space to save the resource') -__all__ = ['status_map'] -status_map={} -for name, value in globals().items(): - if (isinstance(value, (type, types.ClassType)) and - issubclass(value, HTTPException) - and not name.startswith('_')): - __all__.append(name) - if getattr(value, 'code', None): - status_map[value.code]=value - if hasattr(value, 'explanation'): - value.explanation = ' '.join(value.explanation.strip().split()) -del name, value - NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat @@ -1060,6 +1042,13 @@ def default_exceptionresponse_view(context, request): # WSGIHTTPException, a Response (2.5+) return context -__all__.extend(['NotFound', 'Forbidden', 'PredicateMismatch', 'URLDecodeError', - 'ConfigurationError', 'abort', 'redirect', - 'default_exceptionresponse_view']) +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 + -- cgit v1.2.3 From c49e9f42e7932599a5a4864c9a17b494592c98c3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 03:33:16 -0400 Subject: add some tests for WSGIHTTPException --- pyramid/exceptions.py | 13 ++--- pyramid/tests/test_exceptions.py | 103 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index c2db99627..8b3c75549 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -222,7 +222,8 @@ ${body}''') # (e.g. self.content_type or self.charset). html_comment = '' comment = self.comment or '' - if 'html' in self.content_type or '': + content_type = self.content_type or '' + if 'html' in content_type: escape = _html_escape page_template = self.html_template_obj br = '
' @@ -231,7 +232,7 @@ ${body}''') else: escape = _no_escape page_template = self.plain_template_obj - br = '\r\n' + br = '\n' if comment: html_comment = escape(comment) args = { @@ -257,15 +258,11 @@ ${body}''') yield page raise StopIteration - def wsgi_response(self): - # bw compat only - return self - wsgi_response = property(wsgi_response) - + @property def exception(self): # bw compat only return self - exception = property(exception) + wsgi_response = exception # bw compat only class HTTPError(WSGIHTTPException): """ diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index e3e44c9f2..bbeb280f4 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -94,10 +94,10 @@ class Test_default_exceptionresponse_view(unittest.TestCase): result = self._callFUT(None, request) self.assertEqual(result, context) -class Test_no_escape(unittest.TestCase): +class Test__no_escape(unittest.TestCase): def _callFUT(self, val): - from pyramid.exceptions import no_escape - return no_escape(val) + from pyramid.exceptions import _no_escape + return _no_escape(val) def test_null(self): self.assertEqual(self._callFUT(None), '') @@ -112,6 +112,103 @@ class Test_no_escape(unittest.TestCase): duo = DummyUnicodeObject() self.assertEqual(self._callFUT(duo), u'42') +class TestWSGIHTTPException(unittest.TestCase): + def _getTargetClass(self): + from pyramid.exceptions import WSGIHTTPException + return WSGIHTTPException + + 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') + + 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._getTargetClass() + class Subclass(cls): + empty_body = True + exc = Subclass() + 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._getTargetClass() + class Subclass(cls): + code = '200' + title = 'OK' + explanation = 'explanation' + exc = Subclass('detail') + body = list(exc.app_iter)[0] + self.assertTrue(body.startswith(' Date: Sun, 29 May 2011 04:22:09 -0400 Subject: back up to complete coverage --- pyramid/tests/test_exceptions.py | 135 ++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 16 deletions(-) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index bbeb280f4..bf06f0b2e 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -117,6 +117,17 @@ class TestWSGIHTTPException(unittest.TestCase): from pyramid.exceptions 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) @@ -147,10 +158,8 @@ class TestWSGIHTTPException(unittest.TestCase): exc.body_template_obj.substitute({'foo':'foo'}), 'foo') def test_ctor_with_empty_body(self): - cls = self._getTargetClass() - class Subclass(cls): - empty_body = True - exc = Subclass() + cls = self._getTargetSubclass(empty_body=True) + exc = cls() self.assertEqual(exc.content_type, None) self.assertEqual(exc.content_length, None) @@ -167,12 +176,8 @@ class TestWSGIHTTPException(unittest.TestCase): self.assertEqual(exc.app_iter, ['123']) def test_ctor_with_body_sets_default_app_iter_html(self): - cls = self._getTargetClass() - class Subclass(cls): - code = '200' - title = 'OK' - explanation = 'explanation' - exc = Subclass('detail') + cls = self._getTargetSubclass() + exc = cls('detail') body = list(exc.app_iter)[0] self.assertTrue(body.startswith('' 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.exceptions 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.exceptions 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.exceptions 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 -- cgit v1.2.3 From 43378f78c0108831f3e07717c5213f1fd17a3d39 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 04:22:43 -0400 Subject: remove unused comments --- pyramid/tests/test_exceptions.py | 277 --------------------------------------- 1 file changed, 277 deletions(-) diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index bf06f0b2e..f2e577416 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -315,280 +315,3 @@ class TestHTTPForbidden(unittest.TestCase): class DummyRequest(object): exception = None -# from webob.request import Request -# from webob.exc import HTTPException -# from webob.exc import WSGIHTTPException -# from webob.exc import HTTPMethodNotAllowed -# from webob.exc import _HTTPMove -# from webob.exc import HTTPExceptionMiddleware - -# from nose.tools import eq_, ok_, assert_equal, assert_raises - -# def test_HTTPException(self): -# _called = [] -# _result = object() -# def _response(environ, start_response): -# _called.append((environ, start_response)) -# return _result -# environ = {} -# start_response = object() -# exc = HTTPException('testing', _response) -# ok_(exc.wsgi_response is _response) -# ok_(exc.exception is exc) -# result = exc(environ, start_response) -# ok_(result is result) -# assert_equal(_called, [(environ, start_response)]) - -# from webob.dec import wsgify -# @wsgify -# def method_not_allowed_app(req): -# if req.method != 'GET': -# raise HTTPMethodNotAllowed().exception -# return 'hello!' - - -# def test_exception_with_unicode_data(): -# req = Request.blank('/', method=u'POST') -# res = req.get_response(method_not_allowed_app) -# assert res.status_int == 405 - -# def test_WSGIHTTPException_headers(): -# exc = WSGIHTTPException(headers=[('Set-Cookie', 'a=1'), -# ('Set-Cookie', 'a=2')]) -# mixed = exc.headers.mixed() -# assert mixed['set-cookie'] == ['a=1', 'a=2'] - -# def test_WSGIHTTPException_w_body_template(): -# from string import Template -# TEMPLATE = '$foo: $bar' -# exc = WSGIHTTPException(body_template = TEMPLATE) -# assert_equal(exc.body_template, TEMPLATE) -# ok_(isinstance(exc.body_template_obj, Template)) -# eq_(exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}), -# 'FOO: BAR') - -# def test_WSGIHTTPException_w_empty_body(): -# class EmptyOnly(WSGIHTTPException): -# empty_body = True -# exc = EmptyOnly(content_type='text/plain', content_length=234) -# ok_('content_type' not in exc.__dict__) -# ok_('content_length' not in exc.__dict__) - -# def test_WSGIHTTPException___str__(): -# exc1 = WSGIHTTPException(detail='Detail') -# eq_(str(exc1), 'Detail') -# class Explain(WSGIHTTPException): -# explanation = 'Explanation' -# eq_(str(Explain()), 'Explanation') - -# def test_WSGIHTTPException_plain_body_no_comment(): -# class Explain(WSGIHTTPException): -# code = '999' -# title = 'Testing' -# explanation = 'Explanation' -# exc = Explain(detail='Detail') -# eq_(exc.plain_body({}), -# '999 Testing\n\nExplanation\n\n Detail ') - -# def test_WSGIHTTPException_html_body_w_comment(): -# class Explain(WSGIHTTPException): -# code = '999' -# title = 'Testing' -# explanation = 'Explanation' -# exc = Explain(detail='Detail', comment='Comment') -# eq_(exc.html_body({}), -# '\n' -# ' \n' -# ' 999 Testing\n' -# ' \n' -# ' \n' -# '

999 Testing

\n' -# ' Explanation

\n' -# 'Detail\n' -# '\n\n' -# ' \n' -# '' -# ) - -# def test_WSGIHTTPException_generate_response(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'PUT', -# 'HTTP_ACCEPT': 'text/html' -# } -# excep = WSGIHTTPException() -# assert_equal( excep(environ,start_response), [ -# '\n' -# ' \n' -# ' None None\n' -# ' \n' -# ' \n' -# '

None None

\n' -# '

\n' -# '\n' -# '\n\n' -# ' \n' -# '' ] -# ) - -# def test_WSGIHTTPException_call_w_body(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'PUT' -# } -# excep = WSGIHTTPException() -# excep.body = 'test' -# assert_equal( excep(environ,start_response), ['test'] ) - - -# def test_WSGIHTTPException_wsgi_response(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# excep = WSGIHTTPException() -# assert_equal( excep.wsgi_response(environ,start_response), [] ) - -# def test_WSGIHTTPException_exception_newstyle(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# excep = WSGIHTTPException() -# exc.newstyle_exceptions = True -# assert_equal( excep.exception(environ,start_response), [] ) - -# def test_WSGIHTTPException_exception_no_newstyle(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# excep = WSGIHTTPException() -# exc.newstyle_exceptions = False -# assert_equal( excep.exception(environ,start_response), [] ) - -# def test_HTTPMove(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove() -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPMove_location_not_none(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove(location='http://example.com') -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPMove_add_slash_and_location(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# assert_raises( TypeError, _HTTPMove, location='http://example.com', add_slash=True ) - -# def test_HTTPMove_call_add_slash(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove() -# m.add_slash = True -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPMove_call_query_string(): -# def start_response(status, headers, exc_info=None): -# pass -# environ = { -# 'wsgi.url_scheme': 'HTTP', -# 'SERVER_NAME': 'localhost', -# 'SERVER_PORT': '80', -# 'REQUEST_METHOD': 'HEAD' -# } -# m = _HTTPMove() -# m.add_slash = True -# environ[ 'QUERY_STRING' ] = 'querystring' -# assert_equal( m( environ, start_response ), [] ) - -# def test_HTTPExceptionMiddleware_ok(): -# def app( environ, start_response ): -# return '123' -# application = app -# m = HTTPExceptionMiddleware(application) -# environ = {} -# start_response = None -# res = m( environ, start_response ) -# assert_equal( res, '123' ) - -# def test_HTTPExceptionMiddleware_exception(): -# def wsgi_response( environ, start_response): -# return '123' -# def app( environ, start_response ): -# raise HTTPException( None, wsgi_response ) -# application = app -# m = HTTPExceptionMiddleware(application) -# environ = {} -# start_response = None -# res = m( environ, start_response ) -# assert_equal( res, '123' ) - -# def test_HTTPExceptionMiddleware_exception_exc_info_none(): -# class DummySys: -# def exc_info(self): -# return None -# def wsgi_response( environ, start_response): -# return start_response('200 OK', [], exc_info=None) -# def app( environ, start_response ): -# raise HTTPException( None, wsgi_response ) -# application = app -# m = HTTPExceptionMiddleware(application) -# environ = {} -# def start_response(status, headers, exc_info): -# pass -# try: -# from webob import exc -# old_sys = exc.sys -# sys = DummySys() -# res = m( environ, start_response ) -# assert_equal( res, None ) -# finally: -# exc.sys = old_sys -- cgit v1.2.3 From 8662ce04c7a2a2dd490761f330336e4c3e156c7d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 04:45:55 -0400 Subject: docs fixes --- pyramid/exceptions.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 8b3c75549..46367d4ef 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -3,8 +3,9 @@ HTTP Exceptions --------------- This module contains Pyramid HTTP exception classes. Each class relates to a -single HTTP status code. Each class is a subclass of the `~HTTPException`. -Each exception class is also a :term:`response` object. +single HTTP status code. Each class is a subclass of the +:class:`~HTTPException`. Each exception class is also a :term:`response` +object. Each exception class has a status code according to `RFC 2068 `: codes with 100-300 are not really @@ -76,10 +77,10 @@ Each HTTP exception has the following attributes: the template via ${detail} ``body_template`` - a content fragment (in HTML) used for environment and - header substitution; the default template includes both + a ``String.template``-format content fragment used for environment + and header substitution; the default template includes both the explanation and further detail provided in the - message + message. Each HTTP exception accepts the following parameters: @@ -94,7 +95,7 @@ Each HTTP exception accepts the following parameters: usually stripped/hidden for end-users ``body_template`` - a string.Template object containing a content fragment in HTML + a ``string.Template`` object containing a content fragment in HTML that frames the explanation and further detail Substitution of response headers into template values is always performed. @@ -108,15 +109,14 @@ The subclasses of :class:`~_HTTPMove` field. Reflecting this, these subclasses have one additional keyword argument: ``location``, which indicates the location to which to redirect. """ - import types from string import Template -from webob import Response from webob import html_escape as _html_escape - from zope.configuration.exceptions import ConfigurationError as ZCE from zope.interface import implements + from pyramid.interfaces import IExceptionResponse +from pyramid.response import Response def _no_escape(value): if value is None: @@ -1008,24 +1008,28 @@ class ConfigurationError(ZCE): def abort(status_code, **kw): - """Aborts the request immediately by raising an HTTP exception. The - values in ``*kw`` will be passed to the HTTP exception constructor. - Example:: + """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 a redirect exception to the specified URL. + """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) -- cgit v1.2.3 From 356d0327c22d7ced5fe28f9e5cb73671fe63a69b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 29 May 2011 04:46:42 -0400 Subject: docs fixes --- pyramid/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 46367d4ef..c1af43692 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -158,7 +158,7 @@ class WSGIHTTPException(Response, HTTPException): # passed # # - explicitly sets self.message = detail to prevent whining by Python - # 2.6.5+ Exception.message + # 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 -- cgit v1.2.3 From 966b5cfe03009069d7bbe92cc047b32a5e3cd4e6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 30 May 2011 03:23:31 -0400 Subject: - Fix older CHANGES entries. - The ``pyramid.request.Request`` class now has a ``ResponseClass`` interface which points at ``pyramid.response.Response``. - The ``pyramid.request.Response`` class now has a ``RequestClass`` interface which points at ``pyramid.response.Request``. - ``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. --- CHANGES.txt | 37 +++++++++++++++++++++++++------------ pyramid/__init__.py | 7 +++++-- pyramid/config.py | 13 +++++++------ pyramid/exceptions.py | 4 ++-- pyramid/response.py | 10 ++++++++-- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 756d1345c..15c86c13c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -104,12 +104,13 @@ Features section entitled "Static Routes" in the URL Dispatch narrative chapter for more information. -- A default exception view for the context ``webob.exc.HTTPException`` (aka - ``pyramid.httpexceptions.HTTPException``) is now registered by default. - This means that an instance of any exception 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 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 @@ -118,12 +119,18 @@ Features ``webob.exc.HTTPException``. - The Configurator now accepts an additional keyword argument named - ``httpexception_view``. By default, this argument is populated with a - default exception view function that will be used when an HTTP exception is - raised. When ``None`` is passed for this value, an exception view for HTTP - exceptions will not be registered. Passing ``None`` returns the behavior - of raising an HTTP exception to that of Pyramid 1.0 (the exception will - propagate to middleware and to the WSGI server). + ``exceptionresponse_view``. By default, this argument is populated with a + default exception view function that will be used when a response is raised + as an exception. When ``None`` is passed for this value, an exception view + for responses will not be registered. Passing ``None`` returns the + behavior of raising an HTTP exception to that of Pyramid 1.0 (the exception + will propagate to middleware and to the WSGI server). + +- The ``pyramid.request.Request`` class now has a ``ResponseClass`` interface + which points at ``pyramid.response.Response``. + +- The ``pyramid.request.Response`` class now has a ``RequestClass`` interface + which points at ``pyramid.response.Request``. Bug Fixes --------- @@ -289,6 +296,12 @@ Behavior Changes implements its own ``__getattr__``, ``__setattr__`` or ``__delattr__`` as a 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. + Dependencies ------------ diff --git a/pyramid/__init__.py b/pyramid/__init__.py index 5f6a326f8..473d5e1c6 100644 --- a/pyramid/__init__.py +++ b/pyramid/__init__.py @@ -1,2 +1,5 @@ -# pyramid package - +from pyramid.request import Request +from pyramid.response import Response +Response.RequestClass = Request +Request.ResponseClass = Response +del Request, Response diff --git a/pyramid/config.py b/pyramid/config.py index 1013456ec..ce5201ed3 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -260,12 +260,13 @@ class Configurator(object): If ``exceptionresponse_view`` is passed, it must be a :term:`view callable` or ``None``. If it is a view callable, it will be used as an exception view callable when an :term:`exception response` is raised (any - named exception from the ``pyramid.exceptions`` module that begins with - ``HTTP`` as well as the ``NotFound`` and ``Forbidden`` exceptions) as - well as exceptions raised via :func:`pyramid.exceptions.abort`, - :func:`pyramid.exceptions.redirect`. If ``exceptionresponse_view`` is - ``None``, no exception response view will be registered, and all - raised exception responses will be bubbled up to Pyramid's caller. By + object that implements the :class:`pyramid.interaces.IExceptionResponse` + interface, such as a :class:`pyramid.response.Response` object or any + ``HTTP`` exception documented in :mod:`pyramid.httpexceptions` as well as + exception responses raised via :func:`pyramid.exceptions.abort`, + :func:`pyramid.exceptions.redirect`). If ``exceptionresponse_view`` is + ``None``, no exception response view will be registered, and all raised + exception responses will be bubbled up to Pyramid's caller. By default, the ``pyramid.exceptions.default_exceptionresponse_view`` function is used as the ``exceptionresponse_view``. This argument is new in Pyramid 1.1. """ diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index c1af43692..678529c1e 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -128,7 +128,8 @@ def _no_escape(value): value = str(value) return value -class HTTPException(Exception): + +class HTTPException(Exception): # bw compat pass class WSGIHTTPException(Response, HTTPException): @@ -1040,7 +1041,6 @@ def default_exceptionresponse_view(context, request): # config.set_notfound_view or config.set_forbidden_view # instead of as a proper exception view context = request.exception or context - # WSGIHTTPException, a Response (2.5+) return context status_map={} diff --git a/pyramid/response.py b/pyramid/response.py index 26f27b142..e9f5528a5 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,2 +1,8 @@ -from webob import Response -Response = Response # pyflakes +from webob import Response as _Response +from zope.interface import implements + +from pyramid.interfaces import IExceptionResponse + +class Response(_Response, Exception): + implements(IExceptionResponse) + -- cgit v1.2.3 From a7e625785f65c41e5a6dc017b31bd0d74821474e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 31 May 2011 14:40:05 -0400 Subject: the canonical import location for HTTP exceptions/responses is now pyramid.response --- docs/glossary.rst | 4 +- docs/narr/hooks.rst | 48 +- docs/narr/renderers.rst | 31 +- docs/narr/router.rst | 53 +- docs/narr/testing.rst | 13 +- docs/narr/urldispatch.rst | 6 +- docs/narr/views.rst | 82 +- docs/narr/webob.rst | 36 +- docs/tutorials/bfg/index.rst | 2 +- docs/tutorials/wiki/authorization.rst | 2 +- docs/tutorials/wiki/definingviews.rst | 8 +- .../wiki/src/authorization/tutorial/login.py | 4 +- .../wiki/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki/src/views/tutorial/views.py | 2 +- docs/tutorials/wiki2/definingviews.rst | 4 +- .../wiki2/src/authorization/tutorial/__init__.py | 2 +- .../wiki2/src/authorization/tutorial/login.py | 2 +- .../wiki2/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki2/src/views/tutorial/views.py | 2 +- pyramid/config.py | 14 +- pyramid/exceptions.py | 1026 +------------------- pyramid/httpexceptions.py | 116 ++- pyramid/interfaces.py | 17 +- pyramid/response.py | 940 ++++++++++++++++++ pyramid/router.py | 4 +- pyramid/testing.py | 4 +- pyramid/tests/fixtureapp/views.py | 4 +- pyramid/tests/forbiddenapp/__init__.py | 6 +- pyramid/tests/test_config.py | 69 +- pyramid/tests/test_exceptions.py | 299 +----- pyramid/tests/test_httpexceptions.py | 2 +- pyramid/tests/test_response.py | 308 ++++++ pyramid/tests/test_router.py | 56 +- pyramid/tests/test_testing.py | 4 +- pyramid/view.py | 19 +- 35 files changed, 1618 insertions(+), 1575 deletions(-) create mode 100644 pyramid/tests/test_response.py diff --git a/docs/glossary.rst b/docs/glossary.rst index 797343e5e..20b9bfd64 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.exceptions.NotFound`` + developer explicitly raises a ``pyramid.response.HTTPNotFound`` exception from within :term:`view` code or :term:`root factory` code, or when the current request doesn't match any :term:`view configuration`. :app:`Pyramid` provides a default @@ -604,7 +604,7 @@ Glossary Forbidden view An :term:`exception view` invoked by :app:`Pyramid` when the developer explicitly raises a - ``pyramid.exceptions.Forbidden`` exception from within + ``pyramid.response.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 7e3fe0a5c..d620b5672 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -21,7 +21,7 @@ configuration. The :term:`not found view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "not found" view consists -only of naming the :exc:`pyramid.exceptions.NotFound` class as the +only of naming the :exc:`pyramid.response.HTTPNotFound` class as the ``context`` of the view configuration. If your application uses :term:`imperative configuration`, you can replace @@ -31,9 +31,9 @@ method to register an "exception view": .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from helloworld.views import notfound_view - config.add_view(notfound_view, context=NotFound) + config.add_view(notfound_view, context=HTTPNotFound) Replace ``helloworld.views.notfound_view`` with a reference to the :term:`view callable` you want to use to represent the Not Found view. @@ -42,7 +42,7 @@ Like any other view, the notfound view must accept at least a ``request`` parameter, or both ``context`` and ``request``. The ``request`` is the current :term:`request` representing the denied action. The ``context`` (if used in the call signature) will be the instance of the -:exc:`~pyramid.exceptions.NotFound` exception that caused the view to be +:exc:`~pyramid.response.HTTPNotFound` exception that caused the view to be called. Here's some sample code that implements a minimal NotFound view callable: @@ -50,25 +50,25 @@ Here's some sample code that implements a minimal NotFound view callable: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPNotFound + from pyramid.response 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.exceptions.NotFound` - exception that caused the not found view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - not found error was raised. This message will be different when - the ``debug_notfound`` environment setting is true than it is when - it is false. + :term:`request`. The ``exception`` attribute of the request will be an + instance of the :exc:`~pyramid.response.HTTPNotFound` exception that + caused the not found view to be called. The value of + ``request.exception.args[0]`` will be a value explaining why the not found + error was raised. This message will be different when the + ``debug_notfound`` environment setting is true than it is when it is + false. .. warning:: When a NotFound view callable accepts an argument list as described in :ref:`request_and_context_view_definitions`, the ``context`` passed as the first argument to the view callable will be the - :exc:`~pyramid.exceptions.NotFound` exception instance. If available, the - resource context will still be available as ``request.context``. + :exc:`~pyramid.response.HTTPNotFound` exception instance. If available, + the resource context will still be available as ``request.context``. .. index:: single: forbidden view @@ -85,7 +85,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.exceptions.Forbidden` class as the +only of naming the :exc:`pyramid.response.HTTPForbidden` class as the ``context`` of the view configuration. You can replace the forbidden view by using the @@ -96,8 +96,8 @@ view": :linenos: from helloworld.views import forbidden_view - from pyramid.exceptions import Forbidden - config.add_view(forbidden_view, context=Forbidden) + from pyramid.response import HTTPForbidden + config.add_view(forbidden_view, context=HTTPForbidden) Replace ``helloworld.views.forbidden_view`` with a reference to the Python :term:`view callable` you want to use to represent the Forbidden view. @@ -121,13 +121,13 @@ Here's some sample code that implements a minimal forbidden view: return Response('forbidden') .. note:: When a forbidden view callable is invoked, it is passed a - :term:`request`. The ``exception`` attribute of the request will - be an instance of the :exc:`~pyramid.exceptions.Forbidden` - exception that caused the forbidden view to be called. The value - of ``request.exception.args[0]`` will be a value explaining why the - forbidden was raised. This message will be different when the - ``debug_authorization`` environment setting is true than it is when - it is false. + :term:`request`. The ``exception`` attribute of the request will be an + instance of the :exc:`~pyramid.response.HTTPForbidden` exception that + caused the forbidden view to be called. The value of + ``request.exception.args[0]`` will be a value explaining why the forbidden + was raised. This message will be different when the + ``debug_authorization`` environment setting is true than it is when it is + false. .. index:: single: request factory diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index c3533648b..c7a3d7837 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -73,30 +73,43 @@ When this configuration is added to an application, the which renders view return values to a :term:`JSON` response serialization. Other built-in renderers include renderers which use the :term:`Chameleon` -templating language to render a dictionary to a response. +templating language to render a dictionary to a response. Additional +renderers can be added by developers to the system as necessary (see +:ref:`adding_and_overriding_renderers`). + +Views which use a renderer 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 configuration is ignored, and the response is passed back to :app:`Pyramid` unchanged. For example, if your view callable returns an instance of the -:class:`pyramid.httpexceptions.HTTPFound` class as a response, no renderer -will be employed. +:class:`pyramid.response.HTTPFound` class as a response, no renderer will be +employed. .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.response import HTTPFound def view(request): return HTTPFound(location='http://example.com') # any renderer avoided -Views which use a renderer can vary non-body response attributes (such as -headers and the HTTP status code) by attaching a property to the -``request.response`` attribute See :ref:`request_response_attr`. +Likewise for a "plain old response": + +.. code-block:: python + :linenos: + + from pyramid.response import Response + + def view(request): + return Response('OK') # any renderer avoided -Additional renderers can be added by developers to the system as necessary -(see :ref:`adding_and_overriding_renderers`). +Mutations to ``request.response`` in views which return a Response object +like this directly (unless that response *is* ``request.response``) will be +ignored. .. index:: single: renderers (built-in) diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 11f84d4ea..44fa9835b 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -77,40 +77,37 @@ processing? #. A :class:`~pyramid.events.ContextFound` :term:`event` is sent to any subscribers. -#. :app:`Pyramid` looks up a :term:`view` callable using the - context, the request, and the view name. If a view callable - doesn't exist for this combination of objects (based on the type of - the context, the type of the request, and the value of the view - name, and any :term:`predicate` attributes applied to the view - configuration), :app:`Pyramid` raises a - :class:`~pyramid.exceptions.NotFound` exception, which is meant - to be caught by a surrounding exception handler. +#. :app:`Pyramid` looks up a :term:`view` callable using the context, the + request, and the view name. If a view callable doesn't exist for this + combination of objects (based on the type of the context, the type of the + request, and the value of the view name, and any :term:`predicate` + attributes applied to the view configuration), :app:`Pyramid` raises a + :class:`~pyramid.response.HTTPNotFound` exception, which is meant to be + caught by a surrounding exception handler. #. If a view callable was found, :app:`Pyramid` attempts to call the view function. -#. If an :term:`authorization policy` is in use, and the view was - protected by a :term:`permission`, :app:`Pyramid` passes the - context, the request, and the view_name to a function which - determines whether the view being asked for can be executed by the - requesting user, based on credential information in the request and - security information attached to the context. If it returns - ``True``, :app:`Pyramid` calls the view callable to obtain a - response. If it returns ``False``, it raises a - :class:`~pyramid.exceptions.Forbidden` exception, which is meant - to be called by a surrounding exception handler. +#. If an :term:`authorization policy` is in use, and the view was protected + by a :term:`permission`, :app:`Pyramid` passes the context, the request, + and the view_name to a function which determines whether the view being + asked for can be executed by the requesting user, based on credential + information in the request and security information attached to the + context. If it returns ``True``, :app:`Pyramid` calls the view callable + to obtain a response. If it returns ``False``, it raises a + :class:`~pyramid.response.HTTPForbidden` exception, which is meant to be + called by a surrounding exception handler. #. If any exception was raised within a :term:`root factory`, by - :term:`traversal`, by a :term:`view callable` or by - :app:`Pyramid` itself (such as when it raises - :class:`~pyramid.exceptions.NotFound` or - :class:`~pyramid.exceptions.Forbidden`), the router catches the - exception, and attaches it to the request as the ``exception`` - attribute. It then attempts to find a :term:`exception view` for - the exception that was caught. If it finds an exception view - callable, that callable is called, and is presumed to generate a - response. If an :term:`exception view` that matches the exception - cannot be found, the exception is reraised. + :term:`traversal`, by a :term:`view callable` or by :app:`Pyramid` itself + (such as when it raises :class:`~pyramid.response.HTTPNotFound` or + :class:`~pyramid.response.HTTPForbidden`), the router catches the + exception, and attaches it to the request as the ``exception`` attribute. + It then attempts to find a :term:`exception view` for the exception that + was caught. If it finds an exception view callable, that callable is + called, and is presumed to generate a response. If an :term:`exception + view` that matches the exception cannot be found, the exception is + reraised. #. The following steps occur only when a :term:`response` could be successfully generated by a normal :term:`view callable` or an diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index bd45388c2..862eda9f0 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -191,11 +191,11 @@ function. :linenos: from pyramid.security import has_permission - from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden def view_fn(request): if not has_permission('edit', request.context, request): - raise Forbidden + raise HTTPForbidden return {'greeting':'hello'} Without doing anything special during a unit test, the call to @@ -207,7 +207,7 @@ application registry is not created and populated (e.g. by initializing the configurator with an authorization policy), like when you invoke application code via a unit test, :app:`Pyramid` API functions will tend to either fail or return default results. So how do you test the branch of the code in this -view function that raises :exc:`Forbidden`? +view function that raises :exc:`HTTPForbidden`? The testing API provided by :app:`Pyramid` allows you to simulate various application registry registrations for use under a unit testing framework @@ -230,16 +230,15 @@ without needing to invoke the actual application configuration implied by its testing.tearDown() def test_view_fn_forbidden(self): - from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=False) request = testing.DummyRequest() request.context = testing.DummyResource() - self.assertRaises(Forbidden, view_fn, request) + self.assertRaises(HTTPForbidden, view_fn, request) def test_view_fn_allowed(self): - from pyramid.exceptions import Forbidden from my.package import view_fn self.config.testing_securitypolicy(userid='hank', permissive=True) @@ -265,7 +264,7 @@ We call the function being tested with the manufactured request. When the function is called, :func:`pyramid.security.has_permission` will call the "dummy" authentication policy we've registered through :meth:`~pyramid.config.Configuration.testing_securitypolicy`, which denies -access. We check that the view function raises a :exc:`Forbidden` error. +access. We check that the view function raises a :exc:`HTTPForbidden` error. The second test method, named ``test_view_fn_allowed`` tests the alternate case, where the authentication policy allows access. Notice that we pass diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 5df1eb3af..e5228b81e 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -917,7 +917,7 @@ the application's startup configuration, adding the following stanza: :linenos: config.add_view('pyramid.view.append_slash_notfound_view', - context='pyramid.exceptions.NotFound') + context='pyramid.response.HTTPNotFound') See :ref:`view_module` and :ref:`changing_the_notfound_view` for more information about the slash-appending not found view and for a more general @@ -945,14 +945,14 @@ view as the first argument to its constructor. For instance: .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory def notfound_view(context, request): return HTTPNotFound('It aint there, stop trying!') custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=NotFound) + config.add_view(custom_append_slash, context=HTTPNotFound) The ``notfound_view`` supplied must adhere to the two-argument view callable calling convention of ``(context, request)`` (``context`` will be the diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 66e9919e2..73a7c2e2a 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -233,7 +233,7 @@ implements the :term:`Response` interface is to return a You don't need to always use :class:`~pyramid.response.Response` to represent a response. :app:`Pyramid` provides a range of different "exception" classes which can act as response objects too. For example, an instance of the class -:class:`pyramid.httpexceptions.HTTPFound` is also a valid response object +: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. @@ -275,17 +275,18 @@ exist: internal exceptions and HTTP exceptions. Internal Exceptions ~~~~~~~~~~~~~~~~~~~ -:exc:`pyramid.exceptions.NotFound` and :exc:`pyramid.exceptions.Forbidden` -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. +: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.exceptions.NotFound` is raised within view code, the result -of the :term:`Not Found View` will be returned to the user agent which +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.exceptions.Forbidden` is raised within view code, the result -of the :term:`Forbidden View` will be returned to the user agent which +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 @@ -298,13 +299,10 @@ An example: .. code-block:: python :linenos: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound def aview(request): - raise NotFound('not found!') - -Internal exceptions may not be *returned* in order to generate a response, -they must always be *raised*. + raise HTTPNotFound('not found!') .. index:: single: HTTP exceptions @@ -314,32 +312,33 @@ they must always be *raised*. HTTP Exceptions ~~~~~~~~~~~~~~~ -All exception classes documented in the :mod:`pyramid.httpexceptions` module -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.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. -For example, the :class:`pyramid.httpexceptions.HTTPUnauthorized` exception +For example, the :class:`pyramid.response.HTTPUnauthorized` exception can be raised. This will cause a response to be generated with a ``401 Unauthorized`` status: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPUnauthorized + from pyramid.response import HTTPUnauthorized def aview(request): raise HTTPUnauthorized() A shortcut for importing and raising an HTTP exception is the -:func:`pyramid.httpexceptions.abort` function. This function accepts an HTTP +: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: .. code-block:: python :linenos: - from pyramid.httpexceptions import abort + from pyramid.response import abort def aview(request): abort(401) @@ -347,8 +346,8 @@ raise HTTPUnauthorized, instead of the above, you could do: 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.httpexceptions` can be raised via -:func:`pyramid.httpexceptions.abort` as well, as long as the status code +: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. An HTTP exception, instead of being raised, can alternately be *returned* @@ -357,18 +356,11 @@ An HTTP exception, instead of being raised, can alternately be *returned* .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPUnauthorized + from pyramid.response import HTTPUnauthorized def aview(request): return HTTPUnauthorized() -Note that :class:`pyramid.exceptions.NotFound` is *not* the same as -:class:`pyramid.httpexceptions.HTTPNotFound`. If the latter is raised, the -:term:`Not Found view` will *not* be called automatically. Likewise, -:class:`pyramid.exceptions.Forbidden` is not the same exception as -:class:`pyramid.httpexceptions.HTTPForbidden`. If the latter is raised, the -:term:`Forbidden view` will not be called automatically. - .. index:: single: exception views @@ -377,11 +369,11 @@ Note that :class:`pyramid.exceptions.NotFound` is *not* the same as Custom Exception Views ---------------------- -The machinery which allows :exc:`~pyramid.exceptions.NotFound`, -:exc:`~pyramid.exceptions.Forbidden` and HTTP exceptions to be caught by -specialized views as described in :ref:`special_exceptions_in_callables` can -also be used by application developers to convert arbitrary exceptions to -responses. +The machinery which allows :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. 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 @@ -474,14 +466,14 @@ Short Form ~~~~~~~~~~ You can issue an HTTP redirect from within a view callable by using the -:func:`pyramid.httpexceptions.redirect` function. This function raises an -:class:`pyramid.httpexceptions.HTTPFound` exception (a "302"), which is -caught by an exception handler and turned into a response. +: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.httpexceptions import redirect + from pyramid.response import redirect def myview(request): redirect('http://example.com') @@ -490,16 +482,16 @@ Long Form ~~~~~~~~~ You can issue an HTTP redirect from within a view "by hand" instead of -relying on the :func:`pyramid.httpexceptions.redirect` function to do it for +relying on the :func:`pyramid.response.redirect` function to do it for you. -To do so, you can *return* a :class:`pyramid.httpexceptions.HTTPFound` +To do so, you can *return* a :class:`pyramid.response.HTTPFound` instance. .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.response import HTTPFound def myview(request): return HTTPFound(location='http://example.com') @@ -510,7 +502,7 @@ one. .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPFound + from pyramid.response import HTTPFound def myview(request): raise HTTPFound(location='http://example.com') diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 072ca1c74..6cd9418ce 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -362,11 +362,11 @@ To facilitate error responses like ``404 Not Found``, the module :mod:`webob.exc` contains classes for each kind of error response. These include boring, but appropriate error bodies. The exceptions exposed by this module, when used under :app:`Pyramid`, should be imported from the -:mod:`pyramid.httpexceptions` "facade" module. This import location is merely -a facade for the original location of these exceptions: ``webob.exc``. +:mod:`pyramid.response` module. This import location contains subclasses and +replacements that mirror those in the original ``webob.exc``. -Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the reason -for the error. For instance, :class:`pyramid.httpexceptions.HTTPNotFound`. It +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: @@ -374,40 +374,18 @@ the same way. A typical example is: .. code-block:: python :linenos: - from pyramid.httpexceptions import HTTPNotFound - from pyramid.httpexceptions import HTTPMovedPermanently + from pyramid.response import HTTPNotFound + from pyramid.response import HTTPMovedPermanently response = HTTPNotFound('There is no such resource') # or: response = HTTPMovedPermanently(location=new_url) -These are not exceptions unless you are using Python 2.5+, because -they are new-style classes which are not allowed as exceptions until -Python 2.5. To get an exception object use ``response.exception``. -You can use this like: - -.. code-block:: python - :linenos: - - from pyramid.httpexceptions import HTTPException - from pyramid.httpexceptions import HTTPNotFound - - def aview(request): - try: - # ... stuff ... - raise HTTPNotFound('No such resource').exception - except HTTPException, e: - return request.get_response(e) - -The exceptions are still WSGI applications, but you cannot set -attributes like ``content_type``, ``charset``, etc. on these exception -objects. - More Details ++++++++++++ More details about the response object API are available in the :mod:`pyramid.response` documentation. More details about exception responses -are in the :mod:`pyramid.httpexceptions` API documentation. The `WebOb +are in the :mod:`pyramid.response` API documentation. The `WebOb documentation `_ is also useful. diff --git a/docs/tutorials/bfg/index.rst b/docs/tutorials/bfg/index.rst index e68e63b0b..e01345158 100644 --- a/docs/tutorials/bfg/index.rst +++ b/docs/tutorials/bfg/index.rst @@ -106,7 +106,7 @@ Here's how to convert a :mod:`repoze.bfg` application to a - ZCML files which contain directives that have attributes which name a ``repoze.bfg`` API module or attribute of an API module - (e.g. ``context="repoze.bfg.exceptions.NotFound"``) will be + (e.g. ``context="repoze.bfg.exeptions.NotFound"``) will be converted to :app:`Pyramid` compatible ZCML attributes (e.g. ``context="pyramid.exceptions.NotFound``). Every ZCML file beneath the top-level path (files ending with ``.zcml``) will be diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index e4480d6d9..3b102958e 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -131,7 +131,7 @@ callable. The first view configuration decorator configures the ``login`` view callable so it will be invoked when someone visits ``/login`` (when the context is a Wiki and the view name is ``login``). The second decorator (with context of -``pyramid.exceptions.Forbidden``) specifies a :term:`forbidden view`. This +``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 diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index b6c083bbf..ea8842294 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.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 +: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 "location" of the HTTPFound response, forming an HTTP redirect. The ``view_page`` view function diff --git a/docs/tutorials/wiki/src/authorization/tutorial/login.py b/docs/tutorials/wiki/src/authorization/tutorial/login.py index 463db71a6..822b19b9e 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.httpexceptions import HTTPFound +from pyramid.response 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.exceptions.Forbidden', +@view_config(context='pyramid.response.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 a83e17de4..67550d58e 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.httpexceptions import HTTPFound +from pyramid.response 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 42420f2fe..d72cbd3fd 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.httpexceptions import HTTPFound +from pyramid.response 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 832f90b92..32e3c0b24 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.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface), It will use the +:class:`pyramid.response.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 05183d3d4..42013622c 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/__init__.py @@ -39,7 +39,7 @@ def main(global_config, **settings): config.add_view('tutorial.views.edit_page', route_name='edit_page', renderer='tutorial:templates/edit.pt', permission='edit') config.add_view('tutorial.login.login', - context='pyramid.exceptions.Forbidden', + context='pyramid.response.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 7a1d1f663..2bc8a7201 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.httpexceptions import HTTPFound +from pyramid.response 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 5abd8391e..ed441295c 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.httpexceptions import HTTPFound +from pyramid.response 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 b8896abe7..80d817d99 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.httpexceptions import HTTPFound +from pyramid.response import HTTPFound from pyramid.url import route_url from tutorial.models import DBSession diff --git a/pyramid/config.py b/pyramid/config.py index ce5201ed3..ab1729c06 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -56,9 +56,9 @@ from pyramid.compat import md5 from pyramid.compat import any from pyramid.events import ApplicationCreated from pyramid.exceptions import ConfigurationError -from pyramid.exceptions import default_exceptionresponse_view -from pyramid.exceptions import Forbidden -from pyramid.exceptions import NotFound +from pyramid.response import default_exceptionresponse_view +from pyramid.response import HTTPForbidden +from pyramid.response import HTTPNotFound from pyramid.exceptions import PredicateMismatch from pyramid.i18n import get_localizer from pyramid.log import make_stream_logger @@ -1997,7 +1997,8 @@ class Configurator(object): def bwcompat_view(context, request): context = getattr(request, 'context', None) return view(context, request) - return self.add_view(bwcompat_view, context=Forbidden, wrapper=wrapper) + return self.add_view(bwcompat_view, context=HTTPForbidden, + wrapper=wrapper) @action_method def set_notfound_view(self, view=None, attr=None, renderer=None, @@ -2037,7 +2038,8 @@ class Configurator(object): def bwcompat_view(context, request): context = getattr(request, 'context', None) return view(context, request) - return self.add_view(bwcompat_view, context=NotFound, wrapper=wrapper) + return self.add_view(bwcompat_view, context=HTTPNotFound, + wrapper=wrapper) @action_method def set_request_factory(self, factory): @@ -2845,7 +2847,7 @@ class ViewDeriver(object): return view(context, request) msg = getattr(request, 'authdebug_message', 'Unauthorized: %s failed permission check' % view) - raise Forbidden(msg, result=result) + raise HTTPForbidden(msg, result=result) _secured_view.__call_permissive__ = view _secured_view.__permitted__ = _permitted _secured_view.__permission__ = permission diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index 678529c1e..2484f94a3 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,984 +1,7 @@ -""" -HTTP Exceptions ---------------- - -This module contains Pyramid HTTP exception classes. Each class relates to a -single HTTP status code. Each class is a subclass of the -:class:`~HTTPException`. Each exception class is also a :term:`response` -object. - -Each exception class has a status code according to `RFC 2068 -`: codes with 100-300 are not really -errors; 400's are client errors, and 500's are server errors. - -Exception - HTTPException - HTTPOk - * 200 - HTTPOk - * 201 - HTTPCreated - * 202 - HTTPAccepted - * 203 - HTTPNonAuthoritativeInformation - * 204 - HTTPNoContent - * 205 - HTTPResetContent - * 206 - HTTPPartialContent - HTTPRedirection - * 300 - HTTPMultipleChoices - * 301 - HTTPMovedPermanently - * 302 - HTTPFound - * 303 - HTTPSeeOther - * 304 - HTTPNotModified - * 305 - HTTPUseProxy - * 306 - Unused (not implemented, obviously) - * 307 - HTTPTemporaryRedirect - HTTPError - HTTPClientError - * 400 - HTTPBadRequest - * 401 - HTTPUnauthorized - * 402 - HTTPPaymentRequired - * 403 - HTTPForbidden - * 404 - HTTPNotFound - * 405 - HTTPMethodNotAllowed - * 406 - HTTPNotAcceptable - * 407 - HTTPProxyAuthenticationRequired - * 408 - HTTPRequestTimeout - * 409 - HTTPConflict - * 410 - HTTPGone - * 411 - HTTPLengthRequired - * 412 - HTTPPreconditionFailed - * 413 - HTTPRequestEntityTooLarge - * 414 - HTTPRequestURITooLong - * 415 - HTTPUnsupportedMediaType - * 416 - HTTPRequestRangeNotSatisfiable - * 417 - HTTPExpectationFailed - HTTPServerError - * 500 - HTTPInternalServerError - * 501 - HTTPNotImplemented - * 502 - HTTPBadGateway - * 503 - HTTPServiceUnavailable - * 504 - HTTPGatewayTimeout - * 505 - HTTPVersionNotSupported - -Each HTTP exception has the following attributes: - - ``code`` - the HTTP status code for the exception - - ``title`` - remainder of the status line (stuff after the code) - - ``explanation`` - a plain-text explanation of the error message that is - not subject to environment or header substitutions; - it is accessible in the template via ${explanation} - - ``detail`` - a plain-text message customization that is not subject - to environment or header substitutions; accessible in - the template via ${detail} - - ``body_template`` - a ``String.template``-format content fragment used for environment - and header substitution; the default template includes both - the explanation and further detail provided in the - message. - -Each HTTP exception accepts the following parameters: - - ``detail`` - a plain-text override of the default ``detail`` - - ``headers`` - a list of (k,v) header pairs - - ``comment`` - a plain-text additional information which is - usually stripped/hidden for end-users - - ``body_template`` - a ``string.Template`` object containing a content fragment in HTML - that frames the explanation and further detail - -Substitution of response headers into template values is always performed. -Substitution of WSGI environment values is performed if a ``request`` is -passed to the exception's constructor. - -The subclasses of :class:`~_HTTPMove` -(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, -:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and -:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` -field. Reflecting this, these subclasses have one additional keyword argument: -``location``, which indicates the location to which to redirect. -""" -import types -from string import Template -from webob import html_escape as _html_escape from zope.configuration.exceptions import ConfigurationError as ZCE -from zope.interface import implements - -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 environ - # - # - 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
, no other html - # in default body template) - # - # - sets a default app_iter if no body, app_iter, or unicode_body is - # passed - # - # - 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('''\ - - - ${status} - - -

${status}

- ${body} - -''') - - ## 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 = '
' - if comment: - html_comment = '' % 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
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') +from pyramid.response import HTTPNotFound +from pyramid.response import HTTPForbidden NotFound = HTTPNotFound # bw compat Forbidden = HTTPForbidden # bw compat @@ -1008,48 +31,3 @@ class ConfigurationError(ZCE): 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/httpexceptions.py b/pyramid/httpexceptions.py index 8b2a012cc..dbb530b4a 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -1,2 +1,116 @@ -from pyramid.exceptions import * # bw compat +""" +HTTP Exceptions +--------------- + +This module contains Pyramid HTTP exception classes. Each class relates to a +single HTTP status code. Each class is a subclass of the +:class:`~HTTPException`. Each exception class is also a :term:`response` +object. + +Each exception class has a status code according to `RFC 2068 +`: codes with 100-300 are not really +errors; 400's are client errors, and 500's are server errors. + +Exception + HTTPException + HTTPOk + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 306 - Unused (not implemented, obviously) + * 307 - HTTPTemporaryRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + +Each HTTP exception has the following attributes: + + ``code`` + the HTTP status code for the exception + + ``title`` + remainder of the status line (stuff after the code) + + ``explanation`` + a plain-text explanation of the error message that is + not subject to environment or header substitutions; + it is accessible in the template via ${explanation} + + ``detail`` + a plain-text message customization that is not subject + to environment or header substitutions; accessible in + the template via ${detail} + + ``body_template`` + a ``String.template``-format content fragment used for environment + and header substitution; the default template includes both + the explanation and further detail provided in the + message. + +Each HTTP exception accepts the following parameters: + + ``detail`` + a plain-text override of the default ``detail`` + + ``headers`` + a list of (k,v) header pairs + + ``comment`` + a plain-text additional information which is + usually stripped/hidden for end-users + + ``body_template`` + a ``string.Template`` object containing a content fragment in HTML + that frames the explanation and further detail + +Substitution of response headers into template values is always performed. +Substitution of WSGI environment values is performed if a ``request`` is +passed to the exception's constructor. + +The subclasses of :class:`~_HTTPMove` +(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, +:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and +:class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` +field. Reflecting this, these subclasses have one additional keyword argument: +``location``, which indicates the location to which to redirect. +""" + +from pyramid.response import * # API + + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d200d15cf..237727b41 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -55,12 +55,13 @@ class IException(Interface): # not an API """ An interface representing a generic exception """ class IExceptionResponse(IException, IResponse): - """ An interface representing a WSGI response which is also an - exception object. Register an exception view using this interface - as a ``context`` to apply the registered view for all exception - types raised by :app:`Pyramid` internally - (:class:`pyramid.exceptions.NotFound` and - :class:`pyramid.exceptions.Forbidden`).""" + """ An interface representing a WSGI response which is also an exception + object. Register an exception view using this interface as a ``context`` + to apply the registered view for all exception types raised by + :app:`Pyramid` internally (any exception that inherits from + :class:`pyramid.response.Response`, including + :class:`pyramid.response.HTTPNotFound` and + :class:`pyramid.response.HTTPForbidden`).""" class IBeforeRender(Interface): """ @@ -274,9 +275,9 @@ class IExceptionViewClassifier(Interface): class IView(Interface): def __call__(context, request): """ Must return an object that implements IResponse. May - optionally raise ``pyramid.exceptions.Forbidden`` if an + optionally raise ``pyramid.response.HTTPForbidden`` if an authorization failure is detected during view execution or - ``pyramid.exceptions.NotFound`` if the not found page is + ``pyramid.response.HTTPNotFound`` if the not found page is meant to be returned.""" class ISecuredView(IView): diff --git a/pyramid/response.py b/pyramid/response.py index e9f5528a5..41ac354f9 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,8 +1,948 @@ +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 environ + # + # - 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
, no other html + # in default body template) + # + # - sets a default app_iter if no body, app_iter, or unicode_body is + # passed + # + # - 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('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## 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 = '
' + if comment: + html_comment = '' % 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
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') + +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 b8a8639aa..9cd682623 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -16,7 +16,7 @@ from pyramid.interfaces import IViewClassifier from pyramid.events import ContextFound from pyramid.events import NewRequest from pyramid.events import NewResponse -from pyramid.exceptions import NotFound +from pyramid.response import HTTPNotFound from pyramid.request import Request from pyramid.threadlocal import manager from pyramid.traversal import DefaultRootFactory @@ -153,7 +153,7 @@ class Router(object): logger and logger.debug(msg) else: msg = request.path_info - raise NotFound(msg) + raise HTTPNotFound(msg) else: response = view_callable(context, request) diff --git a/pyramid/testing.py b/pyramid/testing.py index a512ede4b..4d7dd252a 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -17,7 +17,7 @@ from pyramid.interfaces import ISession from pyramid.config import Configurator from pyramid.decorator import reify -from pyramid.exceptions import Forbidden +from pyramid.response import HTTPForbidden from pyramid.response import Response from pyramid.registry import Registry from pyramid.security import Authenticated @@ -217,7 +217,7 @@ def registerView(name, result='', view=None, for_=(Interface, Interface), else: def _secure(context, request): if not has_permission(permission, context, request): - raise Forbidden('no permission').exception + raise HTTPForbidden('no permission') else: return view(context, request) _secure.__call_permissive__ = view diff --git a/pyramid/tests/fixtureapp/views.py b/pyramid/tests/fixtureapp/views.py index 9ab985e32..3125c972f 100644 --- a/pyramid/tests/fixtureapp/views.py +++ b/pyramid/tests/fixtureapp/views.py @@ -1,6 +1,6 @@ from zope.interface import Interface from webob import Response -from pyramid.exceptions import Forbidden +from pyramid.response import HTTPForbidden def fixture_view(context, request): """ """ @@ -16,7 +16,7 @@ def exception_view(context, request): def protected_view(context, request): """ """ - raise Forbidden() + raise HTTPForbidden() class IDummy(Interface): pass diff --git a/pyramid/tests/forbiddenapp/__init__.py b/pyramid/tests/forbiddenapp/__init__.py index 614aff037..9ad2dc801 100644 --- a/pyramid/tests/forbiddenapp/__init__.py +++ b/pyramid/tests/forbiddenapp/__init__.py @@ -1,5 +1,5 @@ from webob import Response -from pyramid.exceptions import Forbidden +from pyramid.response import HTTPForbidden def x_view(request): # pragma: no cover return Response('this is private!') @@ -8,7 +8,7 @@ def forbidden_view(context, request): msg = context.message result = context.result message = msg + '\n' + str(result) - resp = Forbidden() + resp = HTTPForbidden() resp.body = message return resp @@ -20,4 +20,4 @@ def includeme(config): config._set_authentication_policy(authn_policy) config._set_authorization_policy(authz_policy) config.add_view(x_view, name='x', permission='private') - config.add_view(forbidden_view, context=Forbidden) + config.add_view(forbidden_view, context=HTTPForbidden) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 7c6389253..6817c5936 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -50,8 +50,8 @@ class ConfiguratorTests(unittest.TestCase): return iface def _assertNotFound(self, wrapper, *arg): - from pyramid.exceptions import NotFound - self.assertRaises(NotFound, wrapper, *arg) + from pyramid.response import HTTPNotFound + self.assertRaises(HTTPNotFound, wrapper, *arg) def _registerEventListener(self, config, event_iface=None): if event_iface is None: # pragma: no cover @@ -205,7 +205,7 @@ class ConfiguratorTests(unittest.TestCase): def test_ctor_httpexception_view_default(self): from pyramid.interfaces import IExceptionResponse - from pyramid.exceptions import default_exceptionresponse_view + from pyramid.response import default_exceptionresponse_view from pyramid.interfaces import IRequest config = self._makeOne() view = self._getViewCallable(config, @@ -321,16 +321,17 @@ 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.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.registry import Registry reg = Registry() config = self._makeOne(reg, autocommit=True) config.setup_registry() # registers IExceptionResponse default view def myview(context, request): return 'OK' - config.add_view(myview, context=NotFound) + config.add_view(myview, context=HTTPNotFound) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, 'OK') @@ -1694,14 +1695,14 @@ class ConfiguratorTests(unittest.TestCase): self._assertNotFound(wrapper, None, request) def test_add_view_with_header_val_missing(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound view = lambda *arg: 'OK' config = self._makeOne(autocommit=True) config.add_view(view=view, header=r'Host:\d') wrapper = self._getViewCallable(config) request = self._makeRequest(config) request.headers = {'NoHost':'1'} - self.assertRaises(NotFound, wrapper, None, request) + self.assertRaises(HTTPNotFound, wrapper, None, request) def test_add_view_with_accept_match(self): view = lambda *arg: 'OK' @@ -2228,12 +2229,13 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) request = self._makeRequest(config) - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, (None, request)) @@ -2241,13 +2243,14 @@ class ConfiguratorTests(unittest.TestCase): def test_set_notfound_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_notfound_view(view) request = self._makeRequest(config) request.context = 'abc' - view = self._getViewCallable(config, ctx_iface=implementedBy(NotFound), + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) self.assertEqual(result, ('abc', request)) @@ -2256,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.exceptions import NotFound + from pyramid.response import HTTPNotFound config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_notfound_view(view, @@ -2265,7 +2268,7 @@ class ConfiguratorTests(unittest.TestCase): try: # chameleon depends on being able to find a threadlocal registry request = self._makeRequest(config) view = self._getViewCallable(config, - ctx_iface=implementedBy(NotFound), + ctx_iface=implementedBy(HTTPNotFound), request_iface=IRequest) result = view(None, request) finally: @@ -2275,7 +2278,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden config = self._makeOne(autocommit=True) view = lambda *arg: 'OK' config.set_forbidden_view(view) @@ -2288,7 +2291,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_request_has_context(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden config = self._makeOne(autocommit=True) view = lambda *arg: arg config.set_forbidden_view(view) @@ -2303,7 +2306,7 @@ class ConfiguratorTests(unittest.TestCase): def test_set_forbidden_view_with_renderer(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden config = self._makeOne(autocommit=True) view = lambda *arg: {} config.set_forbidden_view(view, @@ -3682,7 +3685,7 @@ class TestViewDeriver(unittest.TestCase): "None against context None): True") def test_debug_auth_permission_authpol_denied(self): - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden view = lambda *arg: 'OK' self.config.registry.settings = dict( debug_authorization=True, reload_templates=True) @@ -3810,7 +3813,7 @@ class TestViewDeriver(unittest.TestCase): self.assertEqual(predicates, [True, True]) def test_with_predicates_notall(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound view = lambda *arg: 'OK' predicates = [] def predicate1(context, request): @@ -3823,7 +3826,7 @@ class TestViewDeriver(unittest.TestCase): result = deriver(view) request = self._makeRequest() request.method = 'POST' - self.assertRaises(NotFound, result, None, None) + self.assertRaises(HTTPNotFound, result, None, None) self.assertEqual(predicates, [True, True]) def test_with_wrapper_viewname(self): @@ -4618,14 +4621,14 @@ class TestMultiView(unittest.TestCase): self.assertEqual(mv.get_views(request), mv.views) def test_match_not_found(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.match, context, request) + self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_fails(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() def view(context, request): """ """ @@ -4633,7 +4636,7 @@ class TestMultiView(unittest.TestCase): mv.views = [(100, view, None)] context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.match, context, request) + self.assertRaises(HTTPNotFound, mv.match, context, request) def test_match_predicate_succeeds(self): mv = self._makeOne() @@ -4647,11 +4650,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, view) def test_permitted_no_views(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv.__permitted__, context, request) + self.assertRaises(HTTPNotFound, mv.__permitted__, context, request) def test_permitted_no_match_with__permitted__(self): mv = self._makeOne() @@ -4674,11 +4677,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(result, False) def test__call__not_found(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call__intermediate_not_found(self): from pyramid.exceptions import PredicateMismatch @@ -4696,17 +4699,17 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test___call__raise_not_found_isnt_interpreted_as_pred_mismatch(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() request.view_name = '' def view1(context, request): - raise NotFound + raise HTTPNotFound def view2(context, request): """ """ mv.views = [(100, view1, None), (99, view2, None)] - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call__(self): mv = self._makeOne() @@ -4721,11 +4724,11 @@ class TestMultiView(unittest.TestCase): self.assertEqual(response, expected_response) def test__call_permissive__not_found(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound mv = self._makeOne() context = DummyContext() request = DummyRequest() - self.assertRaises(NotFound, mv, context, request) + self.assertRaises(HTTPNotFound, mv, context, request) def test___call_permissive_has_call_permissive(self): mv = self._makeOne() diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index f2e577416..673fb6712 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -12,6 +12,11 @@ class TestNotFound(unittest.TestCase): self.assertEqual(e.status, '404 Not Found') self.assertEqual(e.message, 'notfound') + def test_response_equivalence(self): + from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound + self.assertTrue(NotFound is HTTPNotFound) + class TestForbidden(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import Forbidden @@ -24,294 +29,8 @@ class TestForbidden(unittest.TestCase): self.assertEqual(e.status, '403 Forbidden') self.assertEqual(e.message, 'forbidden') -class Test_abort(unittest.TestCase): - def _callFUT(self, *arg, **kw): - from pyramid.exceptions import abort - return abort(*arg, **kw) - - def test_status_404(self): - from pyramid.exceptions import HTTPNotFound - self.assertRaises(HTTPNotFound, self._callFUT, 404) - - def test_status_201(self): - from pyramid.exceptions import HTTPCreated - self.assertRaises(HTTPCreated, self._callFUT, 201) - - def test_extra_kw(self): - from pyramid.exceptions 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.exceptions import redirect - return redirect(*arg, **kw) - - def test_default(self): - from pyramid.exceptions 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.exceptions 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.exceptions 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.exceptions 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.exceptions 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.exceptions 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_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('' 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.exceptions 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.exceptions 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.exceptions 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_response_equivalence(self): + from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden + self.assertTrue(Forbidden is HTTPForbidden) diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index afa5a94de..28adc9d3d 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -3,7 +3,7 @@ import unittest class TestIt(unittest.TestCase): def test_bwcompat_imports(self): from pyramid.httpexceptions import HTTPNotFound as one - from pyramid.exceptions import HTTPNotFound as two + from pyramid.response import HTTPNotFound as two self.assertTrue(one is two) diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py new file mode 100644 index 000000000..6cc87fc0a --- /dev/null +++ b/pyramid/tests/test_response.py @@ -0,0 +1,308 @@ +import unittest + +class TestResponse(unittest.TestCase): + def _getTargetClass(self): + 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): + 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') + + 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('' 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 + diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index b869a3830..106f7c57d 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -136,37 +136,37 @@ class TestRouter(unittest.TestCase): self.assertEqual(router.request_factory, DummyRequestFactory) def test_call_traverser_default(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_traverser_raises_notfound_class(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=NotFound) + self._registerTraverserFactory(context, raise_error=HTTPNotFound) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) def test_traverser_raises_notfound_instance(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() - self._registerTraverserFactory(context, raise_error=NotFound('foo')) + self._registerTraverserFactory(context, raise_error=HTTPNotFound('foo')) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('foo' in why[0], why) def test_traverser_raises_forbidden_class(self): - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=Forbidden) @@ -175,7 +175,7 @@ class TestRouter(unittest.TestCase): self.assertRaises(Forbidden, router, environ, start_response) def test_traverser_raises_forbidden_instance(self): - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context, raise_error=Forbidden('foo')) @@ -185,20 +185,20 @@ class TestRouter(unittest.TestCase): self.assertTrue('foo' in why[0], why) def test_call_no_view_registered_no_isettings(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_false(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -206,13 +206,13 @@ class TestRouter(unittest.TestCase): self._registerSettings(debug_notfound=False) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('/' in why[0], why) self.assertFalse('debug_notfound' in why[0]) self.assertEqual(len(logger.messages), 0) def test_call_no_view_registered_debug_notfound_true(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound environ = self._makeEnviron() context = DummyContext() self._registerTraverserFactory(context) @@ -220,7 +220,7 @@ class TestRouter(unittest.TestCase): logger = self._registerLogger() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue( "debug_notfound of url http://localhost:8080/; path_info: '/', " "context:" in why[0]) @@ -323,7 +323,7 @@ class TestRouter(unittest.TestCase): def test_call_view_registered_specific_fail(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.interfaces import IViewClassifier class IContext(Interface): pass @@ -339,12 +339,12 @@ class TestRouter(unittest.TestCase): self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) def test_call_view_raises_forbidden(self): from zope.interface import Interface from zope.interface import directlyProvides - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden class IContext(Interface): pass from pyramid.interfaces import IRequest @@ -368,17 +368,17 @@ class TestRouter(unittest.TestCase): pass from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound context = DummyContext() directlyProvides(context, IContext) self._registerTraverserFactory(context, subpath=['']) response = DummyResponse() - view = DummyView(response, raise_exception=NotFound("notfound")) + view = DummyView(response, raise_exception=HTTPNotFound("notfound")) environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, IRequest, IContext) router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertEqual(why[0], 'notfound') def test_call_request_has_response_callbacks(self): @@ -566,7 +566,7 @@ class TestRouter(unittest.TestCase): "pattern: 'archives/:action/:article', ")) def test_call_route_match_miss_debug_routematch(self): - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound logger = self._registerLogger() self._registerSettings(debug_routematch=True) self._registerRouteRequest('foo') @@ -577,7 +577,7 @@ class TestRouter(unittest.TestCase): self._registerRootFactory(context) router = self._makeOne() start_response = DummyStartResponse() - self.assertRaises(NotFound, router, environ, start_response) + self.assertRaises(HTTPNotFound, router, environ, start_response) self.assertEqual(len(logger.messages), 1) self.assertEqual( @@ -627,11 +627,11 @@ class TestRouter(unittest.TestCase): def test_root_factory_raises_notfound(self): from pyramid.interfaces import IRootFactory - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): - raise NotFound('from root factory') + raise HTTPNotFound('from root factory') self.registry.registerUtility(rootfactory, IRootFactory) class IContext(Interface): pass @@ -640,12 +640,12 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() router = self._makeOne() start_response = DummyStartResponse() - why = exc_raised(NotFound, router, environ, start_response) + why = exc_raised(HTTPNotFound, router, environ, start_response) self.assertTrue('from root factory' in why[0]) def test_root_factory_raises_forbidden(self): from pyramid.interfaces import IRootFactory - from pyramid.exceptions import Forbidden + from pyramid.response import Forbidden from zope.interface import Interface from zope.interface import directlyProvides def rootfactory(request): diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py index 58ca2b7d9..0288884b7 100644 --- a/pyramid/tests/test_testing.py +++ b/pyramid/tests/test_testing.py @@ -150,7 +150,7 @@ class Test_registerView(TestBase): def test_registerView_with_permission_denying(self): from pyramid import testing - from pyramid.exceptions import Forbidden + from pyramid.response import HTTPForbidden def view(context, request): """ """ view = testing.registerView('moo.html', view=view, permission='bar') @@ -160,7 +160,7 @@ class Test_registerView(TestBase): from pyramid.view import render_view_to_response request = DummyRequest() request.registry = self.registry - self.assertRaises(Forbidden, render_view_to_response, + self.assertRaises(HTTPForbidden, render_view_to_response, None, request, 'moo.html') def test_registerView_with_permission_denying2(self): diff --git a/pyramid/view.py b/pyramid/view.py index 975464124..0b5c7cdc9 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.exceptions import HTTPFound -from pyramid.exceptions import default_exceptionresponse_view +from pyramid.response import HTTPFound +from pyramid.response import default_exceptionresponse_view from pyramid.renderers import RendererHelper from pyramid.static import static_view from pyramid.threadlocal import get_current_registry @@ -48,7 +48,7 @@ def render_view_to_response(context, request, name='', secure=True): protected by a permission, the permission will be checked before calling the view function. If the permission check disallows view execution (based on the current :term:`authorization policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised. + :exc:`pyramid.response.HTTPForbidden` exception will be raised. The exception's ``args`` attribute explains why the view access was disallowed. @@ -92,7 +92,7 @@ def render_view_to_iterable(context, request, name='', secure=True): permission, the permission will be checked before the view function is invoked. If the permission check disallows view execution (based on the current :term:`authentication policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised; + :exc:`pyramid.response.HTTPForbidden` exception will be raised; its ``args`` attribute explains why the view access was disallowed. @@ -121,7 +121,7 @@ def render_view(context, request, name='', secure=True): permission, the permission will be checked before the view is invoked. If the permission check disallows view execution (based on the current :term:`authorization policy`), a - :exc:`pyramid.exceptions.Forbidden` exception will be raised; + :exc:`pyramid.response.HTTPForbidden` exception will be raised; its ``args`` attribute explains why the view access was disallowed. @@ -249,14 +249,13 @@ class AppendSlashNotFoundViewFactory(object): .. code-block:: python - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.view import AppendSlashNotFoundViewFactory - from pyramid.exceptions import HTTPNotFound def notfound_view(context, request): return HTTPNotFound('nope') custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) - config.add_view(custom_append_slash, context=NotFound) + config.add_view(custom_append_slash, context=HTTPNotFound) The ``notfound_view`` supplied must adhere to the two-argument view callable calling convention of ``(context, request)`` @@ -303,9 +302,9 @@ routes are not considered when attempting to find a matching route. Use the :meth:`pyramid.config.Configurator.add_view` method to configure this view as the Not Found view:: - from pyramid.exceptions import NotFound + from pyramid.response import HTTPNotFound from pyramid.view import append_slash_notfound_view - config.add_view(append_slash_notfound_view, context=NotFound) + config.add_view(append_slash_notfound_view, context=HTTPNotFound) See also :ref:`changing_the_notfound_view`. -- cgit v1.2.3 From 71738bc9418170cebfd532fbed6bb48ac8c3fb40 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 31 May 2011 15:30:24 -0400 Subject: broke this in the last commit --- docs/tutorials/bfg/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/bfg/index.rst b/docs/tutorials/bfg/index.rst index e01345158..e68e63b0b 100644 --- a/docs/tutorials/bfg/index.rst +++ b/docs/tutorials/bfg/index.rst @@ -106,7 +106,7 @@ Here's how to convert a :mod:`repoze.bfg` application to a - ZCML files which contain directives that have attributes which name a ``repoze.bfg`` API module or attribute of an API module - (e.g. ``context="repoze.bfg.exeptions.NotFound"``) will be + (e.g. ``context="repoze.bfg.exceptions.NotFound"``) will be converted to :app:`Pyramid` compatible ZCML attributes (e.g. ``context="pyramid.exceptions.NotFound``). Every ZCML file beneath the top-level path (files ending with ``.zcml``) will be -- cgit v1.2.3 From df15ed98612e7962e3122da52d8d5f5b9d8882b2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 4 Jun 2011 18:43:25 -0400 Subject: - 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.: 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 --- CHANGES.txt | 26 ++++++++++++++++++++++++++ docs/api/interfaces.rst | 2 ++ docs/glossary.rst | 4 ++++ docs/narr/hooks.rst | 36 ++++++++++++++++++++++++++++++++++++ pyramid/interfaces.py | 8 ++++++++ pyramid/response.py | 5 ++--- pyramid/router.py | 33 ++++++++++++++++++++++----------- pyramid/tests/test_router.py | 31 +++++++++++++++++++++++++++++++ 8 files changed, 131 insertions(+), 14 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 15c86c13c..7840bc525 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -132,6 +132,32 @@ Features - The ``pyramid.request.Response`` class now has a ``RequestClass`` interface which points at ``pyramid.response.Request``. +- 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.: + + 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 + Bug Fixes --------- diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index ac282fbcc..3a60fa4dc 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -57,4 +57,6 @@ Other Interfaces .. autointerface:: IMultiDict :members: + .. autointerface:: IResponder + :members: diff --git a/docs/glossary.rst b/docs/glossary.rst index 20b9bfd64..dbab331c1 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -900,5 +900,9 @@ Glossary http://docs.python.org/distutils/index.html for more information. :term:`setuptools` is actually an *extension* of the Distutils. + exception response + A :term:`response` that is generated as the result of a raised exception + being caught by an :term:`exception view`. + diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index d620b5672..aa151d281 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -521,6 +521,42 @@ The default context URL generator is available for perusal as the class `_ of the :term:`Pylons` GitHub Pyramid repository. +.. index:: + single: IResponder + +.. _using_iresponder: + +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:: + + from pyramid.interfaces import IResponder + from pyramid.response import Response + from myapp import MyResponder + + config.registry.registerAdapter(MyResponder, (Response,), + 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.: + + 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 + .. index:: single: view mapper diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 237727b41..d5d382492 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -229,6 +229,14 @@ class IMultiDict(Interface): # docs-only interface dictionary. This is similar to the kind of dictionary often used to represent the variables in a web request. """ +class IResponder(Interface): + """ Adapter from IResponse to an IResponder. See :ref:`using_iresponder` + for usage details. New in Pyramid 1.1. + """ + def __call__(self, request, start_response): + """ Call the WSGI ``start_response`` callable passed as + ``start_response`` and return an ``app_iter``.""" + # internal interfaces class IRequest(Interface): diff --git a/pyramid/response.py b/pyramid/response.py index 41ac354f9..6e6af32c8 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -21,7 +21,6 @@ def _no_escape(value): value = str(value) return value - class HTTPException(Exception): # bw compat pass @@ -40,7 +39,7 @@ class WSGIHTTPException(Response, HTTPException): # as a result: # # - bases plaintext vs. html result on self.content_type rather than - # on request environ + # on request accept header # # - doesn't add request.environ keys to template substitutions unless # 'request' is passed as a constructor keyword argument. @@ -49,7 +48,7 @@ class WSGIHTTPException(Response, HTTPException): # in default body template) # # - sets a default app_iter if no body, app_iter, or unicode_body is - # passed + # 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 diff --git a/pyramid/router.py b/pyramid/router.py index 9cd682623..92c6cc920 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -12,6 +12,7 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import ITraverser from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier +from pyramid.interfaces import IResponder from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -58,6 +59,7 @@ class Router(object): logger = self.logger manager = self.threadlocal_manager request = None + responder = default_responder threadlocals = {'registry':registry, 'request':request} manager.push(threadlocals) @@ -186,21 +188,30 @@ class Router(object): if request.response_callbacks: request._process_response_callbacks(response) - try: - headers = response.headerlist - app_iter = response.app_iter - status = response.status - except AttributeError: - raise ValueError( - 'Non-response object returned from view named %s ' - '(and no renderer): %r' % (view_name, response)) + responder = adapters.queryAdapter(response, IResponder) + if responder is None: + responder = default_responder(response) finally: if request is not None and request.finished_callbacks: request._process_finished_callbacks() - start_response(status, headers) - return app_iter - + return responder(request, start_response) + finally: manager.pop() + +def default_responder(response): + def inner(request, start_response): + try: + headers = response.headerlist + app_iter = response.app_iter + status = response.status + except AttributeError: + raise ValueError( + 'Non-response object returned from view ' + '(and no renderer): %r' % (response)) + start_response(status, headers) + return app_iter + return inner + diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 106f7c57d..a89de7a36 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -449,6 +449,37 @@ class TestRouter(unittest.TestCase): exc_raised(NotImplementedError, router, environ, start_response) self.assertEqual(environ['called_back'], True) + def test_call_with_overridden_iresponder_factory(self): + from zope.interface import Interface + from zope.interface import directlyProvides + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponder + context = DummyContext() + class IFoo(Interface): + pass + directlyProvides(context, IFoo) + self._registerTraverserFactory(context, subpath=['']) + class DummyResponder(object): + def __init__(self, response): + self.response = response + def __call__(self, request, start_response): + self.response.responder_used = True + return '123' + self.registry.registerAdapter(DummyResponder, (None,), + IResponder, name='') + response = DummyResponse('200 OK') + directlyProvides(response, IFoo) + def view(context, request): + return response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, Interface) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertTrue(response.responder_used) + self.assertEqual(result, '123') + def test_call_request_factory_raises(self): # making sure finally doesnt barf when a request cannot be created environ = self._makeEnviron() -- cgit v1.2.3 From 99edc51a3b05309c7f5d98ff96289ec51b1d7660 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 11 Jun 2011 05:35:27 -0400 Subject: - 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. --- CHANGES.txt | 79 +- TODO.txt | 6 + docs/api/httpexceptions.rst | 6 +- docs/api/response.rst | 1 + docs/glossary.rst | 4 +- docs/narr/hooks.rst | 42 +- docs/narr/renderers.rst | 69 +- docs/narr/router.rst | 12 +- docs/narr/testing.rst | 4 +- docs/narr/urldispatch.rst | 4 +- docs/narr/views.rst | 209 ++--- docs/narr/webob.rst | 23 +- docs/tutorials/wiki/authorization.rst | 22 +- docs/tutorials/wiki/definingviews.rst | 8 +- .../wiki/src/authorization/tutorial/login.py | 4 +- .../wiki/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki/src/views/tutorial/views.py | 2 +- docs/tutorials/wiki2/definingviews.rst | 4 +- .../wiki2/src/authorization/tutorial/__init__.py | 2 +- .../wiki2/src/authorization/tutorial/login.py | 2 +- .../wiki2/src/authorization/tutorial/views.py | 2 +- docs/tutorials/wiki2/src/views/tutorial/views.py | 2 +- docs/whatsnew-1.1.rst | 12 +- pyramid/config.py | 6 +- pyramid/exceptions.py | 6 +- pyramid/httpexceptions.py | 902 +++++++++++++++++++- pyramid/interfaces.py | 16 +- pyramid/response.py | 946 +-------------------- pyramid/router.py | 17 +- pyramid/testing.py | 2 +- pyramid/tests/fixtureapp/views.py | 2 +- pyramid/tests/forbiddenapp/__init__.py | 2 +- pyramid/tests/test_config.py | 46 +- pyramid/tests/test_exceptions.py | 15 +- pyramid/tests/test_httpexceptions.py | 278 +++++- pyramid/tests/test_response.py | 305 +------ pyramid/tests/test_router.py | 117 ++- pyramid/tests/test_testing.py | 2 +- pyramid/view.py | 44 +- 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 ------------ diff --git a/TODO.txt b/TODO.txt index d85f3b7f0..04c6e60d7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -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. ``['Hello - world!']`` 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 `_ is also useful. +:mod:`pyramid.response` documentation. More details about exception +responses are in the :mod:`pyramid.httpexceptions` API documentation. The +`WebOb documentation `_ 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
, 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('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## 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 = '
' + if comment: + html_comment = '' % 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
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
, 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('''\ - - - ${status} - - -

${status}

- ${body} - -''') - - ## 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 = '
' - if comment: - html_comment = '' % 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
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('' 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('' 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) -- cgit v1.2.3 From fc048afce7c58a1e794b495b438f4ee76f084b69 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 11 Jun 2011 05:42:43 -0400 Subject: todo --- TODO.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TODO.txt b/TODO.txt index 04c6e60d7..1a178a23d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,11 @@ Must-Have - Depend on only __call__ interface or only 3-attr interface in builtin code that deals with response objects. +- Figure out what to do with ``is_response``. + +- Docs mention ``exception.args[0]`` as a way to get messages; check that + this works. + Should-Have ----------- -- cgit v1.2.3 From f0d77e8f3cec1ff90a2029fe143580fd42cf81aa Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 11 Jun 2011 05:45:15 -0400 Subject: todo --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index 1a178a23d..4b82208bd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,8 @@ Pyramid TODOs Must-Have --------- +- To subclass or not subclass http exceptions. + - Depend on only __call__ interface or only 3-attr interface in builtin code that deals with response objects. -- cgit v1.2.3 From d868fff7597c5a05acd1f5c024fc45dde9880413 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 06:17:00 -0400 Subject: - Remove IResponder abstraction in favor of more general IResponse abstraction. - It is now possible to return an arbitrary object from a Pyramid view callable even if a renderer is not used, as long as a suitable adapter to ``pyramid.interfaces.IResponse`` is registered for the type of the returned object. See the section in the Hooks chapter of the documentation entitled "Changing How Pyramid Treats View Responses". - The Pyramid router now, by default, expects response objects returned from view callables to implement the ``pyramid.interfaces.IResponse`` interface. Unlike the Pyramid 1.0 version of this interface, objects which implement IResponse now must define a ``__call__`` method that accepts ``environ`` and ``start_response``, and which returns an ``app_iter`` iterable, among other things. Previously, it was possible to return any object which had the three WebOb ``app_iter``, ``headerlist``, and ``status`` attributes as a response, so this is a backwards incompatibility. It is possible to get backwards compatibility back by registering an adapter to IResponse from the type of object you're now returning from view callables. See the section in the Hooks chapter of the documentation entitled "Changing How Pyramid Treats View Responses". - The ``pyramid.interfaces.IResponse`` interface is now much more extensive. Previously it defined only ``app_iter``, ``status`` and ``headerlist``; now it is basically intended to directly mirror the ``webob.Response`` API, which has many methods and attributes. - Documentation changes to support above. --- CHANGES.txt | 42 ++++++----- TODO.txt | 19 ++++- docs/api/interfaces.rst | 2 +- docs/api/request.rst | 33 ++++---- docs/designdefense.rst | 4 +- docs/glossary.rst | 12 +-- docs/narr/assets.rst | 2 +- docs/narr/hooks.rst | 134 ++++++++++++++++++++++++--------- docs/narr/renderers.rst | 2 +- docs/narr/router.rst | 6 +- docs/narr/views.rst | 36 ++++----- docs/narr/webob.rst | 66 ++++++++-------- docs/tutorials/wiki/definingviews.rst | 10 ++- docs/tutorials/wiki2/definingviews.rst | 3 +- pyramid/config.py | 39 ++++++++-- pyramid/interfaces.py | 8 -- pyramid/registry.py | 19 +++++ pyramid/renderers.py | 4 +- pyramid/request.py | 2 + pyramid/response.py | 2 +- pyramid/router.py | 47 +++--------- pyramid/session.py | 14 +--- pyramid/tests/test_config.py | 78 ++++++++++++++++--- pyramid/tests/test_router.py | 122 +++++++----------------------- pyramid/tests/test_session.py | 15 +--- pyramid/tests/test_view.py | 23 +++++- pyramid/view.py | 6 ++ 27 files changed, 411 insertions(+), 339 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e413f0657..5e8df1a0b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -127,11 +127,11 @@ Features - The ``pyramid.request.Response`` class now has a ``RequestClass`` interface which points at ``pyramid.response.Request``. -- 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. See the section in the Hooks chapter of the documentation - entitled "Changing How Pyramid Treats Response Objects". +- It is now possible to return an arbitrary object from a Pyramid view + callable even if a renderer is not used, as long as a suitable adapter to + ``pyramid.interfaces.IResponse`` is registered for the type of the returned + object. See the section in the Hooks chapter of the documentation entitled + "Changing How Pyramid Treats View Responses". - The Pyramid router will now, by default, call the ``__call__`` method of WebOb response objects when returning a WSGI response. This means that, @@ -306,22 +306,26 @@ Behavior Changes ``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). +Backwards Incompatibilities +--------------------------- - 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. + view callables to implement the ``pyramid.interfaces.IResponse`` interface. + Unlike the Pyramid 1.0 version of this interface, objects which implement + IResponse now must define a ``__call__`` method that accepts ``environ`` + and ``start_response``, and which returns an ``app_iter`` iterable, among + other things. Previously, it was possible to return any object which had + the three WebOb ``app_iter``, ``headerlist``, and ``status`` attributes as + a response, so this is a backwards incompatibility. It is possible to get + backwards compatibility back by registering an adapter to IResponse from + the type of object you're now returning from view callables. See the + section in the Hooks chapter of the documentation entitled "Changing How + Pyramid Treats View Responses". + +- The ``pyramid.interfaces.IResponse`` interface is now much more extensive. + Previously it defined only ``app_iter``, ``status`` and ``headerlist``; now + it is basically intended to directly mirror the ``webob.Response`` API, + which has many methods and attributes. Dependencies ------------ diff --git a/TODO.txt b/TODO.txt index 4b82208bd..ca2433d3c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,14 +6,27 @@ Must-Have - To subclass or not subclass http exceptions. -- Depend on only __call__ interface or only 3-attr interface in builtin code - that deals with response objects. +- Flesh out IResponse interface. Attributes Used internally: unicode_body / + body / content_type / charset / cache_expires / headers/ + default_content_type / set_cookie / headerlist / app_iter / status / + __call__. -- Figure out what to do with ``is_response``. +- Deprecate view.is_response? + +- Move is_response to response.py? + +- Make sure registering IResponse adapter for webob.Response doesn't make it + impossible to register an IResponse adapter for an interface that a + webob.Response happens to implement. + +- Run whatsitdoing tests. - Docs mention ``exception.args[0]`` as a way to get messages; check that this works. +- Deprecate response_foo attrs on request at attribute set time rather than + lookup time. + Should-Have ----------- diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 3a60fa4dc..51a1963b5 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -57,6 +57,6 @@ Other Interfaces .. autointerface:: IMultiDict :members: - .. autointerface:: IResponder + .. autointerface:: IResponse :members: diff --git a/docs/api/request.rst b/docs/api/request.rst index 8cb424658..27ce395ac 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -107,7 +107,9 @@ return {'text':'Value that will be used by the renderer'} Mutations to this response object will be preserved in the response sent - to the client after rendering. + to the client after rendering. For more information about using + ``request.response`` in conjunction with a renderer, see + :ref:`request_response_attr`. Non-renderer code can also make use of request.response instead of creating a response "by hand". For example, in view code:: @@ -162,20 +164,21 @@ .. attribute:: response_* - .. warning:: As of Pyramid 1.1, assignment to ``response_*`` attrs are - deprecated. Assigning to one will cause a deprecation warning to be - emitted. Instead of assigning ``response_*`` attributes to the - request, use API of the the :attr:`pyramid.request.Request.response` - object (exposed to view code as ``request.response``) to influence - response behavior. - - You can set attributes on a :class:`pyramid.request.Request` which will - influence the behavor of *rendered* responses (views which use a - :term:`renderer` and which don't directly return a response). These - attributes begin with ``response_``, such as ``response_headerlist``. If - you need to influence response values from a view that uses a renderer - (such as the status code, a header, the content type, etc) see, - :ref:`response_prefixed_attrs`. + In Pyramid 1.0, you could set attributes on a + :class:`pyramid.request.Request` which influenced the behavor of + *rendered* responses (views which use a :term:`renderer` and which + don't directly return a response). These attributes began with + ``response_``, such as ``response_headerlist``. If you needed to + influence response values from a view that uses a renderer (such as the + status code, a header, the content type, etc) you would set these + attributes. See :ref:`response_prefixed_attrs` for further discussion. + As of Pyramid 1.1, assignment to ``response_*`` attrs are deprecated. + Assigning to one is still supported but will cause a deprecation + warning to be emitted, and eventually the feature will be removed. For + new code, instead of assigning ``response_*`` attributes to the + request, use API of the the :attr:`pyramid.request.Request.response` + object (exposed to view code as ``request.response``) to influence + rendered response behavior. .. note:: diff --git a/docs/designdefense.rst b/docs/designdefense.rst index 136b9c5de..de6c0af33 100644 --- a/docs/designdefense.rst +++ b/docs/designdefense.rst @@ -428,7 +428,7 @@ allowing people to define "custom" view predicates: :linenos: from pyramid.view import view_config - from webob import Response + from pyramid.response import Response def subpath(context, request): return request.subpath and request.subpath[0] == 'abc' @@ -1497,7 +1497,7 @@ comments take into account what we've discussed in the .. code-block:: python :linenos: - from webob import Response # explicit response objects, no TL + from pyramid.response import Response # explicit response objects, no TL from paste.httpserver import serve # explicitly WSGI def hello_world(request): # accepts a request; no request thread local reqd diff --git a/docs/glossary.rst b/docs/glossary.rst index 079a069b4..d3ba9a545 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -16,12 +16,12 @@ Glossary positional argument, returns a ``WebOb`` compatible request. response - An object that has three attributes: ``app_iter`` (representing an - iterable body), ``headerlist`` (representing the http headers sent - to the user agent), and ``status`` (representing the http status - string sent to the user agent). This is the interface defined for - ``WebOb`` response objects. See :ref:`webob_chapter` for - information about response objects. + An object returned by a :term:`view callable` that represents response + data returned to the requesting user agent. It must implements the + :class:`pyramid.interfaces.IResponse` interface. A response object is + typically an instance of the :class:`pyramid.response.Response` class or + a subclass such as :class:`pyramid.httpexceptions.HTTPFound`. See + :ref:`webob_chapter` for information about response objects. Repoze "Repoze" is essentially a "brand" of software developed by `Agendaless diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 8d0e7058c..0d50b0106 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -358,7 +358,7 @@ do so, do things "by hand". First define the view callable. :linenos: import os - from webob import Response + from pyramid.response import Response def favicon_view(request): here = os.path.dirname(__file__) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index b6a781417..0db8ce5e0 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -523,41 +523,103 @@ The default context URL generator is available for perusal as the class :term:`Pylons` GitHub Pyramid repository. .. index:: - single: IResponder - -.. _using_iresponder: - -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 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 - from myapp import MyResponder - - config.registry.registerAdapter(MyResponder, (Response,), - IResponder, name='') - -Overriding makes it possible to reuse response object implementations which -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): - """ Obtain a reference to the response """ - self.response = response - def __call__(self, request, start_response): - """ Call start_response and return an app_iter """ - start_response(self.response.status, self.response.headerlist) - return self.response.app_iter + single: IResponse + +.. _using_iresponse: + +Changing How Pyramid Treats View Responses +------------------------------------------ + +It is possible to control how Pyramid treats the result of calling a view +callable on a per-type basis by using a hook involving +:class:`pyramid.interfaces.IResponse`. + +.. note:: This feature is new as of Pyramid 1.1. + +Pyramid, in various places, adapts the result of calling a view callable to +the :class:`~pyramid.interfaces.IResponse` interface to ensure that the +object returned by the view callable is a "true" response object. The vast +majority of time, the result of this adaptation is the result object itself, +as view callables written by "civilians" who read the narrative documentation +contained in this manual will always return something that implements the +:class:`~pyramid.interfaces.IResponse` interface. Most typically, this will +be an instance of the :class:`pyramid.response.Response` class or a subclass. +If a civilian returns a non-Response object from a view callable that isn't +configured to use a :term:`renderer`, he will typically expect the router to +raise an error. However, you can hook Pyramid in such a way that users can +return arbitrary values from a view callable by providing an adapter which +converts the arbitrary return value into something that implements +:class:`~pyramid.interfaces.IResponse`. + +For example, if you'd like to allow view callables to return bare string +objects (without requiring a a :term:`renderer` to convert a string to a +response object), you can register an adapter which converts the string to a +Response: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import IResponse + from pyramid.response import Response + + def string_response_adapter(s): + response = Response(s) + return response + + # config is an instance of pyramid.config.Configurator + + config.registry.registerAdapter(string_response_adapter, (str,), + IResponse) + +Likewise, if you want to be able to return a simplified kind of response +object from view callables, you can use the IResponse hook to register an +adapter to the more complex IResponse interface: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import IResponse + from pyramid.response import Response + + class SimpleResponse(object): + def __init__(self, body): + self.body = body + + def simple_response_adapter(simple_response): + response = Response(simple_response.body) + return response + + # config is an instance of pyramid.config.Configurator + + config.registry.registerAdapter(simple_response_adapter, + (SimpleResponse,), + IResponse) + +If you want to implement your own Response object instead of using the +:class:`pyramid.response.Response` object in any capacity at all, you'll have +to make sure the object implements every attribute and method outlined in +:class:`pyramid.interfaces.IResponse` *and* you'll have to ensure that it's +marked up with ``zope.interface.implements(IResponse)``: + + from pyramid.interfaces import IResponse + from zope.interface import implements + + class MyResponse(object): + implements(IResponse) + # ... an implementation of every method and attribute + # documented in IResponse should follow ... + +When an alternate response object implementation is returned by a view +callable, if that object asserts that it implements +:class:`~pyramid.interfaces.IResponse` (via +``zope.interface.implements(IResponse)``) , an adapter needn't be registered +for the object; Pyramid will use it directly. + +An IResponse adapter for ``webob.Response`` (as opposed to +:class:`pyramid.response.Response`) is registered by Pyramid by default at +startup time, as by their nature, instances of this class (and instances of +subclasses of the class) will natively provide IResponse. The adapter +registered for ``webob.Response`` simply returns the response object. .. index:: single: view mapper @@ -628,7 +690,7 @@ A user might make use of these framework components like so: # user application - from webob import Response + from pyramid.response import Response from pyramid.config import Configurator import pyramid_handlers from paste.httpserver import serve diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 99ee14908..c4a37c23d 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -416,7 +416,7 @@ effect, you must return ``request.response``: For more information on attributes of the request, see the API documentation in :ref:`request_module`. For more information on the API of -``request.response``, see :class:`pyramid.response.Response`. +``request.response``, see :attr:`pyramid.request.Request.response`. .. _response_prefixed_attrs: diff --git a/docs/narr/router.rst b/docs/narr/router.rst index 30d54767e..0812f7ec7 100644 --- a/docs/narr/router.rst +++ b/docs/narr/router.rst @@ -115,9 +115,9 @@ processing? any :term:`response callback` functions attached via :meth:`~pyramid.request.Request.add_response_callback`. A :class:`~pyramid.events.NewResponse` :term:`event` is then sent to any - subscribers. The response object's ``app_iter``, ``status``, and - ``headerlist`` attributes are then used to generate a WSGI response. The - response is sent back to the upstream WSGI server. + subscribers. The response object's ``__call__`` method is then used to + generate a WSGI response. The response is sent back to the upstream WSGI + server. #. :app:`Pyramid` will attempt to execute any :term:`finished callback` functions attached via diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 990828f80..e3d0a37e5 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -230,29 +230,19 @@ implements the :term:`Response` interface is to return a def view(request): return Response('OK') -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`). +:app:`Pyramid` provides a range of different "exception" classes which +inherit from :class:`pyramid.response.Response`. For example, an instance of +the class :class:`pyramid.httpexceptions.HTTPFound` is also a valid response +object because it inherits from :class:`~pyramid.response.Response`. For +examples, see :ref:`http_exceptions` and ref:`http_redirect`. + +You can also return objects from view callables that aren't instances of (or +instances of classes which are subclasses of) +:class:`pyramid.response.Response` in various circumstances. This can be +helpful when writing tests and when attempting to share code between view +callables. See :ref:`renderers_chapter` for the common way to allow for +this. A much less common way to allow for view callables to return +non-Response objects is documented in :ref:`using_iresponse`. .. index:: single: view exceptions diff --git a/docs/narr/webob.rst b/docs/narr/webob.rst index 70ab5eea8..0ff8e1de7 100644 --- a/docs/narr/webob.rst +++ b/docs/narr/webob.rst @@ -10,15 +10,15 @@ Request and Response Objects .. note:: This chapter is adapted from a portion of the :term:`WebOb` documentation, originally written by Ian Bicking. -:app:`Pyramid` uses the :term:`WebOb` package to supply +:app:`Pyramid` uses the :term:`WebOb` package as a basis for its :term:`request` and :term:`response` object implementations. The -:term:`request` object that is passed to a :app:`Pyramid` -:term:`view` is an instance of the :class:`pyramid.request.Request` -class, which is a subclass of :class:`webob.Request`. The -:term:`response` returned from a :app:`Pyramid` :term:`view` -:term:`renderer` is an instance of the :mod:`webob.Response` class. -Users can also return an instance of :mod:`webob.Response` directly -from a view as necessary. +:term:`request` object that is passed to a :app:`Pyramid` :term:`view` is an +instance of the :class:`pyramid.request.Request` class, which is a subclass +of :class:`webob.Request`. The :term:`response` returned from a +:app:`Pyramid` :term:`view` :term:`renderer` is an instance of the +:mod:`pyramid.response.Response` class, which is a subclass of the +:class:`webob.Response` class. Users can also return an instance of +:class:`pyramid.response.Response` directly from a view as necessary. WebOb is a project separate from :app:`Pyramid` with a separate set of authors and a fully separate `set of documentation @@ -26,16 +26,15 @@ authors and a fully separate `set of documentation standard WebOb request, which is documented in the :ref:`request_module` API documentation. -WebOb provides objects for HTTP requests and responses. Specifically -it does this by wrapping the `WSGI `_ request -environment and response status/headers/app_iter (body). +WebOb provides objects for HTTP requests and responses. Specifically it does +this by wrapping the `WSGI `_ request environment and +response status, header list, and app_iter (body) values. -WebOb request and response objects provide many conveniences for -parsing WSGI requests and forming WSGI responses. WebOb is a nice way -to represent "raw" WSGI requests and responses; however, we won't -cover that use case in this document, as users of :app:`Pyramid` -don't typically need to use the WSGI-related features of WebOb -directly. The `reference documentation +WebOb request and response objects provide many conveniences for parsing WSGI +requests and forming WSGI responses. WebOb is a nice way to represent "raw" +WSGI requests and responses; however, we won't cover that use case in this +document, as users of :app:`Pyramid` don't typically need to use the +WSGI-related features of WebOb directly. The `reference documentation `_ shows many examples of creating requests and using response objects in this manner, however. @@ -170,9 +169,9 @@ of the request. I'll show various values for an example URL Methods +++++++ -There are `several methods -`_ but -only a few you'll use often: +There are methods of request objects documented in +:class:`pyramid.request.Request` but you'll find that you won't use very many +of them. Here are a couple that might be useful: ``Request.blank(base_url)``: Creates a new request with blank information, based at the given @@ -183,9 +182,9 @@ only a few you'll use often: subrequests). ``req.get_response(wsgi_application)``: - This method calls the given WSGI application with this request, - and returns a `Response`_ object. You can also use this for - subrequests, or testing. + This method calls the given WSGI application with this request, and + returns a :class:`pyramid.response.Response` object. You can also use + this for subrequests, or testing. .. index:: single: request (and unicode) @@ -259,8 +258,10 @@ Response ~~~~~~~~ The :app:`Pyramid` response object can be imported as -:class:`pyramid.response.Response`. This import location is merely a facade -for its original location: ``webob.Response``. +:class:`pyramid.response.Response`. This class is a subclass of the +``webob.Response`` class. The subclass does not add or change any +functionality, so the WebOb Response documentation will be completely +relevant for this class as well. A response object has three fundamental parts: @@ -283,8 +284,8 @@ A response object has three fundamental parts: ``response.body_file`` (a file-like object; writing to it appends to ``app_iter``). -Everything else in the object derives from this underlying state. -Here's the highlights: +Everything else in the object typically derives from this underlying state. +Here are some highlights: ``response.content_type`` The content type *not* including the ``charset`` parameter. @@ -359,11 +360,12 @@ Exception Responses +++++++++++++++++++ To facilitate error responses like ``404 Not Found``, the module -:mod:`webob.exc` contains classes for each kind of error response. These -include boring, but appropriate error bodies. The exceptions exposed by this -module, when used under :app:`Pyramid`, should be imported from the -:mod:`pyramid.httpexceptions` module. This import location contains -subclasses and replacements that mirror those in the original ``webob.exc``. +:mod:`pyramid.httpexceptions` contains classes for each kind of error +response. These include boring, but appropriate error bodies. The +exceptions exposed by this module, when used under :app:`Pyramid`, should be +imported from the :mod:`pyramid.httpexceptions` module. This import location +contains subclasses and replacements that mirror those in the ``webob.exc`` +module. Each class is named ``pyramid.httpexceptions.HTTP*``, where ``*`` is the reason for the error. For instance, diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index b6c083bbf..92a3da09c 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -84,10 +84,12 @@ No renderer is necessary when a view returns a response object. The ``view_wiki`` view callable always redirects to the URL of a Page resource named "FrontPage". To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface). The :func:`pyramid.url.resource_url` -API. :func:`pyramid.url.resource_url` constructs a URL to the ``FrontPage`` -page resource (e.g. ``http://localhost:6543/FrontPage``), and uses it as the -"location" of the HTTPFound response, forming an HTTP redirect. +the :class:`pyramid.interfaces.IResponse` interface like +:class:`pyramid.response.Response` does). The +:func:`pyramid.url.resource_url` API. :func:`pyramid.url.resource_url` +constructs a URL to the ``FrontPage`` page resource +(e.g. ``http://localhost:6543/FrontPage``), and uses it as the "location" of +the HTTPFound response, forming an HTTP redirect. The ``view_page`` view function ------------------------------- diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index 832f90b92..43cbc3483 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -91,7 +91,8 @@ path to our "FrontPage". The ``view_wiki`` function returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement -the WebOb :term:`response` interface), It will use the +the :class:`pyramid.interfaces.IResponse` interface like +:class:`pyramid.response.Response` does), It will use the :func:`pyramid.url.route_url` API to construct a URL to the ``FrontPage`` page (e.g. ``http://localhost:6543/FrontPage``), and will use it as the "location" of the HTTPFound response, forming an HTTP redirect. diff --git a/pyramid/config.py b/pyramid/config.py index 91ba414b3..fab75f56d 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -19,6 +19,7 @@ from zope.interface import implementedBy from zope.interface.interfaces import IInterface from zope.interface import implements from zope.interface import classProvides +from zope.interface import providedBy from pyramid.interfaces import IAuthenticationPolicy from pyramid.interfaces import IAuthorizationPolicy @@ -36,6 +37,7 @@ from pyramid.interfaces import IRendererFactory from pyramid.interfaces import IRendererGlobalsFactory from pyramid.interfaces import IRequest from pyramid.interfaces import IRequestFactory +from pyramid.interfaces import IResponse from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IRoutesMapper @@ -82,7 +84,6 @@ from pyramid.traversal import traversal_path from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver from pyramid.view import render_view_to_response -from pyramid.view import is_response DEFAULT_RENDERERS = ( ('.mak', mako_renderer_factory), @@ -417,7 +418,8 @@ class Configurator(object): def _fix_registry(self): """ Fix up a ZCA component registry that is not a pyramid.registry.Registry by adding analogues of ``has_listeners``, - and ``notify`` through monkey-patching.""" + ``notify``, ``queryAdapterOrSelf``, and ``registerSelfAdapter`` + through monkey-patching.""" _registry = self.registry @@ -429,6 +431,24 @@ class Configurator(object): if not hasattr(_registry, 'has_listeners'): _registry.has_listeners = True + if not hasattr(_registry, 'queryAdapterOrSelf'): + def queryAdapterOrSelf(object, interface, name=u'', default=None): + provides = providedBy(object) + if not interface in provides: + return _registry.queryAdapter(object, interface, name=name, + default=default) + return object + _registry.queryAdapterOrSelf = queryAdapterOrSelf + + if not hasattr(_registry, 'registerSelfAdapter'): + def registerSelfAdapter(required=None, provided=None, + name=u'', info=u'', event=True): + return _registry.registerAdapter(lambda x: x, + required=required, + provided=provided, name=name, + info=info, event=event) + _registry.registerSelfAdapter = registerSelfAdapter + def _make_context(self, autocommit=False): context = PyramidConfigurationMachine() registerCommonDirectives(context) @@ -697,6 +717,9 @@ class Configurator(object): self._fix_registry() self._set_settings(settings) self._set_root_factory(root_factory) + # cope with WebOb response objects that aren't decorated with IResponse + from webob import Response as WebobResponse + registry.registerSelfAdapter((WebobResponse,), IResponse) debug_logger = self.maybe_dotted(debug_logger) if debug_logger is None: debug_logger = make_stream_logger('pyramid.debug', sys.stderr) @@ -2942,22 +2965,24 @@ class ViewDeriver(object): def _rendered_view(context, request): renderer = static_renderer - response = wrapped_view(context, request) - if not is_response(response): + result = wrapped_view(context, request) + registry = self.kw['registry'] + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: attrs = getattr(request, '__dict__', {}) if 'override_renderer' in attrs: # renderer overridden by newrequest event or other renderer_name = attrs.pop('override_renderer') renderer = RendererHelper(name=renderer_name, package=self.kw.get('package'), - registry = self.kw['registry']) + registry = registry) if '__view__' in attrs: view_inst = attrs.pop('__view__') else: view_inst = getattr(wrapped_view, '__original_view__', wrapped_view) - return renderer.render_view(request, response, view_inst, - context) + response = renderer.render_view(request, result, view_inst, + context) return response return _rendered_view diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index b8ff2d4c9..dea7174fb 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -233,14 +233,6 @@ class IMultiDict(Interface): # docs-only interface dictionary. This is similar to the kind of dictionary often used to represent the variables in a web request. """ -class IResponder(Interface): - """ Adapter from IResponse to an IResponder. See :ref:`using_iresponder` - for usage details. New in Pyramid 1.1. - """ - def __call__(self, request, start_response): - """ Call the WSGI ``start_response`` callable passed as - ``start_response`` and return an ``app_iter``.""" - # internal interfaces class IRequest(Interface): diff --git a/pyramid/registry.py b/pyramid/registry.py index 37e230dc3..26f84d493 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -1,4 +1,5 @@ from zope.component.registry import Components +from zope.interface import providedBy from pyramid.interfaces import ISettings @@ -28,6 +29,24 @@ class Registry(Components, dict): self.has_listeners = True return result + def registerSelfAdapter(self, required=None, provided=None, name=u'', + info=u'', event=True): + # registerAdapter analogue which always returns the object itself + # when required is matched + return self.registerAdapter(lambda x: x, required=required, + provided=provided, name=name, + info=info, event=event) + + def queryAdapterOrSelf(self, object, interface, name=u'', default=None): + # queryAdapter analogue which returns the object if it implements + # the interface, otherwise it will return an adaptation to the + # interface + provides = providedBy(object) + if not interface in provides: + return self.queryAdapter(object, interface, name=name, + default=default) + return object + def registerHandler(self, *arg, **kw): result = Components.registerHandler(self, *arg, **kw) self.has_listeners = True diff --git a/pyramid/renderers.py b/pyramid/renderers.py index a6dce9b3a..6865067dd 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -316,9 +316,7 @@ class RendererHelper(object): 'context':context, 'request':request } - return self.render_to_response(response, system, - request=request) - + return self.render_to_response(response, system, request=request) def render(self, value, system_values, request=None): renderer = self.renderer diff --git a/pyramid/request.py b/pyramid/request.py index d387a0b2f..b69440ac6 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -5,12 +5,14 @@ from zope.interface.interface import InterfaceClass from webob import BaseRequest from pyramid.interfaces import IRequest +from pyramid.interfaces import IResponse from pyramid.interfaces import ISessionFactory from pyramid.interfaces import IResponseFactory from pyramid.exceptions import ConfigurationError from pyramid.decorator import reify from pyramid.response import Response +from pyramid.threadlocal import get_current_registry from pyramid.url import resource_url from pyramid.url import route_url from pyramid.url import static_url diff --git a/pyramid/response.py b/pyramid/response.py index 1d2ef296f..68496e386 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -4,4 +4,4 @@ from pyramid.interfaces import IResponse class Response(_Response): implements(IResponse) - + diff --git a/pyramid/router.py b/pyramid/router.py index 4d2750efb..48640b39d 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -1,5 +1,3 @@ -import warnings - from zope.interface import implements from zope.interface import providedBy @@ -14,7 +12,7 @@ from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import ITraverser from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier -from pyramid.interfaces import IResponder +from pyramid.interfaces import IResponse from pyramid.events import ContextFound from pyramid.events import NewRequest @@ -61,7 +59,6 @@ class Router(object): logger = self.logger manager = self.threadlocal_manager request = None - responder = default_responder threadlocals = {'registry':registry, 'request':request} manager.push(threadlocals) @@ -159,7 +156,7 @@ class Router(object): msg = request.path_info raise HTTPNotFound(msg) else: - response = view_callable(context, request) + result = view_callable(context, request) # handle exceptions raised during root finding and view-exec except Exception, why: @@ -181,52 +178,26 @@ class Router(object): # repoze.bfg.message docs-deprecated in Pyramid 1.0 environ['repoze.bfg.message'] = msg - response = view_callable(why, request) + result = view_callable(why, request) # process the response + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: + raise ValueError( + 'Could not convert view return value "%s" into a ' + 'response' % (result,)) has_listeners and registry.notify(NewResponse(request,response)) if request.response_callbacks: request._process_response_callbacks(response) - responder = adapters.queryAdapter(response, IResponder) - if responder is None: - responder = default_responder(response) - finally: if request is not None and request.finished_callbacks: request._process_finished_callbacks() - return responder(request, start_response) + return response(request.environ, start_response) finally: manager.pop() -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 - status = response.status - except AttributeError: - raise ValueError( - '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/session.py b/pyramid/session.py index 5772c80d0..bb3226a1e 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -8,8 +8,6 @@ try: except ImportError: # pragma: no cover import pickle -from webob import Response - import base64 import binascii import hmac @@ -213,17 +211,7 @@ def UnencryptedCookieSessionFactoryConfig( 'Cookie value is too long to store (%s bytes)' % len(cookieval) ) - if hasattr(response, 'set_cookie'): - # ``response`` is a "real" webob response - set_cookie = response.set_cookie - else: - # ``response`` is not a "real" webob response, cope - def set_cookie(*arg, **kw): - tmp_response = Response() - tmp_response.set_cookie(*arg, **kw) - response.headerlist.append( - tmp_response.headerlist[-1]) - set_cookie( + response.set_cookie( self._cookie_name, value=cookieval, max_age = self._cookie_max_age, diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 703a2577c..49bfab396 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -289,27 +289,58 @@ class ConfiguratorTests(unittest.TestCase): result = config.absolute_asset_spec('templates') self.assertEqual(result, 'pyramid.tests:templates') - def test_setup_registry_fixed(self): - class DummyRegistry(object): - def subscribers(self, events, name): - self.events = events - return events - def registerUtility(self, *arg, **kw): - pass + def test__fix_registry_has_listeners(self): reg = DummyRegistry() config = self._makeOne(reg) - config.add_view = lambda *arg, **kw: False - config.setup_registry() + config._fix_registry() self.assertEqual(reg.has_listeners, True) + + def test__fix_registry_notify(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() self.assertEqual(reg.notify(1), None) self.assertEqual(reg.events, (1,)) + def test__fix_registry_queryAdapterOrSelf(self): + from zope.interface import Interface + class IFoo(Interface): + pass + class Foo(object): + implements(IFoo) + class Bar(object): + pass + adaptation = () + foo = Foo() + bar = Bar() + reg = DummyRegistry(adaptation) + config = self._makeOne(reg) + config._fix_registry() + self.assertTrue(reg.queryAdapterOrSelf(foo, IFoo) is foo) + self.assertTrue(reg.queryAdapterOrSelf(bar, IFoo) is adaptation) + + def test__fix_registry_registerSelfAdapter(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + reg.registerSelfAdapter('required', 'provided', name='abc') + self.assertEqual(len(reg.adapters), 1) + args, kw = reg.adapters[0] + self.assertEqual(args[0]('abc'), 'abc') + self.assertEqual(kw, + {'info': u'', 'provided': 'provided', + 'required': 'required', 'name': 'abc', 'event': True}) + + def test_setup_registry_calls_fix_registry(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config.add_view = lambda *arg, **kw: False + config.setup_registry() + self.assertEqual(reg.has_listeners, True) + def test_setup_registry_registers_default_exceptionresponse_view(self): from pyramid.interfaces import IExceptionResponse from pyramid.view import default_exceptionresponse_view - class DummyRegistry(object): - def registerUtility(self, *arg, **kw): - pass reg = DummyRegistry() config = self._makeOne(reg) views = [] @@ -318,6 +349,15 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(views[0], ((default_exceptionresponse_view,), {'context':IExceptionResponse})) + def test_setup_registry_registers_default_webob_iresponse_adapter(self): + from webob import Response + from pyramid.interfaces import IResponse + config = self._makeOne() + config.setup_registry() + response = Response() + self.assertTrue( + config.registry.queryAdapter(response, IResponse) is response) + def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): from zope.interface import implementedBy from pyramid.interfaces import IRequest @@ -5134,3 +5174,17 @@ def dummy_extend(config, discrim): def dummy_extend2(config, discrim): config.action(discrim, None, config.registry) +class DummyRegistry(object): + def __init__(self, adaptation=None): + self.utilities = [] + self.adapters = [] + self.adaptation = adaptation + def subscribers(self, events, name): + self.events = events + return events + def registerUtility(self, *arg, **kw): + self.utilities.append((arg, kw)) + def registerAdapter(self, *arg, **kw): + self.adapters.append((arg, kw)) + def queryAdapter(self, *arg, **kw): + return self.adaptation diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 765a26751..5fd2cf01e 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -2,19 +2,6 @@ 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() @@ -249,7 +236,7 @@ class TestRouter(unittest.TestCase): self.assertTrue("view_name: ''" in message) self.assertTrue("subpath: []" in message) - def test_call_view_returns_nonresponse(self): + def test_call_view_returns_non_iresponse(self): from pyramid.interfaces import IViewClassifier context = DummyContext() self._registerTraverserFactory(context) @@ -260,6 +247,24 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(ValueError, router, environ, start_response) + def test_call_view_returns_adapted_response(self): + from pyramid.response import Response + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponse + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron() + view = DummyView('abc') + self._registerView(view, '', IViewClassifier, None, None) + router = self._makeOne() + start_response = DummyStartResponse() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + app_iter = router(environ, start_response) + self.assertEqual(app_iter, ['abc']) + self.assertEqual(start_response.status, '200 OK') + def test_call_view_registered_nonspecific_default_path(self): from pyramid.interfaces import IViewClassifier context = DummyContext() @@ -464,37 +469,6 @@ class TestRouter(unittest.TestCase): exc_raised(NotImplementedError, router, environ, start_response) self.assertEqual(environ['called_back'], True) - def test_call_with_overridden_iresponder_factory(self): - from zope.interface import Interface - from zope.interface import directlyProvides - from pyramid.interfaces import IRequest - from pyramid.interfaces import IViewClassifier - from pyramid.interfaces import IResponder - context = DummyContext() - class IFoo(Interface): - pass - directlyProvides(context, IFoo) - self._registerTraverserFactory(context, subpath=['']) - class DummyResponder(object): - def __init__(self, response): - self.response = response - def __call__(self, request, start_response): - self.response.responder_used = True - return '123' - self.registry.registerAdapter(DummyResponder, (None,), - IResponder, name='') - response = DummyResponse('200 OK') - directlyProvides(response, IFoo) - def view(context, request): - return response - environ = self._makeEnviron() - self._registerView(view, '', IViewClassifier, IRequest, Interface) - router = self._makeOne() - start_response = DummyStartResponse() - result = router(environ, start_response) - self.assertTrue(response.responder_used) - self.assertEqual(result, '123') - def test_call_request_factory_raises(self): # making sure finally doesnt barf when a request cannot be created environ = self._makeEnviron() @@ -875,7 +849,7 @@ class TestRouter(unittest.TestCase): result = router(environ, start_response) self.assertEqual(result, ["Hello, world"]) - def test_exception_view_returns_non_response(self): + def test_exception_view_returns_non_iresponse(self): from pyramid.interfaces import IRequest from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IExceptionViewClassifier @@ -1072,52 +1046,6 @@ 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 @@ -1147,14 +1075,16 @@ class DummyStartResponse: self.status = status self.headers = headers -class DummyResponseWithoutCall: +from pyramid.interfaces import IResponse +from zope.interface import implements + +class DummyResponse(object): + implements(IResponse) headerlist = () app_iter = () + environ = None def __init__(self, status='200 OK'): self.status = status - -class DummyResponse(DummyResponseWithoutCall): - environ = None def __call__(self, environ, start_response): self.environ = environ diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 17a437af6..5c6454a38 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -90,16 +90,8 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertEqual(session._set_cookie(response), True) self.assertEqual(response.headerlist[-1][0], 'Set-Cookie') - def test__set_cookie_other_kind_of_response(self): - request = testing.DummyRequest() - request.exception = None - session = self._makeOne(request) - session['abc'] = 'x' - response = DummyResponse() - self.assertEqual(session._set_cookie(response), True) - self.assertEqual(len(response.headerlist), 1) - def test__set_cookie_options(self): + from pyramid.response import Response request = testing.DummyRequest() request.exception = None session = self._makeOne(request, @@ -110,10 +102,9 @@ class TestUnencryptedCookieSession(unittest.TestCase): cookie_httponly = True, ) session['abc'] = 'x' - response = DummyResponse() + response = Response() self.assertEqual(session._set_cookie(response), True) - self.assertEqual(len(response.headerlist), 1) - cookieval= response.headerlist[0][1] + cookieval= response.headerlist[-1][1] val, domain, path, secure, httponly = [x.strip() for x in cookieval.split(';')] self.assertTrue(val.startswith('abc=')) diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index b1d48b98b..0ea9a11a0 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -120,6 +120,19 @@ class RenderViewToIterableTests(BaseTest, unittest.TestCase): secure=True) self.assertEqual(iterable, ()) + def test_call_view_returns_iresponse_adaptable(self): + from pyramid.response import Response + request = self._makeRequest() + context = self._makeContext() + view = make_view('123') + self._registerView(request.registry, view, 'registered') + def str_response(s): + return Response(s) + request.registry.registerAdapter(str_response, (str,), IResponse) + iterable = self._callFUT(context, request, name='registered', + secure=True) + self.assertEqual(iterable, ['123']) + def test_call_view_registered_insecure_no_call_permissive(self): context = self._makeContext() request = self._makeRequest() @@ -536,9 +549,15 @@ def make_view(response): class DummyRequest: exception = None -class DummyResponse: - status = '200 OK' +from pyramid.interfaces import IResponse +from zope.interface import implements + +class DummyResponse(object): + implements(IResponse) headerlist = () + app_iter = () + status = '200 OK' + environ = None def __init__(self, body=None): if body is None: self.app_iter = () diff --git a/pyramid/view.py b/pyramid/view.py index 9a4be7580..a89df8859 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -4,6 +4,7 @@ import venusian from zope.interface import providedBy from zope.deprecation import deprecated +from pyramid.interfaces import IResponse from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier @@ -100,6 +101,11 @@ def render_view_to_iterable(context, request, name='', secure=True): response = render_view_to_response(context, request, name, secure) if response is None: return None + try: + reg = request.registry + except AttributeError: + reg = get_current_registry() + response = reg.queryAdapterOrSelf(response, IResponse) return response.app_iter def render_view(context, request, name='', secure=True): -- cgit v1.2.3 From c209f8013189745983c628b1ddf3438858600a15 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 06:25:58 -0400 Subject: name argument makes no sense here --- pyramid/config.py | 4 ++-- pyramid/registry.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index fab75f56d..dbeb0788c 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -432,10 +432,10 @@ class Configurator(object): _registry.has_listeners = True if not hasattr(_registry, 'queryAdapterOrSelf'): - def queryAdapterOrSelf(object, interface, name=u'', default=None): + def queryAdapterOrSelf(object, interface, default=None): provides = providedBy(object) if not interface in provides: - return _registry.queryAdapter(object, interface, name=name, + return _registry.queryAdapter(object, interface, default=default) return object _registry.queryAdapterOrSelf = queryAdapterOrSelf diff --git a/pyramid/registry.py b/pyramid/registry.py index 26f84d493..4c6262ce1 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -37,14 +37,13 @@ class Registry(Components, dict): provided=provided, name=name, info=info, event=event) - def queryAdapterOrSelf(self, object, interface, name=u'', default=None): + def queryAdapterOrSelf(self, object, interface, default=None): # queryAdapter analogue which returns the object if it implements # the interface, otherwise it will return an adaptation to the # interface provides = providedBy(object) if not interface in provides: - return self.queryAdapter(object, interface, name=name, - default=default) + return self.queryAdapter(object, interface, default=default) return object def registerHandler(self, *arg, **kw): -- cgit v1.2.3 From 51fb07becd49a187da84542981f38f820efe8f8b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 23:04:45 -0400 Subject: flesh out IResponse interface --- pyramid/interfaces.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 204 insertions(+), 5 deletions(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index dea7174fb..fee8d549d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -47,13 +47,212 @@ class IApplicationCreated(Interface): IWSGIApplicationCreatedEvent = IApplicationCreated # b /c class IResponse(Interface): - status = Attribute('WSGI status code of response') - headerlist = Attribute('List of response headers') - app_iter = Attribute('Iterable representing the response body') + """ Represents a WSGI response using the WebOb response interface. Some + attribute and method documentation of this interface references `RFC 2616 + `_. + + This interface is most famously implemented by + :class:`pyramid.response.Response` and the HTTP exception classes in + :mod:`pyramid.httpexceptions`.""" + + RequestClass = Attribute( + """ Alias for :class:`pyramid.request.Request` """) def __call__(environ, start_response): - """ WSGI call interface, should call the start_response callback - and should return an iterable """ + """ :term:`WSGI` call interface, should call the start_response + callback and should return an iterable""" + + accept_ranges = Attribute( + """Gets and sets and deletes the Accept-Ranges header. For more + information on Accept-Ranges see RFC 2616, section 14.5""") + + age = Attribute( + """Gets and sets and deletes the Age header. Converts using int. + For more information on Age see RFC 2616, section 14.6.""") + + allow = Attribute( + """Gets and sets and deletes the Allow header. Converts using + list. For more information on Allow see RFC 2616, Section 14.7.""") + + app_iter = Attribute( + """Returns the app_iter of the response. + + If body was set, this will create an app_iter from that body + (a single-item list)""") + + def app_iter_range(start, stop): + """ Return a new app_iter built from the response app_iter that + serves up only the given start:stop range. """ + + body = Attribute( + """The body of the response, as a str. This will read in the entire + app_iter if necessary.""") + + body_file = Attribute( + """A file-like object that can be used to write to the body. If you + passed in a list app_iter, that app_iter will be modified by writes.""") + + cache_control = Attribute( + """Get/set/modify the Cache-Control header (RFC 2616 section 14.9)""") + + cache_expires = Attribute( + """ Get/set the Cache-Control and Expires headers. This sets the + response to expire in the number of seconds passed when set. """) + + charset = Attribute( + """Get/set the charset (in the Content-Type)""") + + def conditional_response_app(environ, start_response): + """ Like the normal __call__ interface, but checks conditional + headers: + + - If-Modified-Since (304 Not Modified; only on GET, HEAD) + + - If-None-Match (304 Not Modified; only on GET, HEAD) + + - Range (406 Partial Content; only on GET, HEAD)""" + + content_disposition = Attribute( + """Gets and sets and deletes the Content-Disposition header. + For more information on Content-Disposition see RFC 2616 section + 19.5.1.""") + + content_encoding = Attribute( + """Gets and sets and deletes the Content-Encoding header. For more + information about Content-Encoding see RFC 2616 section 14.11.""") + + content_language = Attribute( + """Gets and sets and deletes the Content-Language header. Converts + using list. For more information about Content-Language see RFC 2616 + section 14.12.""") + + content_length = Attribute( + """Gets and sets and deletes the Content-Length header. For more + information on Content-Length see RFC 2616 section 14.17. + Converts using int. """) + + content_location = Attribute( + """Gets and sets and deletes the Content-Location header. For more + information on Content-Location see RFC 2616 section 14.14.""") + + content_md5 = Attribute( + """Gets and sets and deletes the Content-MD5 header. For more + information on Content-MD5 see RFC 2616 section 14.14.""") + + content_range = Attribute( + """Gets and sets and deletes the Content-Range header. For more + information on Content-Range see section 14.16. Converts using + ContentRange object.""") + + content_type = Attribute( + """Get/set the Content-Type header (or None), without the charset + or any parameters. If you include parameters (or ; at all) when + setting the content_type, any existing parameters will be deleted; + otherwise they will be preserved.""") + + content_type_params = Attribute( + """A dictionary of all the parameters in the content type. This is + not a view, set to change, modifications of the dict would not + be applied otherwise.""") + + def copy(): + """ Makes a copy of the response and returns the copy. """ + + date = Attribute( + """Gets and sets and deletes the Date header. For more information on + Date see RFC 2616 section 14.18. Converts using HTTP date.""") + + def delete_cookie(key, path='/', domain=None): + """ Delete a cookie from the client. Note that path and domain must + match how the cookie was originally set. This sets the cookie to the + empty string, and max_age=0 so that it should expire immediately. """ + + def encode_content(encoding='gzip', lazy=False): + """ Encode the content with the given encoding (only gzip and + identity are supported).""" + + environ = Attribute( + """Get/set the request environ associated with this response, + if any.""") + + etag = Attribute( + """ Gets and sets and deletes the ETag header. For more information + on ETag see RFC 2616 section 14.19. Converts using Entity tag.""") + + expires = Attribute( + """ Gets and sets and deletes the Expires header. For more + information on Expires see RFC 2616 section 14.21. Converts using + HTTP date.""") + + headerlist = Attribute( + """ The list of response headers. """) + + headers = Attribute( + """ The headers in a dictionary-like object """) + + last_modified = Attribute( + """ Gets and sets and deletes the Last-Modified header. For more + information on Last-Modified see RFC 2616 section 14.29. Converts + using HTTP date.""") + + location = Attribute( + """ Gets and sets and deletes the Location header. For more + information on Location see RFC 2616 section 14.30.""") + + def md5_etag(body=None, set_content_md5=False): + """ Generate an etag for the response object using an MD5 hash of the + body (the body parameter, or self.body if not given). Sets self.etag. + If set_content_md5 is True sets self.content_md5 as well """ + + def merge_cookies(resp): + """ Merge the cookies that were set on this response with the given + resp object (which can be any WSGI application). If the resp is a + webob.Response object, then the other object will be modified + in-place. """ + + pragma = Attribute( + """ Gets and sets and deletes the Pragma header. For more information + on Pragma see RFC 2616 section 14.32. """) + + request = Attribute( + """ Return the request associated with this response if any. """) + + retry_after = Attribute( + """ Gets and sets and deletes the Retry-After header. For more + information on Retry-After see RFC 2616 section 14.37. Converts + using HTTP date or delta seconds.""") + + server = Attribute( + """ Gets and sets and deletes the Server header. For more information + on Server see RFC216 section 14.38. """) + + def set_cookie(key, value='', max_age=None, path='/', domain=None, + secure=False, httponly=False, comment=None, expires=None, + overwrite=False): + """ Set (add) a cookie for the response """ + + status = Attribute( + """ The status string. """) + + status_int = Attribute( + """ The status as an integer """) + + unicode_body = Attribute( + """ Get/set the unicode value of the body (using the charset of + the Content-Type)""") + + def unset_cookie(key, strict=True): + """ Unset a cookie with the given name (remove it from the + response).""" + + vary = Attribute( + """Gets and sets and deletes the Vary header. For more information + on Vary see section 14.44. Converts using list.""") + + www_authenticate = Attribute( + """ Gets and sets and deletes the WWW-Authenticate header. For more + information on WWW-Authenticate see RFC 2616 section 14.47. Converts + using 'parse_auth' and 'serialize_auth'. """) class IException(Interface): # not an API """ An interface representing a generic exception """ -- cgit v1.2.3 From 8b1057beec5bf237e162ac3d3ab648cc7c22b392 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 23:05:03 -0400 Subject: render nicer docs --- pyramid/httpexceptions.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index a692380f8..f3df574a0 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -8,8 +8,8 @@ single HTTP status code. Each class is a subclass of the object. Each exception class has a status code according to `RFC 2068 -`: codes with 100-300 are not really -errors; 400's are client errors, and 500's are server errors. +`_: codes with 100-300 are not really +errors; 400s are client errors, and 500s are server errors. Exception HTTPException @@ -131,7 +131,7 @@ def _no_escape(value): return value class HTTPException(Exception): # bw compat - pass + """ Base class for all :term:`exception response` objects.""" class WSGIHTTPException(Response, HTTPException): implements(IExceptionResponse) @@ -271,16 +271,15 @@ ${body}''') class HTTPError(WSGIHTTPException): """ - base class for status codes in the 400's and 500's + base class for exceptions with status codes in the 400s and 500s This is an exception which indicates that an error has occurred, - and that any work in progress should not be committed. These are - typically results in the 400's and 500's. + and that any work in progress should not be committed. """ class HTTPRedirection(WSGIHTTPException): """ - base class for 300's status code (redirections) + base class for exceptions with status codes in the 300s (redirections) This is an abstract base class for 3xx redirection. It indicates that further action needs to be taken by the user agent in order @@ -290,7 +289,8 @@ class HTTPRedirection(WSGIHTTPException): class HTTPOk(WSGIHTTPException): """ - Base class for the 200's status code (successful responses) + Base class for exceptions with status codes in the 200s (successful + responses) code: 200, title: OK """ @@ -522,7 +522,7 @@ class HTTPTemporaryRedirect(_HTTPMove): class HTTPClientError(HTTPError): """ - base class for the 400's, where the client is in error + base class for the 400s, where the client is in error This is an error condition in which the client is presumed to be in-error. This is an expected problem, and thus is not considered @@ -882,12 +882,10 @@ class HTTPFailedDependency(HTTPClientError): class HTTPServerError(HTTPError): """ - base class for the 500's, where the server is in-error + base class for the 500s, where the server is in-error This is an error condition in which the server is presumed to be - in-error. 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' + in-error. Unless specialized, this is a '500 Internal Server Error'. """ code = 500 title = 'Internal Server Error' -- cgit v1.2.3 From 3f4f67e76c2e1338377babd983e4071f52235132 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 13 Jun 2011 23:05:38 -0400 Subject: garden --- TODO.txt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/TODO.txt b/TODO.txt index ca2433d3c..9d9bbf1eb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,15 +6,12 @@ Must-Have - To subclass or not subclass http exceptions. -- Flesh out IResponse interface. Attributes Used internally: unicode_body / - body / content_type / charset / cache_expires / headers/ - default_content_type / set_cookie / headerlist / app_iter / status / - __call__. - - Deprecate view.is_response? - Move is_response to response.py? +- Create add_response_adapter Configurator API? + - Make sure registering IResponse adapter for webob.Response doesn't make it impossible to register an IResponse adapter for an interface that a webob.Response happens to implement. -- cgit v1.2.3 From 1a6fc7062f803b9f15b7677db9a9257a4f00bfcb Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 02:36:07 -0400 Subject: - Added new add_response_adapter method to Configurator. - Fix Configurator docstring wrt exception responses. - Speed up registry.queryAdapterOrSelf --- CHANGES.txt | 6 ++++-- docs/api/config.rst | 2 ++ docs/narr/hooks.rst | 13 ++++--------- pyramid/config.py | 42 ++++++++++++++++++++++++++++++------------ pyramid/registry.py | 3 +-- pyramid/router.py | 2 +- pyramid/tests/test_config.py | 28 ++++++++++++++++++++++++++++ 7 files changed, 70 insertions(+), 26 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5e8df1a0b..c7ca3794d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -130,8 +130,10 @@ Features - It is now possible to return an arbitrary object from a Pyramid view callable even if a renderer is not used, as long as a suitable adapter to ``pyramid.interfaces.IResponse`` is registered for the type of the returned - object. See the section in the Hooks chapter of the documentation entitled - "Changing How Pyramid Treats View Responses". + object by using the new + ``pyramid.config.Configurator.add_response_adapter`` API. See the section + in the Hooks chapter of the documentation entitled "Changing How Pyramid + Treats View Responses". - The Pyramid router will now, by default, call the ``__call__`` method of WebOb response objects when returning a WSGI response. This means that, diff --git a/docs/api/config.rst b/docs/api/config.rst index 2b9d7bcef..274ee0292 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -40,6 +40,8 @@ .. automethod:: add_renderer(name, factory) + .. automethod:: add_response_adapter + .. automethod:: add_route .. automethod:: add_static_view(name, path, cache_max_age=3600, permission='__no_permission_required__') diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 0db8ce5e0..8e5b93ed4 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -532,7 +532,7 @@ Changing How Pyramid Treats View Responses It is possible to control how Pyramid treats the result of calling a view callable on a per-type basis by using a hook involving -:class:`pyramid.interfaces.IResponse`. +:method:`pyramid.config.Configurator.add_response_adapter`. .. note:: This feature is new as of Pyramid 1.1. @@ -559,7 +559,6 @@ Response: .. code-block:: python :linenos: - from pyramid.interfaces import IResponse from pyramid.response import Response def string_response_adapter(s): @@ -568,8 +567,7 @@ Response: # config is an instance of pyramid.config.Configurator - config.registry.registerAdapter(string_response_adapter, (str,), - IResponse) + config.add_response_adapter(string_response_adapter, str) Likewise, if you want to be able to return a simplified kind of response object from view callables, you can use the IResponse hook to register an @@ -578,7 +576,6 @@ adapter to the more complex IResponse interface: .. code-block:: python :linenos: - from pyramid.interfaces import IResponse from pyramid.response import Response class SimpleResponse(object): @@ -591,14 +588,12 @@ adapter to the more complex IResponse interface: # config is an instance of pyramid.config.Configurator - config.registry.registerAdapter(simple_response_adapter, - (SimpleResponse,), - IResponse) + config.add_response_adapter(simple_response_adapter, SimpleResponse) If you want to implement your own Response object instead of using the :class:`pyramid.response.Response` object in any capacity at all, you'll have to make sure the object implements every attribute and method outlined in -:class:`pyramid.interfaces.IResponse` *and* you'll have to ensure that it's +:class:`pyramid.interfaces.IResponse` and you'll have to ensure that it's marked up with ``zope.interface.implements(IResponse)``: from pyramid.interfaces import IResponse diff --git a/pyramid/config.py b/pyramid/config.py index dbeb0788c..70b5cd639 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -19,7 +19,6 @@ from zope.interface import implementedBy from zope.interface.interfaces import IInterface from zope.interface import implements from zope.interface import classProvides -from zope.interface import providedBy from pyramid.interfaces import IAuthenticationPolicy from pyramid.interfaces import IAuthorizationPolicy @@ -260,15 +259,11 @@ class Configurator(object): If ``exceptionresponse_view`` is passed, it must be a :term:`view callable` or ``None``. If it is a view callable, it will be used as an - exception view callable when an :term:`exception response` is raised (any - object that implements the :class:`pyramid.interaces.IExceptionResponse` - interface, such as a :class:`pyramid.response.Response` object or any - ``HTTP`` exception documented in :mod:`pyramid.httpexceptions` as well as - exception responses raised via :func:`pyramid.exceptions.abort`, - :func:`pyramid.exceptions.redirect`). If ``exceptionresponse_view`` is - ``None``, no exception response view will be registered, and all raised - exception responses will be bubbled up to Pyramid's caller. By - default, the ``pyramid.exceptions.default_exceptionresponse_view`` + exception view callable when an :term:`exception response` is raised. If + ``exceptionresponse_view`` is ``None``, no exception response view will + be registered, and all raised exception responses will be bubbled up to + Pyramid's caller. By + default, the ``pyramid.httpexceptions.default_exceptionresponse_view`` function is used as the ``exceptionresponse_view``. This argument is new in Pyramid 1.1. """ @@ -433,8 +428,7 @@ class Configurator(object): if not hasattr(_registry, 'queryAdapterOrSelf'): def queryAdapterOrSelf(object, interface, default=None): - provides = providedBy(object) - if not interface in provides: + if not interface.providedBy(object): return _registry.queryAdapter(object, interface, default=default) return object @@ -893,6 +887,30 @@ class Configurator(object): self.action(None, register) return subscriber + @action_method + def add_response_adapter(self, adapter, type_or_iface): + """ When an object of type (or interface) ``type_or_iface`` is + returned from a view callable, Pyramid will use the adapter + ``adapter`` to convert it into an object which implements the + :class:`pyramid.interfaces.IResponse` interface. If ``adapter`` is + None, an object returned of type (or interface) ``type_or_iface`` + will itself be used as a response object. + + ``adapter`` and ``type_or_interface`` may be Python objects or + strings representing dotted names to importable Python global + objects. + + See :ref:`using_iresponse` for more information.""" + adapter = self.maybe_dotted(adapter) + type_or_iface = self.maybe_dotted(type_or_iface) + def register(): + reg = self.registry + if adapter is None: + reg.registerSelfAdapter((type_or_iface,), IResponse) + else: + reg.registerAdapter(adapter, (type_or_iface,), IResponse) + self.action((IResponse, type_or_iface), register) + def add_settings(self, settings=None, **kw): """Augment the ``settings`` argument passed in to the Configurator constructor with one or more 'setting' key/value pairs. A setting is diff --git a/pyramid/registry.py b/pyramid/registry.py index 4c6262ce1..5db0a11e2 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -41,8 +41,7 @@ class Registry(Components, dict): # queryAdapter analogue which returns the object if it implements # the interface, otherwise it will return an adaptation to the # interface - provides = providedBy(object) - if not interface in provides: + if not interface.providedBy(object): return self.queryAdapter(object, interface, default=default) return object diff --git a/pyramid/router.py b/pyramid/router.py index 48640b39d..8e33332df 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -185,7 +185,7 @@ class Router(object): if response is None: raise ValueError( 'Could not convert view return value "%s" into a ' - 'response' % (result,)) + 'response object' % (result,)) has_listeners and registry.notify(NewResponse(request,response)) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 49bfab396..cc4a037c2 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -2628,6 +2628,34 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), pyramid.tests) + def test_add_response_adapter(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + def __init__(self, other): + self.other = other + config.add_response_adapter(Adapter, str) + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.other, 'foo') + + def test_add_response_adapter_self(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + pass + config.add_response_adapter(None, Adapter) + adapter = Adapter() + result = config.registry.queryAdapter(adapter, IResponse) + self.assertTrue(result is adapter) + + def test_add_response_adapter_dottednames(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + config.add_response_adapter('pyramid.response.Response', + 'types.StringType') + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.body, 'foo') + def test_scan_integration(self): import os from zope.interface import alsoProvides -- cgit v1.2.3 From 92099080859976ce78882de477ddc2c01bc880b2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 03:37:08 -0400 Subject: - New method named ``pyramid.request.Request.is_response``. This method should be used instead of the ``pyramid.view.is_response`` function, which has been deprecated. - Deprecated ``pyramid.view.is_response`` function in favor of (newly-added) ``pyramid.request.Request.is_response`` method. Determining if an object is truly a valid response object now requires access to the registry, which is only easily available as a request attribute. The ``pyramid.view.is_response`` function will still work until it is removed, but now may return an incorrect answer under some (very uncommon) circumstances. --- CHANGES.txt | 12 ++++++++++++ TODO.txt | 12 ------------ pyramid/request.py | 9 +++++++++ pyramid/tests/test_request.py | 28 ++++++++++++++++++++++++++++ pyramid/tests/test_view.py | 8 ++++++++ pyramid/view.py | 13 +++++++------ 6 files changed, 64 insertions(+), 18 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c7ca3794d..ea4bedc7e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -140,6 +140,10 @@ Features among other things, the ``conditional_response`` feature of WebOb response objects will now behave properly. +- New method named ``pyramid.request.Request.is_response``. This method + should be used instead of the ``pyramid.view.is_response`` function, which + has been deprecated. + Bug Fixes --------- @@ -270,6 +274,14 @@ Deprecations 1.0 and before). In a future version, these methods will be removed entirely. +- Deprecated ``pyramid.view.is_response`` function in favor of (newly-added) + ``pyramid.request.Request.is_response`` method. Determining if an object + is truly a valid response object now requires access to the registry, which + is only easily available as a request attribute. The + ``pyramid.view.is_response`` function will still work until it is removed, + but now may return an incorrect answer under some (very uncommon) + circumstances. + Behavior Changes ---------------- diff --git a/TODO.txt b/TODO.txt index 9d9bbf1eb..27ab9ffb5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,18 +6,6 @@ Must-Have - To subclass or not subclass http exceptions. -- Deprecate view.is_response? - -- Move is_response to response.py? - -- Create add_response_adapter Configurator API? - -- Make sure registering IResponse adapter for webob.Response doesn't make it - impossible to register an IResponse adapter for an interface that a - webob.Response happens to implement. - -- Run whatsitdoing tests. - - Docs mention ``exception.args[0]`` as a way to get messages; check that this works. diff --git a/pyramid/request.py b/pyramid/request.py index b69440ac6..06dbddd29 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -323,6 +323,15 @@ class Request(BaseRequest): default=Response) return response_factory() + def is_response(self, ob): + """ Return ``True`` if the object passed as ``ob`` is a valid + response object, ``False`` otherwise.""" + registry = self.registry + adapted = registry.queryAdapterOrSelf(ob, IResponse) + if adapted is None: + return False + return adapted is ob + # b/c dict interface for "root factory" code that expects a bare # environ. Explicitly omitted dict methods: clear (unnecessary), # copy (implemented by WebOb), fromkeys (unnecessary); deprecated diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index e35856ce0..90c55b0f0 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -203,7 +203,35 @@ class TestRequest(unittest.TestCase): self.assertEqual(result, 'abc') self.assertEqual(info.args, ('pyramid.tests:static/foo.css', request, {}) ) + + def test_is_response_false(self): + request = self._makeOne({}) + request.registry = self.config.registry + self.assertEqual(request.is_response('abc'), False) + + def test_is_response_false_adapter_is_not_self(self): + from pyramid.interfaces import IResponse + request = self._makeOne({}) + request.registry = self.config.registry + def adapter(ob): + return object() + class Foo(object): + pass + foo = Foo() + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), False) + def test_is_response_adapter_true(self): + from pyramid.interfaces import IResponse + request = self._makeOne({}) + request.registry = self.config.registry + class Foo(object): + pass + foo = Foo() + def adapter(ob): + return ob + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), True) class TestRequestDeprecatedMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 0ea9a11a0..b42224d4c 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -198,6 +198,14 @@ class RenderViewTests(BaseTest, unittest.TestCase): self.assertEqual(s, 'anotherview') class TestIsResponse(unittest.TestCase): + def setUp(self): + from zope.deprecation import __show__ + __show__.off() + + def tearDown(self): + from zope.deprecation import __show__ + __show__.on() + def _callFUT(self, *arg, **kw): from pyramid.view import is_response return is_response(*arg, **kw) diff --git a/pyramid/view.py b/pyramid/view.py index a89df8859..afa10fd0f 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -318,14 +318,15 @@ def is_response(ob): """ Return ``True`` if ``ob`` implements the interface implied by :ref:`the_response`. ``False`` if not. - .. note:: This isn't a true interface or subclass check. Instead, it's a - duck-typing check, as response objects are not obligated to be of a - particular class or provide any particular Zope interface.""" - - # response objects aren't obligated to implement a Zope interface, - # so we do it the hard way + .. warning:: This function is deprecated as of :app:`Pyramid` 1.1. New + code should not use it. Instead, new code should use the + :func:`pyramid.request.Request.is_response` method.""" if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and hasattr(ob, 'status') ): return True return False +deprecated( + 'is_response', + 'pyramid.view.is_response is deprecated as of Pyramid 1.1. Use ' + 'pyramid.request.Request.is_response instead.') -- cgit v1.2.3 From 53d11e7793317eee0f756b1e77b853ae7e1e6726 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 04:31:26 -0400 Subject: - Move default app_iter generation logic into __call__ for exception responses. - Add note about why we've created a shadow exception hierarchy parallel to that of webob.exc. --- CHANGES.txt | 19 +++++++++ pyramid/httpexceptions.py | 43 ++++++++------------ pyramid/tests/test_httpexceptions.py | 77 +++++++++++++++++++++++++----------- 3 files changed, 89 insertions(+), 50 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ea4bedc7e..a2976d1a2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -320,6 +320,25 @@ Behavior Changes ``webob.response.Response`` (in order to directly implement the ``pyramid.interfaces.IResponse`` interface). +- The "exception response" objects importable from ``pyramid.httpexceptions`` + (e.g. ``HTTPNotFound``) are no longer just import aliases for classes that + actually live in ``webob.exc``. Instead, we've defined our own exception + classes within the module that mirror and emulate the ``webob.exc`` + exception response objects almost entirely. We do this in order to a) + allow the exception responses to subclass ``pyramid.response.Response``, + which speeds up response generation slightly due to the way the Pyramid + router works, b) allows us to provide alternate __call__ logic which also + speeds up response generation, c) allows the exception classes to provide + for the proper value of ``self.RequestClass`` (pyramid.request.Request), d) + allows us freedom from having to think about backwards compatibility code + present in ``webob.exc`` having to do with Python 2.4, which we no longer + support, e) We change the behavior of two classes (HTTPNotFound and + HTTPForbidden) in the module so that they can be used internally for + notfound and forbidden exceptions, f) allows us to influence the docstrings + of the exception classes to provide Pyramid-specific documentation, and g) + allows us to silence a stupid deprecation warning under Python 2.6 when the + response objects are used as exceptions (related to ``self.message``). + Backwards Incompatibilities --------------------------- diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index f3df574a0..6d689988e 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -143,21 +143,16 @@ class WSGIHTTPException(Response, HTTPException): # 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. + # - bases plaintext vs. html result on self.content_type rather than + # on request accept header # # - doesn't use "strip_tags" (${br} placeholder for
, 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") + # - sets a default app_iter onto self during __call__ using a template if + # no body, app_iter, or unicode_body is set onto the response (instead of + # the replaced version's "generate_response") # # - explicitly sets self.message = detail to prevent whining by Python # 2.6.5+ access of Exception.message @@ -213,18 +208,11 @@ ${body}''') 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). + def _default_app_iter(self, environ): html_comment = '' comment = self.comment or '' content_type = self.content_type or '' @@ -250,24 +238,27 @@ ${body}''') 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 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 + return [page] @property - def exception(self): + def wsgi_response(self): # bw compat only return self - wsgi_response = exception # bw compat only + + exception = wsgi_response # bw compat only + + def __call__(self, environ, start_response): + if not self.body and not self.empty_body: + self.app_iter = self._default_app_iter(environ) + return Response.__call__(self, environ, start_response) class HTTPError(WSGIHTTPException): """ diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 629bbe225..60bde460e 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -138,7 +138,9 @@ class TestWSGIHTTPException(unittest.TestCase): def test_ctor_with_body_sets_default_app_iter_html(self): cls = self._getTargetSubclass() exc = cls('detail') - body = list(exc.app_iter)[0] + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] self.assertTrue(body.startswith('' in body) - def test_custom_body_template_no_environ(self): + def test_custom_body_template(self): cls = self._getTargetSubclass() - exc = cls(body_template='${location}', location='foo') + exc = cls(body_template='${REQUEST_METHOD}') 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] + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] self.assertEqual(body, '200 OK\n\nGET') def test_body_template_unicode(self): - 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) + environ = _makeEnviron(unicodeval=la) + exc = cls(body_template='${unicodeval}') exc.content_type = 'text/plain' - body = list(exc._default_app_iter())[0] + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] self.assertEqual(body, '200 OK\n\n/La Pe\xc3\xb1a') class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): @@ -230,9 +244,11 @@ class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): L = [] self.assertTrue(status_map) for v in status_map.values(): + environ = _makeEnviron() + start_response = DummyStartResponse() exc = v() exc.content_type = content_type - result = list(exc.app_iter)[0] + result = list(exc(environ, start_response))[0] if exc.empty_body: self.assertEqual(result, '') else: @@ -275,3 +291,16 @@ class TestHTTPForbidden(unittest.TestCase): class DummyRequest(object): exception = None +class DummyStartResponse(object): + def __call__(self, status, headerlist): + self.status = status + self.headerlist = headerlist + +def _makeEnviron(**kw): + environ = {'REQUEST_METHOD':'GET', + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'80'} + environ.update(kw) + return environ + -- cgit v1.2.3 From cecfc9e459166f3de13141954a61eaa2d6c905f2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Jun 2011 05:29:45 -0400 Subject: garden: --- TODO.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 27ab9ffb5..fb72d42b5 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,7 +4,8 @@ Pyramid TODOs Must-Have --------- -- To subclass or not subclass http exceptions. +- Copy exception templates from webob.exc into pyramid.httpexceptions and + ensure they all work. - Docs mention ``exception.args[0]`` as a way to get messages; check that this works. -- cgit v1.2.3