Properly handle Python3 Unicode path segments in pecan routing.
Change-Id: I3890d73a087f7635ddc51b71d3d6f68a41058c42 Closes-Bug: 1451842
This commit is contained in:
parent
d907987e70
commit
9a6a893d5e
|
@ -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):
|
||||
'''
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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!'
|
||||
|
|
2
tox.ini
2
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]
|
||||
|
|
Loading…
Reference in New Issue