Properly handle Python3 Unicode path segments in pecan routing.

Change-Id: I3890d73a087f7635ddc51b71d3d6f68a41058c42
Closes-Bug: 1451842
This commit is contained in:
Ryan Petrello 2015-05-07 10:45:39 -07:00
parent d907987e70
commit 9a6a893d5e
7 changed files with 132 additions and 13 deletions

View File

@ -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):
'''

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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!'

View File

@ -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]