diff options
| author | Chris McDonough <chrism@plope.com> | 2011-04-21 23:43:07 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-04-21 23:43:07 -0400 |
| commit | 990efe90b5296f4f3d140b5a5fad9cf89834c47b (patch) | |
| tree | 2f12f9cdacf4237b347be53cc93a0544e1ebae4c | |
| parent | f6799bbb6e9aca55f344f5d7f49f8fcb7af13db4 (diff) | |
| parent | ba0a5f88d916d97fe52540a535b8b5520815201a (diff) | |
| download | pyramid-990efe90b5296f4f3d140b5a5fad9cf89834c47b.tar.gz pyramid-990efe90b5296f4f3d140b5a5fad9cf89834c47b.tar.bz2 pyramid-990efe90b5296f4f3d140b5a5fad9cf89834c47b.zip | |
Merge branch 'scriptname_fixes'
| -rw-r--r-- | CHANGES.txt | 13 | ||||
| -rw-r--r-- | pyramid/request.py | 52 | ||||
| -rw-r--r-- | pyramid/static.py | 43 | ||||
| -rw-r--r-- | pyramid/tests/test_integration.py | 20 | ||||
| -rw-r--r-- | pyramid/tests/test_request.py | 67 | ||||
| -rw-r--r-- | pyramid/tests/test_static.py | 9 | ||||
| -rw-r--r-- | pyramid/tests/test_wsgi.py | 48 | ||||
| -rw-r--r-- | pyramid/tests/wsgiapp2app/__init__.py | 17 | ||||
| -rw-r--r-- | pyramid/wsgi.py | 40 |
9 files changed, 204 insertions, 105 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c3bdd54ef..c0108ff3a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -140,6 +140,13 @@ Bug Fixes - Redirects issued by a static view did not take into account any existing ``SCRIPT_NAME`` (such as one set by a url mapping composite). Now they do. +- The ``pyramid.wsgi.wsgiapp2`` decorator did not take into account the + ``SCRIPT_NAME`` in the origin request. + +- The ``pyramid.wsgi.wsgiapp2`` decorator effectively only worked when it + decorated a view found via traversal; it ignored the ``PATH_INFO`` that was + part of a url-dispatch-matched view. + Deprecations ------------ @@ -173,6 +180,12 @@ Behavior Changes renderer changes the content type (to ``application/json`` or ``text/plain`` for JSON and string renderers respectively). +- The ``pyramid.wsgi.wsgiapp2`` now uses a slightly different method of + figuring out how to "fix" ``SCRIPT_NAME`` and ``PATH_INFO`` for the + downstream application. As a result, those values may differ slightly from + the perspective of the downstream application (for example, ``SCRIPT_NAME`` + will now never possess a trailing slash). + Dependencies ------------ diff --git a/pyramid/request.py b/pyramid/request.py index 9d2b9344b..0fe8b9379 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -10,6 +10,7 @@ from pyramid.interfaces import IResponseFactory from pyramid.exceptions import ConfigurationError from pyramid.decorator import reify from pyramid.response import Response +from pyramid.traversal import quote_path_segment from pyramid.url import resource_url from pyramid.url import route_url from pyramid.url import static_url @@ -393,3 +394,54 @@ def add_global_response_headers(request, headerlist): response.headerlist.append((k, v)) request.add_response_callback(add_headers) +def call_app_with_subpath_as_path_info(request, app): + # Copy the request. Use the source request's subpath (if it exists) as + # the new request's PATH_INFO. Set the request copy's SCRIPT_NAME to the + # prefix before the subpath. Call the application with the new request + # and return a response. + # + # Postconditions: + # - SCRIPT_NAME and PATH_INFO are empty or start with / + # - At least one of SCRIPT_NAME or PATH_INFO are set. + # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should + # be '/'). + + environ = request.environ + script_name = environ.get('SCRIPT_NAME', '') + path_info = environ.get('PATH_INFO', '/') + subpath = list(getattr(request, 'subpath', ())) + + new_script_name = '' + + # compute new_path_info + new_path_info = '/' + '/'.join([x.encode('utf-8') for x in subpath]) + + if new_path_info != '/': # don't want a sole double-slash + if path_info != '/': # if orig path_info is '/', we're already done + if path_info.endswith('/'): + # readd trailing slash stripped by subpath (traversal) + # conversion + new_path_info += '/' + + # compute new_script_name + workback = (script_name + path_info).split('/') + + tmp = [] + while workback: + if tmp == subpath: + break + el = workback.pop() + if el: + tmp.insert(0, el.decode('utf-8')) + + # strip all trailing slashes from workback to avoid appending undue slashes + # to end of script_name + while workback and (workback[-1] == ''): + workback = workback[:-1] + + new_script_name = '/'.join(workback) + + new_request = request.copy() + new_request.environ['SCRIPT_NAME'] = new_script_name + new_request.environ['PATH_INFO'] = new_path_info + return new_request.get_response(app) diff --git a/pyramid/static.py b/pyramid/static.py index 223652768..170b027f1 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -13,6 +13,7 @@ from zope.interface import implements from pyramid.asset import resolve_asset_spec from pyramid.interfaces import IStaticURLInfo from pyramid.path import caller_package +from pyramid.request import call_app_with_subpath_as_path_info from pyramid.url import route_url class PackageURLParser(StaticURLParser): @@ -208,44 +209,4 @@ class static_view(object): self.app = app def __call__(self, context, request): - # Point PATH_INFO to the static file/dir path; point SCRIPT_NAME - # to the prefix before it. - - # Postconditions: - # - SCRIPT_NAME and PATH_INFO are empty or start with / - # - At least one of SCRIPT_NAME or PATH_INFO are set. - # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should - # be '/'). - - request_copy = request.copy() - script_name = request_copy.environ.get('SCRIPT_NAME', '') - path_info = request_copy.environ.get('PATH_INFO', '/') - - new_script_name = script_name - new_path_info = path_info - - subpath = list(request.subpath) - - if subpath: - # compute new_path_info - new_path_info = '/' + '/'.join(subpath) - if path_info.endswith('/'): - # readd trailing slash stripped by subpath (traversal) - # conversion - new_path_info += '/' - - # compute new_script_name - tmp = [] - workback = (script_name + path_info).split('/') - while workback: - el = workback.pop() - if el: - tmp.insert(0, el) - if tmp == subpath: - new_script_name = '/'.join(workback) - break - - request_copy.environ['SCRIPT_NAME'] = new_script_name - request_copy.environ['PATH_INFO'] = new_path_info - - return request_copy.get_response(self.app) + return call_app_with_subpath_as_path_info(request, self.app) diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 1f9484279..dd77d3aec 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -104,7 +104,7 @@ class TestStaticApp(unittest.TestCase): from webob import Request context = DummyContext() from StringIO import StringIO - request = Request({'PATH_INFO':'', + request = Request({'PATH_INFO':'/static', 'SCRIPT_NAME':'/script_name', 'SERVER_NAME':'localhost', 'SERVER_PORT':'80', @@ -112,7 +112,7 @@ class TestStaticApp(unittest.TestCase): 'wsgi.version':(1,0), 'wsgi.url_scheme':'http', 'wsgi.input':StringIO()}) - request.subpath = ['static'] + request.subpath = ('static',) result = staticapp(context, request) self.assertEqual(result.status, '301 Moved Permanently') self.assertEqual(result.location, @@ -390,6 +390,22 @@ class SelfScanAppTest(unittest.TestCase): res = self.testapp.get('/two', status=200) self.assertTrue('two' in res.body) +class WSGIApp2AppTest(unittest.TestCase): + def setUp(self): + from pyramid.tests.wsgiapp2app import main + config = main() + app = config.make_wsgi_app() + from webtest import TestApp + self.testapp = TestApp(app) + self.config = config + + def tearDown(self): + self.config.end() + + def test_hello(self): + res = self.testapp.get('/hello', status=200) + self.assertTrue('Hello' in res.body) + class DummyContext(object): pass diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 66a451220..60d59ece6 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -325,6 +325,66 @@ class Test_add_global_response_headers(unittest.TestCase): request.response_callbacks[0](None, response) self.assertEqual(response.headerlist, [('c', 1)] ) +class Test_call_app_with_subpath_as_path_info(unittest.TestCase): + def _callFUT(self, request, app): + from pyramid.request import call_app_with_subpath_as_path_info + return call_app_with_subpath_as_path_info(request, app) + + def test_it_all_request_and_environment_data_missing(self): + request = DummyRequest({}) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + self.assertEqual(request.environ['PATH_INFO'], '/') + + def test_it_with_subpath_and_path_info(self): + request = DummyRequest({'PATH_INFO':'/hello'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + self.assertEqual(request.environ['PATH_INFO'], '/hello') + + def test_it_with_subpath_and_path_info_path_info_endswith_slash(self): + request = DummyRequest({'PATH_INFO':'/hello/'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + self.assertEqual(request.environ['PATH_INFO'], '/hello/') + + def test_it_with_subpath_and_path_info_extra_script_name(self): + request = DummyRequest({'PATH_INFO':'/hello', 'SCRIPT_NAME':'/script'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '/script') + self.assertEqual(request.environ['PATH_INFO'], '/hello') + + def test_it_with_extra_slashes_in_path_info(self): + request = DummyRequest({'PATH_INFO':'//hello/', + 'SCRIPT_NAME':'/script'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '/script') + self.assertEqual(request.environ['PATH_INFO'], '/hello/') + + def test_subpath_path_info_and_script_name_have_utf8(self): + la = 'La Pe\xc3\xb1a' + request = DummyRequest({'PATH_INFO':'/'+la, 'SCRIPT_NAME':'/'+la}) + request.subpath = (unicode(la, 'utf-8'), ) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '/' + la) + self.assertEqual(request.environ['PATH_INFO'], '/' + la) + class DummyRequest: def __init__(self, environ=None): if environ is None: @@ -334,6 +394,13 @@ class DummyRequest: def add_response_callback(self, callback): self.response_callbacks = [callback] + def get_response(self, app): + return app + + def copy(self): + self.copied = True + return self + class DummyResponse: def __init__(self): self.headerlist = [] diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index b9d464bf1..588dc32e2 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -268,8 +268,9 @@ class Test_static_view(unittest.TestCase): SCRIPT_NAME='/script_name') view(context, request) self.assertEqual(request.copied, True) - self.assertEqual(request.environ['PATH_INFO'], '/path_info') - self.assertEqual(request.environ['SCRIPT_NAME'], '/script_name') + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], + '/script_name/path_info') def test_with_subpath_path_info_ends_with_slash(self): view = self._makeOne('fixtures', package_name='another') @@ -295,7 +296,7 @@ class Test_static_view(unittest.TestCase): self.assertEqual(request.environ['SCRIPT_NAME'], '/scriptname/path_info') - def test_with_subpath_new_script_name_fixes_trailing_double_slashes(self): + def test_with_subpath_new_script_name_fixes_trailing_slashes(self): view = self._makeOne('fixtures', package_name='another') context = DummyContext() request = DummyRequest() @@ -304,7 +305,7 @@ class Test_static_view(unittest.TestCase): view(context, request) self.assertEqual(request.copied, True) self.assertEqual(request.environ['PATH_INFO'], '/sub/path/') - self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/path_info') class TestStaticURLInfo(unittest.TestCase): def _getTargetClass(self): diff --git a/pyramid/tests/test_wsgi.py b/pyramid/tests/test_wsgi.py index f63667352..06bcf1cb2 100644 --- a/pyramid/tests/test_wsgi.py +++ b/pyramid/tests/test_wsgi.py @@ -20,11 +20,9 @@ class WSGIApp2Tests(unittest.TestCase): def test_decorator_with_subpath_and_view_name(self): context = DummyContext() request = DummyRequest() - request.traversed = ['a', 'b'] - request.virtual_root_path = ['a'] - request.subpath = ['subpath'] - request.view_name = 'view_name' - request.environ = {'SCRIPT_NAME':'/foo'} + request.subpath = ('subpath',) + request.environ = {'SCRIPT_NAME':'/foo', + 'PATH_INFO':'/b/view_name/subpath'} decorator = self._callFUT(dummyapp) response = decorator(context, request) self.assertEqual(response, dummyapp) @@ -34,11 +32,8 @@ class WSGIApp2Tests(unittest.TestCase): def test_decorator_with_subpath_no_view_name(self): context = DummyContext() request = DummyRequest() - request.traversed = ['a', 'b'] - request.virtual_root_path = ['a'] - request.subpath = ['subpath'] - request.view_name = '' - request.environ = {'SCRIPT_NAME':'/foo'} + request.subpath = ('subpath',) + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/b/subpath'} decorator = self._callFUT(dummyapp) response = decorator(context, request) self.assertEqual(response, dummyapp) @@ -48,11 +43,8 @@ class WSGIApp2Tests(unittest.TestCase): def test_decorator_no_subpath_with_view_name(self): context = DummyContext() request = DummyRequest() - request.traversed = ['a', 'b'] - request.virtual_root_path = ['a'] - request.subpath = [] - request.view_name = 'view_name' - request.environ = {'SCRIPT_NAME':'/foo'} + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/b/view_name'} decorator = self._callFUT(dummyapp) response = decorator(context, request) self.assertEqual(response, dummyapp) @@ -62,11 +54,8 @@ class WSGIApp2Tests(unittest.TestCase): def test_decorator_traversed_empty_with_view_name(self): context = DummyContext() request = DummyRequest() - request.traversed = [] - request.virtual_root_path = [] - request.subpath = [] - request.view_name = 'view_name' - request.environ = {'SCRIPT_NAME':'/foo'} + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/view_name'} decorator = self._callFUT(dummyapp) response = decorator(context, request) self.assertEqual(response, dummyapp) @@ -76,11 +65,8 @@ class WSGIApp2Tests(unittest.TestCase): def test_decorator_traversed_empty_no_view_name(self): context = DummyContext() request = DummyRequest() - request.traversed = [] - request.virtual_root_path = [] - request.subpath = [] - request.view_name = '' - request.environ = {'SCRIPT_NAME':'/foo'} + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/'} decorator = self._callFUT(dummyapp) response = decorator(context, request) self.assertEqual(response, dummyapp) @@ -90,11 +76,8 @@ class WSGIApp2Tests(unittest.TestCase): def test_decorator_traversed_empty_no_view_name_no_script_name(self): context = DummyContext() request = DummyRequest() - request.traversed = [] - request.virtual_root_path = [] - request.subpath = [] - request.view_name = '' - request.environ = {'SCRIPT_NAME':''} + request.subpath = () + request.environ = {'SCRIPT_NAME':'', 'PATH_INFO':'/'} decorator = self._callFUT(dummyapp) response = decorator(context, request) self.assertEqual(response, dummyapp) @@ -110,3 +93,8 @@ class DummyContext: class DummyRequest: def get_response(self, application): return application + + def copy(self): + self.copied = True + return self + diff --git a/pyramid/tests/wsgiapp2app/__init__.py b/pyramid/tests/wsgiapp2app/__init__.py new file mode 100644 index 000000000..0880556ef --- /dev/null +++ b/pyramid/tests/wsgiapp2app/__init__.py @@ -0,0 +1,17 @@ +from pyramid.view import view_config +from pyramid.wsgi import wsgiapp2 + +@view_config(name='hello', renderer='string') +@wsgiapp2 +def hello(environ, start_response): + assert environ['PATH_INFO'] == '/' + assert environ['SCRIPT_NAME'] == '/hello' + response_headers = [('Content-Type', 'text/plain')] + start_response('200 OK', response_headers) + return ['Hello!'] + +def main(): + from pyramid.config import Configurator + c = Configurator() + c.scan() + return c diff --git a/pyramid/wsgi.py b/pyramid/wsgi.py index e988a000e..e4c61ff63 100644 --- a/pyramid/wsgi.py +++ b/pyramid/wsgi.py @@ -1,5 +1,5 @@ from pyramid.compat import wraps -from pyramid.traversal import quote_path_segment +from pyramid.request import call_app_with_subpath_as_path_info def wsgiapp(wrapped): """ Decorator to turn a WSGI application into a :app:`Pyramid` @@ -31,7 +31,7 @@ def wsgiapp(wrapped): """ def decorator(context, request): return request.get_response(wrapped) - return wraps(wrapped)(decorator) # grokkability + return wraps(wrapped)(decorator) def wsgiapp2(wrapped): """ Decorator to turn a WSGI application into a :app:`Pyramid` @@ -56,31 +56,15 @@ def wsgiapp2(wrapped): config.add_view(hello_world, name='hello_world.txt') The ``wsgiapp2`` decorator will convert the result of the WSGI - application to a Response and return it to :app:`Pyramid` as if - the WSGI app were a :app:`Pyramid` view. The ``SCRIPT_NAME`` - and ``PATH_INFO`` values present in the WSGI environment are fixed - up before the application is invoked. """ + application to a Response and return it to :app:`Pyramid` as if the WSGI + app were a :app:`Pyramid` view. The ``SCRIPT_NAME`` and ``PATH_INFO`` + values present in the WSGI environment are fixed up before the + application is invoked. In particular, a new WSGI environment is + generated, and the :term:`subpath` of the request passed to ``wsgiapp2`` + is used as the new request's ``PATH_INFO`` and everything preceding the + subpath is used as the ``SCRIPT_NAME``. The new environment is passed to + the downstream WSGI application.""" def decorator(context, request): - traversed = request.traversed - vroot_path = request.virtual_root_path - if not vroot_path: - vroot_path = () - view_name = request.view_name - subpath = request.subpath - if not subpath: - subpath = () - script_tuple = traversed[len(vroot_path):] - script_list = [ quote_path_segment(name) for name in script_tuple ] - if view_name: - script_list.append(quote_path_segment(view_name)) - script_name = '/' + '/'.join(script_list) - path_list = [ quote_path_segment(name) for name in subpath ] - path_info = '/' + '/'.join(path_list) - request.environ['PATH_INFO'] = path_info - script_name = request.environ['SCRIPT_NAME'] + script_name - if script_name.endswith('/'): - script_name = script_name[:-1] - request.environ['SCRIPT_NAME'] = script_name - return request.get_response(wrapped) - return wraps(wrapped)(decorator) # grokkability + return call_app_with_subpath_as_path_info(request, wrapped) + return wraps(wrapped)(decorator) |
