diff options
author | Ryan Petrello <lists@ryanpetrello.com> | 2015-05-07 10:45:39 -0700 |
---|---|---|
committer | Ryan Petrello <lists@ryanpetrello.com> | 2015-05-07 11:15:45 -0700 |
commit | 9a6a893d5e2f090087be92d301705e35ccf158ba (patch) | |
tree | 081c06e89567db76a03e701ac595e4b5cdddf65a /pecan | |
parent | d907987e7022025f3ad067d7407532125e6b8e2e (diff) | |
download | pecan-9a6a893d5e2f090087be92d301705e35ccf158ba.tar.gz |
Properly handle Python3 Unicode path segments in pecan routing.
Change-Id: I3890d73a087f7635ddc51b71d3d6f68a41058c42
Closes-Bug: 1451842
Diffstat (limited to 'pecan')
-rw-r--r-- | pecan/core.py | 4 | ||||
-rw-r--r-- | pecan/rest.py | 27 | ||||
-rw-r--r-- | pecan/routing.py | 5 | ||||
-rw-r--r-- | pecan/tests/test_base.py | 31 | ||||
-rw-r--r-- | pecan/tests/test_no_thread_locals.py | 2 | ||||
-rw-r--r-- | pecan/tests/test_rest.py | 74 |
6 files changed, 131 insertions, 12 deletions
diff --git a/pecan/core.py b/pecan/core.py index ad32525..141e7c3 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -423,7 +423,7 @@ class PecanBase(object): # store the routing path for the current application to allow hooks to # modify it - pecan_state['routing_path'] = path = req.encget('PATH_INFO') + pecan_state['routing_path'] = path = req.path_info # handle "on_route" hooks self.handle_hooks(self.hooks, 'on_route', state) @@ -538,7 +538,7 @@ class PecanBase(object): # handle "before" hooks self.handle_hooks(self.determine_hooks(controller), 'before', state) - return controller, args+varargs, kwargs + return controller, args + varargs, kwargs def invoke_controller(self, controller, args, kwargs, state): ''' diff --git a/pecan/rest.py b/pecan/rest.py index 9406ad4..ef5d753 100644 --- a/pecan/rest.py +++ b/pecan/rest.py @@ -59,6 +59,17 @@ class RestController(object): # invalid path. abort(404) + def _lookup_child(self, remainder): + """ + Lookup a child controller with a named path (handling Unicode paths + properly for Python 2). + """ + try: + controller = getattr(self, remainder, None) + except UnicodeEncodeError: + return None + return controller + @expose() def _route(self, args, request=None): ''' @@ -138,7 +149,7 @@ class RestController(object): Returns the appropriate controller for routing a custom action. ''' for name in args: - obj = getattr(self, name, None) + obj = self._lookup_child(name) if obj and iscontroller(obj): return obj return None @@ -167,7 +178,7 @@ class RestController(object): # attempt to locate a sub-controller if var_args: for i, item in enumerate(remainder): - controller = getattr(self, item, None) + controller = self._lookup_child(item) if controller and not ismethod(controller): self._set_routing_args(request, remainder[:i]) return lookup_controller(controller, remainder[i + 1:], @@ -175,7 +186,7 @@ class RestController(object): elif fixed_args < len(remainder) and hasattr( self, remainder[fixed_args] ): - controller = getattr(self, remainder[fixed_args]) + controller = self._lookup_child(remainder[fixed_args]) if not ismethod(controller): self._set_routing_args(request, remainder[:fixed_args]) return lookup_controller( @@ -201,7 +212,7 @@ class RestController(object): if remainder: if self._find_controller(remainder[0]): abort(405) - sub_controller = getattr(self, remainder[0], None) + sub_controller = self._lookup_child(remainder[0]) if sub_controller: return lookup_controller(sub_controller, remainder[1:], request) @@ -237,7 +248,7 @@ class RestController(object): if match: return match - controller = getattr(self, remainder[0], None) + controller = self._lookup_child(remainder[0]) if controller and not ismethod(controller): return lookup_controller(controller, remainder[1:], request) @@ -261,7 +272,7 @@ class RestController(object): if match: return match - controller = getattr(self, remainder[0], None) + controller = self._lookup_child(remainder[0]) if controller and not ismethod(controller): return lookup_controller(controller, remainder[1:], request) @@ -275,7 +286,7 @@ class RestController(object): if remainder: if self._find_controller(remainder[0]): abort(405) - sub_controller = getattr(self, remainder[0], None) + sub_controller = self._lookup_child(remainder[0]) if sub_controller: return lookup_controller(sub_controller, remainder[1:], request) @@ -295,7 +306,7 @@ class RestController(object): if match: return match - controller = getattr(self, remainder[0], None) + controller = self._lookup_child(remainder[0]) if controller and not ismethod(controller): return lookup_controller(controller, remainder[1:], request) diff --git a/pecan/routing.py b/pecan/routing.py index 1ac825f..1d534aa 100644 --- a/pecan/routing.py +++ b/pecan/routing.py @@ -152,7 +152,10 @@ def find_object(obj, remainder, notfound_handlers, request): prev_remainder = remainder prev_obj = obj remainder = rest - obj = getattr(obj, next_obj, None) + try: + obj = getattr(obj, next_obj, None) + except UnicodeEncodeError: + obj = None # Last-ditch effort: if there's not a matching subcontroller, no # `_default`, no `_lookup`, and no `_route`, look to see if there's diff --git a/pecan/tests/test_base.py b/pecan/tests/test_base.py index 58b5734..6332392 100644 --- a/pecan/tests/test_base.py +++ b/pecan/tests/test_base.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import sys import os import json @@ -259,6 +261,35 @@ class TestObjectDispatch(PecanTestCase): assert r.body == b_('/sub/sub/deeper') +@unittest.skipIf(not six.PY3, "tests are Python3 specific") +class TestUnicodePathSegments(PecanTestCase): + + def test_unicode_methods(self): + class RootController(object): + pass + setattr(RootController, '🌰', expose()(lambda self: 'Hello, World!')) + app = TestApp(Pecan(RootController())) + + resp = app.get('/%F0%9F%8C%B0/') + assert resp.status_int == 200 + assert resp.body == b_('Hello, World!') + + def test_unicode_child(self): + class ChildController(object): + @expose() + def index(self): + return 'Hello, World!' + + class RootController(object): + pass + setattr(RootController, '🌰', ChildController()) + app = TestApp(Pecan(RootController())) + + resp = app.get('/%F0%9F%8C%B0/') + assert resp.status_int == 200 + assert resp.body == b_('Hello, World!') + + class TestLookups(PecanTestCase): @property diff --git a/pecan/tests/test_no_thread_locals.py b/pecan/tests/test_no_thread_locals.py index 0345391..80aa88f 100644 --- a/pecan/tests/test_no_thread_locals.py +++ b/pecan/tests/test_no_thread_locals.py @@ -1433,7 +1433,7 @@ class TestGeneric(PecanTestCase): uniq = str(time.time()) with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) - r = app.get('/extra/123/456', headers={'X-Unique': uniq}) + r = app.get('/extra/123/456', headers={'X-Unique': uniq}) assert r.status_int == 200 json_resp = loads(r.body.decode()) assert json_resp['first'] == '123' diff --git a/pecan/tests/test_rest.py b/pecan/tests/test_rest.py index 391b7d0..433e424 100644 --- a/pecan/tests/test_rest.py +++ b/pecan/tests/test_rest.py @@ -1,5 +1,14 @@ +# -*- coding: utf-8 -*- + import struct +import sys import warnings + +if sys.version_info < (2, 7): + import unittest2 as unittest # pragma: nocover +else: + import unittest # pragma: nocover + try: from simplejson import dumps, loads except: @@ -1480,3 +1489,68 @@ class TestRestController(PecanTestCase): r = app.delete('/foos/foo/bars/bar') assert r.status_int == 200 assert r.body == b_('DELETE_BAR') + + def test_rest_with_utf8_uri(self): + + class FooController(RestController): + key = chr(0x1F330) if PY3 else unichr(0x1F330) + data = {key: 'Success!'} + + @expose() + def get_one(self, id_): + return self.data[id_] + + @expose() + def get_all(self): + return "Hello, World!" + + @expose() + def put(self, id_, value): + return self.data[id_] + + @expose() + def delete(self, id_): + return self.data[id_] + + class RootController(RestController): + foo = FooController() + + app = TestApp(make_app(RootController())) + + r = app.get('/foo/%F0%9F%8C%B0') + assert r.status_int == 200 + assert r.body == b'Success!' + + r = app.put('/foo/%F0%9F%8C%B0', {'value': 'pecans'}) + assert r.status_int == 200 + assert r.body == b'Success!' + + r = app.delete('/foo/%F0%9F%8C%B0') + assert r.status_int == 200 + assert r.body == b'Success!' + + r = app.get('/foo/') + assert r.status_int == 200 + assert r.body == b'Hello, World!' + + @unittest.skipIf(not PY3, "test is Python3 specific") + def test_rest_with_utf8_endpoint(self): + class ChildController(object): + @expose() + def index(self): + return 'Hello, World!' + + class FooController(RestController): + pass + + # okay, so it's technically a chestnut, but close enough... + setattr(FooController, '🌰', ChildController()) + + class RootController(RestController): + foo = FooController() + + app = TestApp(make_app(RootController())) + + r = app.get('/foo/%F0%9F%8C%B0/') + assert r.status_int == 200 + assert r.body == b'Hello, World!' |