diff --git a/AUTHORS b/AUTHORS index 371fa09..57ac3ef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,6 +7,7 @@ by date of contribution: * Chad Lung (chadlung) * Josh Brand (joshbrand) * Jamie Painter (painterjd) +* Flavio Percoco (flaper87) * Randall Burt (rs-randallburt) * Zhihao Yuan (lichray) * Ashutosh Das (pyprism) diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py index bbf17fc..a323196 100644 --- a/falcon/api_helpers.py +++ b/falcon/api_helpers.py @@ -19,8 +19,6 @@ limitations under the License. import re from functools import wraps -import six - from falcon import responders, HTTP_METHODS import falcon.status_codes as status @@ -77,10 +75,10 @@ def set_content_length(resp): content_length = 0 - if resp.body is not None: + if resp.body_encoded is not None: # Since body is assumed to be a byte string (str in Python 2, bytes in # Python 3), figure out the length using standard functions. - content_length = len(resp.body) + content_length = len(resp.body_encoded) elif resp.data is not None: content_length = len(resp.data) elif resp.stream is not None: @@ -111,13 +109,10 @@ def get_body(resp): """ - body = resp.body + body = resp.body_encoded if body is not None: - if isinstance(body, six.text_type): - return [body.encode('utf-8')] - else: - return [body] + return [body] elif resp.data is not None: return [resp.data] diff --git a/falcon/response.py b/falcon/response.py index 450a0fe..a9ed6c2 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -16,6 +16,8 @@ limitations under the License. """ +import six + from falcon.response_helpers import header_property, format_range from falcon.util import dt_to_http, percent_escape @@ -36,7 +38,8 @@ class Response(object): """ __slots__ = ( - 'body', # Stuff + '_body', # Stuff + '_body_encoded', # Stuff 'data', '_headers', 'status', @@ -55,11 +58,50 @@ class Response(object): self.status = '200 OK' self._headers = {} - self.body = None + self._body = None + self._body_encoded = None self.data = None self.stream = None self.stream_len = None + def _get_body(self): + """Returns the body as-is.""" + return self._body + + def _set_body(self, value): + """Sets the body and clears the encoded cache.""" + self._body = value + self._body_encoded = None + + # NOTE(flaper87): Lets use a property + # for the body in case its content was + # encoded and then modified. + body = property(_get_body, _set_body) + + @property + def body_encoded(self): + """Encode the body and return it + + This property will encode `_body` and + cache the result in the `_body_encoded` + attribute. + """ + # NOTE(flaper87): Notice this property + # is not thread-safe. If body is modified + # before this property returns, we might + # end up returning None. + body = self._body + if body and self._body_encoded is None: + + # NOTE(flaper87): Assume it is an + # encoded str, then check and encode + # if it isn't. + self._body_encoded = body + if isinstance(body, six.text_type): + self._body_encoded = body.encode('utf-8') + + return self._body_encoded + def set_header(self, name, value): """Set a header for this response to a given value. diff --git a/falcon/tests/test_after_hooks.py b/falcon/tests/test_after_hooks.py index 3849589..11dec80 100644 --- a/falcon/tests/test_after_hooks.py +++ b/falcon/tests/test_after_hooks.py @@ -85,7 +85,7 @@ class TestHooks(testing.TestBase): self.api.add_route(self.test_route, zoo_resource) self.simulate_request(self.test_route) - self.assertEqual('fluffy', zoo_resource.resp.body) + self.assertEqual(b'fluffy', zoo_resource.resp.body_encoded) def test_multiple_global_hook(self): self.api = falcon.API(after=[fluffiness, cuteness]) @@ -94,29 +94,28 @@ class TestHooks(testing.TestBase): self.api.add_route(self.test_route, zoo_resource) self.simulate_request(self.test_route) - self.assertEqual('fluffy and cute', zoo_resource.resp.body) + self.assertEqual(b'fluffy and cute', zoo_resource.resp.body_encoded) def test_output_validator(self): self.simulate_request(self.test_route) self.assertEqual(falcon.HTTP_723, self.srmock.status) - self.assertEqual(None, self.resource.resp.body) + self.assertEqual(None, self.resource.resp.body_encoded) def test_serializer(self): self.simulate_request(self.test_route, method='PUT') - actual_body = self.resource.resp.body - self.assertEqual('{"animal": "falcon"}', actual_body) + actual_body = self.resource.resp.body_encoded + self.assertEqual(b'{"animal": "falcon"}', actual_body) def test_wrapped_resource(self): - expected = 'fluffy and cute' + expected = b'fluffy and cute' self.simulate_request('/wrapped') self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual(expected, self.wrapped_resource.resp.body) + self.assertEqual(expected, self.wrapped_resource.resp.body_encoded) self.simulate_request('/wrapped', method='HEAD') self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual(expected, self.wrapped_resource.resp.body) self.simulate_request('/wrapped', method='POST') self.assertEqual(falcon.HTTP_405, self.srmock.status) diff --git a/falcon/tests/test_hello.py b/falcon/tests/test_hello.py index 878cef7..728c38a 100644 --- a/falcon/tests/test_hello.py +++ b/falcon/tests/test_hello.py @@ -97,11 +97,11 @@ class TestHelloWorld(testing.TestBase): resp = self.resource.resp content_length = int(self.srmock.headers_dict['Content-Length']) - self.assertEquals(content_length, len(self.resource.sample_unicode)) + self.assertEquals(content_length, len(self.resource.sample_utf8)) self.assertEquals(self.srmock.status, self.resource.sample_status) self.assertEquals(resp.status, self.resource.sample_status) - self.assertEquals(resp.body, self.resource.sample_unicode) + self.assertEquals(resp.body_encoded, self.resource.sample_utf8) self.assertEquals(body, [self.resource.sample_utf8]) def test_body_bytes(self): @@ -113,7 +113,7 @@ class TestHelloWorld(testing.TestBase): self.assertEquals(self.srmock.status, self.resource.sample_status) self.assertEquals(resp.status, self.resource.sample_status) - self.assertEquals(resp.body, self.resource.sample_utf8) + self.assertEquals(resp.body_encoded, self.resource.sample_utf8) self.assertEquals(body, [self.resource.sample_utf8]) def test_data(self): diff --git a/falcon/tests/test_response_body.py b/falcon/tests/test_response_body.py new file mode 100644 index 0000000..1c12ae2 --- /dev/null +++ b/falcon/tests/test_response_body.py @@ -0,0 +1,17 @@ + +import falcon +import falcon.testing as testing + + +class TestResponseBody(testing.TestBase): + + def test_append_body(self): + text = "Hello beautiful world! " + resp = falcon.Response() + resp.body = "" + + for token in text.split(): + resp.body += token + resp.body += " " + + self.assertEquals(resp.body, text)