From cf94a4d6007d3b44728e95374b5f515176ac86ea Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Fri, 7 Feb 2014 23:22:50 -0600 Subject: [PATCH] app: Fixed an issue with unicode responses in Python 2.7 If a controller callable returns a string, it needs to be wrapped in a Response object. To determine if this is the case, the Application tests to see if the returned object is an instance of `basestring`. Since `basestring` doesn't exist in Python 3, only `str` is a valid return type. Unfortunately, my way of testing whether the `basestring` type existed was flawed. Instead of raising `NameError` when it doesn't exist, `UnboundLocalError` (a subclass `NameError`) is *always* raised. Since the exception handler sets `basestring` equal to `str` assuming this is Python 3, most of the time this isn't a problem. If, however, the controller returns a `unicode` object in Python 2, the `isinstance` call returns `False`, so the response is not wrapped in a Response object. Rather than try to reassign the `basestring` name, now we just use `_string`, which will either be `basestring` (in Python 2) or `str` (in Python 3). Apparently, the unit tests didn't cover this case... --- src/milla/app.py | 9 ++-- src/milla/tests/test_app.py | 87 ++++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/milla/app.py b/src/milla/app.py index 3dcb944..7148fd2 100644 --- a/src/milla/app.py +++ b/src/milla/app.py @@ -103,12 +103,11 @@ class Application(object): # but we need to wrap it in a Response object try: # In Python 2, it could be a str or a unicode object - basestring = basestring #@UndefinedVariable + _string = basestring except NameError: - # Python 3 has no unicode objects and thus no need for - # basestring so we, just make it an alias for str - basestring = str - if isinstance(response, basestring) or not response: + # In Python 3, we are only interested in str objects + _string = str + if isinstance(response, _string) or not response: response = request.ResponseClass(response) if not start_response_wrapper.called: diff --git a/src/milla/tests/test_app.py b/src/milla/tests/test_app.py index f42f0ca..1866c72 100644 --- a/src/milla/tests/test_app.py +++ b/src/milla/tests/test_app.py @@ -3,13 +3,31 @@ :Created: Nov 27, 2012 :Author: dustin ''' +from unittest.case import SkipTest import functools import milla.app import milla.dispatch import nose.tools +import sys import wsgiref.util import webob.exc +def python2_only(test): + @functools.wraps(test) + def wrapper(): + if sys.version_info[0] != 2: + raise SkipTest + return test() + return wrapper + +def python3_only(test): + @functools.wraps(test) + def wrapper(): + if sys.version_info[0] != 3: + raise SkipTest + return test() + return wrapper + class StubResolver(object): '''Stub resolver for testing purposes''' @@ -122,7 +140,7 @@ def test_allow_header_options(): def test_emulated_method(): '''Emulated HTTP methods are interpreted correctly - + For applications that cannot use the proper HTTP method and instead use HTTP POST with an ``_method`` parameter ''' @@ -145,6 +163,64 @@ def test_emulated_method(): response.finish_response(app_iter) assert response.headers.startswith('HTTP/1.1 200'), response.headers +def test_return_none(): + '''Controllers can return None + ''' + + def controller(request): + return None + + app = milla.app.Application(StubResolver(controller)) + environ = environ_for_testing() + response = ResponseMaker() + app_iter = app(environ, response.start_response) + response.finish_response(app_iter) + assert not response.body, response.body + +def test_return_str(): + '''Controllers can return str objects + ''' + + def controller(request): + return 'Hello, world' + + app = milla.app.Application(StubResolver(controller)) + environ = environ_for_testing() + response = ResponseMaker() + app_iter = app(environ, response.start_response) + response.finish_response(app_iter) + assert response.body == b'Hello, world', response.body + +@python2_only +def test_return_unicode(): + '''Controllers can return unicode objects + ''' + + def controller(request): + return unicode('Hello, world') + + app = milla.app.Application(StubResolver(controller)) + environ = environ_for_testing() + response = ResponseMaker() + app_iter = app(environ, response.start_response) + response.finish_response(app_iter) + assert response.body == unicode('Hello, world'), response.body + +@nose.tools.raises(AttributeError) +@python3_only +def test_return_bytes(): + '''Controllers cannot return bytes objects + ''' + + def controller(request): + return b'Hello, world' + + app = milla.app.Application(StubResolver(controller)) + environ = environ_for_testing() + response = ResponseMaker() + app_iter = app(environ, response.start_response) + response.finish_response(app_iter) + @nose.tools.raises(BeforeCalled) def test_function_before(): '''__before__ attribute is called for controller functions @@ -460,10 +536,10 @@ def test_create_href_full_keywords(): def test_static_resource(): '''Request.static_resource creates valid URL from config''' - + def controller(request): return request.static_resource('/image.png') - + environ = environ_for_testing() app = milla.Application(StubResolver(controller)) app.config['milla.static_root'] = '/static' @@ -474,14 +550,13 @@ def test_static_resource(): def test_static_resource_undefined(): '''Request.static_resource returns the path unmodified with no root defined''' - + def controller(request): return request.static_resource('/image.png') - + environ = environ_for_testing() app = milla.Application(StubResolver(controller)) response = ResponseMaker() app_iter = app(environ, response.start_response) response.finish_response(app_iter) assert response.body == b'/image.png', response.body - \ No newline at end of file