Chris McDonough
2011-06-14 53d11e7793317eee0f756b1e77b853ae7e1e6726
- 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.
3 files modified
139 ■■■■■ changed files
CHANGES.txt 19 ●●●●● patch | view | raw | blame | history
pyramid/httpexceptions.py 43 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_httpexceptions.py 77 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -320,6 +320,25 @@
  ``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
---------------------------
pyramid/httpexceptions.py
@@ -143,21 +143,16 @@
    # 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 <br/>, no other html
    #   in default body template)
    #
    # - sets a default app_iter if no body, app_iter, or unicode_body is
    #   passed using a template (ala the replaced version's "generate_response")
    # - 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 @@
        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_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):
    """
pyramid/tests/test_httpexceptions.py
@@ -138,7 +138,9 @@
    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('<html'))
        self.assertTrue('200 OK' in body)
        self.assertTrue('explanation' in body)
@@ -148,7 +150,9 @@
        cls = self._getTargetSubclass()
        exc = cls('detail')
        exc.content_type = 'text/plain'
        body = list(exc.app_iter)[0]
        environ = _makeEnviron()
        start_response = DummyStartResponse()
        body = list(exc(environ, start_response))[0]
        self.assertEqual(body, '200 OK\n\nexplanation\n\n\ndetail\n\n')
    def test__str__detail(self):
@@ -169,59 +173,69 @@
        exc = self._makeOne()
        self.assertTrue(exc is exc.exception)
    def test__calls_start_response(self):
        cls = self._getTargetSubclass()
        exc = cls()
        exc.content_type = 'text/plain'
        environ = _makeEnviron()
        start_response = DummyStartResponse()
        exc(environ, start_response)
        self.assertTrue(start_response.headerlist)
        self.assertEqual(start_response.status, '200 OK')
    def test__default_app_iter_no_comment_plain(self):
        cls = self._getTargetSubclass()
        exc = cls()
        exc.content_type = 'text/plain'
        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\nexplanation\n\n\n\n\n')
    def test__default_app_iter_with_comment_plain(self):
        cls = self._getTargetSubclass()
        exc = cls(comment='comment')
        exc.content_type = 'text/plain'
        body = list(exc._default_app_iter())[0]
        environ = _makeEnviron()
        start_response = DummyStartResponse()
        body = list(exc(environ, start_response))[0]
        self.assertEqual(body, '200 OK\n\nexplanation\n\n\n\ncomment\n')
        
    def test__default_app_iter_no_comment_html(self):
        cls = self._getTargetSubclass()
        exc = cls()
        exc.content_type = 'text/html'
        body = list(exc._default_app_iter())[0]
        environ = _makeEnviron()
        start_response = DummyStartResponse()
        body = list(exc(environ, start_response))[0]
        self.assertFalse('<!-- ' in body)
    def test__default_app_iter_with_comment_html(self):
        cls = self._getTargetSubclass()
        exc = cls(comment='comment & comment')
        exc.content_type = 'text/html'
        body = list(exc._default_app_iter())[0]
        environ = _makeEnviron()
        start_response = DummyStartResponse()
        body = list(exc(environ, start_response))[0]
        self.assertTrue('<!-- comment &amp; comment -->' 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 @@
        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 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