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