summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Petrello <lists@ryanpetrello.com>2015-05-07 10:45:39 -0700
committerRyan Petrello <lists@ryanpetrello.com>2015-05-07 11:15:45 -0700
commit9a6a893d5e2f090087be92d301705e35ccf158ba (patch)
tree081c06e89567db76a03e701ac595e4b5cdddf65a
parentd907987e7022025f3ad067d7407532125e6b8e2e (diff)
downloadpecan-9a6a893d5e2f090087be92d301705e35ccf158ba.tar.gz
Properly handle Python3 Unicode path segments in pecan routing.
Change-Id: I3890d73a087f7635ddc51b71d3d6f68a41058c42 Closes-Bug: 1451842
-rw-r--r--pecan/core.py4
-rw-r--r--pecan/rest.py27
-rw-r--r--pecan/routing.py5
-rw-r--r--pecan/tests/test_base.py31
-rw-r--r--pecan/tests/test_no_thread_locals.py2
-rw-r--r--pecan/tests/test_rest.py74
-rw-r--r--tox.ini2
7 files changed, 132 insertions, 13 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!'
diff --git a/tox.ini b/tox.ini
index 74e3088..9a50acd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -170,7 +170,7 @@ commands = tox -e py27 --notest # ensure a virtualenv is built
[testenv:pep8]
deps = pep8
-commands = pep8 --repeat --show-source pecan setup.py
+commands = pep8 --repeat --show-source pecan setup.py --ignore=E402
# Generic environment for running commands like packaging
[testenv:venv]