diff options
-rw-r--r-- | .gitignore | 14 | ||||
-rw-r--r-- | routes/mapper.py | 98 | ||||
-rw-r--r-- | routes/route.py | 11 | ||||
-rw-r--r-- | routes/util.py | 27 | ||||
-rw-r--r-- | setup.cfg | 1 | ||||
-rw-r--r-- | setup.py | 23 | ||||
-rw-r--r-- | tests/test_functional/test_middleware.py | 42 | ||||
-rw-r--r-- | tests/test_functional/test_recognition.py | 3 | ||||
-rw-r--r-- | tests/test_functional/test_utils.py | 2 |
9 files changed, 147 insertions, 74 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17db388 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +syntax:glob +.svn +*.pyc +*.egg-info +*.egg +build +dist +docs/_build +*.xml +html_coverage +.hgignore +.idea +*.iml diff --git a/routes/mapper.py b/routes/mapper.py index 524c177..6382762 100644 --- a/routes/mapper.py +++ b/routes/mapper.py @@ -7,7 +7,7 @@ import pkg_resources from repoze.lru import LRUCache from routes import request_config -from routes.util import controller_scan, MatchException, RoutesException +from routes.util import controller_scan, MatchException, RoutesException, as_unicode from routes.route import Route @@ -742,6 +742,9 @@ class Mapper(SubMapperParent): if val != self: return val + controller = as_unicode(controller, self.encoding) + action = as_unicode(action, self.encoding) + actionlist = self._gendict.get(controller) or self._gendict.get('*', {}) if not actionlist and not args: return None @@ -765,44 +768,60 @@ class Mapper(SubMapperParent): if len(route.minkeys - route.dotkeys - keys) == 0: newlist.append(route) keylist = newlist + + class KeySorter: + + def __init__(self, obj, *args): + self.obj = obj + + def __lt__(self, other): + return self._keysort(self.obj, other.obj) < 0 - def keysort(a, b): - """Sorts two sets of sets, to order them ideally for - matching.""" - am = a.minkeys - a = a.maxkeys - b = b.maxkeys - - lendiffa = len(keys^a) - lendiffb = len(keys^b) - # If they both match, don't switch them - if lendiffa == 0 and lendiffb == 0: - return 0 - - # First, if a matches exactly, use it - if lendiffa == 0: - return -1 - - # Or b matches exactly, use it - if lendiffb == 0: - return 1 - - # Neither matches exactly, return the one with the most in - # common - if cmp(lendiffa, lendiffb) != 0: - return cmp(lendiffa, lendiffb) - - # Neither matches exactly, but if they both have just as much - # in common - if len(keys&b) == len(keys&a): - # Then we return the shortest of the two - return cmp(len(a), len(b)) - - # Otherwise, we return the one that has the most in common - else: - return cmp(len(keys&b), len(keys&a)) + def _keysort(self, a, b): + """Sorts two sets of sets, to order them ideally for + matching.""" + am = a.minkeys + a = a.maxkeys + b = b.maxkeys + + lendiffa = len(keys^a) + lendiffb = len(keys^b) + # If they both match, don't switch them + if lendiffa == 0 and lendiffb == 0: + return 0 + + # First, if a matches exactly, use it + if lendiffa == 0: + return -1 + + # Or b matches exactly, use it + if lendiffb == 0: + return 1 + + # Neither matches exactly, return the one with the most in + # common + if self._compare(lendiffa, lendiffb) != 0: + return self._compare(lendiffa, lendiffb) + + # Neither matches exactly, but if they both have just as much + # in common + if len(keys&b) == len(keys&a): + # Then we return the shortest of the two + return self._compare(len(a), len(b)) + + # Otherwise, we return the one that has the most in common + else: + return self._compare(len(keys&b), len(keys&a)) + + def _compare(self, obj1, obj2): + if obj1 < obj2: + return -1 + elif obj1 < obj2: + return 1 + else: + return 0 - keylist.sort(keysort) + keylist.sort(key=KeySorter) if cacheset: sortcache[cachekey] = keylist @@ -814,10 +833,7 @@ class Mapper(SubMapperParent): kval = kargs.get(key) if not kval: continue - if isinstance(kval, str): - kval = kval.decode(self.encoding) - else: - kval = unicode(kval) + kval = as_unicode(kval, self.encoding) if kval != route.defaults[key] and not callable(route.defaults[key]): fail = True break diff --git a/routes/route.py b/routes/route.py index 688d6e4..13e5efd 100644 --- a/routes/route.py +++ b/routes/route.py @@ -5,7 +5,7 @@ import urllib if sys.version < '2.4': from sets import ImmutableSet as frozenset -from routes.util import _url_quote as url_quote, _str_encode +from routes.util import _url_quote as url_quote, _str_encode, as_unicode class Route(object): @@ -135,7 +135,7 @@ class Route(object): """Transform the given argument into a unicode string.""" if isinstance(s, unicode): return s - elif isinstance(s, str): + elif isinstance(s, bytes): return s.decode(self.encoding) elif callable(s): return s @@ -557,7 +557,7 @@ class Route(object): # change back into python unicode objects from the URL # representation try: - val = val and val.decode(self.encoding, self.decode_errors) + val = as_unicode(val, self.encoding, self.decode_errors) except UnicodeDecodeError: return False @@ -654,6 +654,7 @@ class Route(object): else: return False + val = as_unicode(val, self.encoding) urllist.append(url_quote(val, self.encoding)) if part['type'] == '.': urllist.append('.') @@ -699,7 +700,7 @@ class Route(object): # Verify that if we have a method arg, its in the method accept list. # Also, method will be changed to _method for route generation - meth = kargs.get('method') + meth = as_unicode(kargs.get('method'), self.encoding) if meth: if self.conditions and 'method' in self.conditions \ and meth.upper() not in self.conditions['method']: @@ -731,8 +732,10 @@ class Route(object): val = kargs[key] if isinstance(val, (tuple, list)): for value in val: + value = as_unicode(value, self.encoding) fragments.append((key, _str_encode(value, self.encoding))) else: + val = as_unicode(val, self.encoding) fragments.append((key, _str_encode(val, self.encoding))) if fragments: url += '?' diff --git a/routes/util.py b/routes/util.py index d0d9672..f7b98df 100644 --- a/routes/util.py +++ b/routes/util.py @@ -40,7 +40,7 @@ def _screenargs(kargs, mapper, environ, force_explicit=False): elif mapper.explicit and not force_explicit: return kargs - controller_name = kargs.get('controller') + controller_name = as_unicode(kargs.get('controller'), encoding) if controller_name and controller_name.startswith('/'): # If the controller name starts with '/', ignore route memory @@ -92,6 +92,7 @@ def _subdomain_check(kargs, mapper, environ): port += ':' + hostmatch[1] sub_match = re.compile('^.+?\.(%s)$' % mapper.domain_match) domain = re.sub(sub_match, r'\1', host) + subdomain = as_unicode(subdomain, mapper.encoding) if subdomain and not host.startswith(subdomain) and \ subdomain not in mapper.sub_domains_ignore: kargs['_host'] = subdomain + '.' + domain + port @@ -260,7 +261,7 @@ def url_for(*args, **kargs): if url is not None: url = protocol + '://' + host + url - if not isinstance(url, str) and url is not None: + if not ascii_characters(url) and url is not None: raise GenerationException("url_for can only return a string, got " "unicode instead: %s" % url) if url is None: @@ -411,7 +412,7 @@ class URLGenerator(object): host += '/' url = protocol + '://' + host + url.lstrip('/') - if not isinstance(url, str) and url is not None: + if not ascii_characters(url) and url is not None: raise GenerationException("Can only return a string, got " "unicode instead: %s" % url) if url is None: @@ -496,9 +497,21 @@ def controller_scan(directory=None): controllers.extend(find_controllers(filename, prefix=prefix+fname+'/')) return controllers - def longest_first(fst, lst): - """Compare the length of one string to another, shortest goes first""" - return cmp(len(lst), len(fst)) controllers = find_controllers(directory) - controllers.sort(longest_first) + # Sort by string length, shortest goes first + controllers.sort(key=len, reverse=True) return controllers + +def as_unicode(value, encoding, errors='strict'): + + if value is not None and isinstance(value, bytes): + return value.decode(encoding, errors) + + return value + +def ascii_characters(string): + + if string is None: + return True + + return all(ord(c) < 128 for c in string)
\ No newline at end of file @@ -12,3 +12,4 @@ cover-html=True cover-html-dir=html_coverage #cover-tests=True cover-package=routes +py3where=build @@ -1,13 +1,24 @@ __version__ = '1.13' -import os +import os, sys from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.rst')).read() CHANGES = open(os.path.join(here, 'CHANGELOG.rst')).read() +PY3 = sys.version_info[0] == 3 +extra_options = { + "packages": find_packages(), + } + +if PY3: + extra_options["use_2to3"] = True + if "test" in sys.argv: + for root, directories, files in os.walk("tests"): + for directory in directories: + extra_options["packages"].append(os.path.join(root, directory)) setup(name="Routes", version=__version__, @@ -16,16 +27,21 @@ setup(name="Routes", classifiers=["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", - "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules", + 'Programming Language :: Python', + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3" ], keywords='routes webob dispatch', author="Ben Bangert", author_email="ben@groovie.org", url='http://routes.groovie.org/', license="MIT", - packages=find_packages(), test_suite="nose.collector", include_package_data=True, zip_safe=False, @@ -33,4 +49,5 @@ setup(name="Routes", install_requires=[ "repoze.lru>=0.3" ], + **extra_options ) diff --git a/tests/test_functional/test_middleware.py b/tests/test_functional/test_middleware.py index 6b51901..c0bdefd 100644 --- a/tests/test_functional/test_middleware.py +++ b/tests/test_functional/test_middleware.py @@ -8,7 +8,7 @@ def simple_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) items = route_dict.items() items.sort() - return ['The matchdict items are %s and environ is %s' % (items, environ)] + return [('The matchdict items are %s and environ is %s' % (items, environ)).encode()] def test_basic(): map = Mapper(explicit=False) @@ -17,10 +17,11 @@ def test_basic(): map.create_regs(['content']) app = TestApp(RoutesMiddleware(simple_app, map)) res = app.get('/') - assert 'matchdict items are []' in res + assert b'matchdict items are []' in res res = app.get('/content') - assert "matchdict items are [('action', 'index'), ('controller', u'content'), ('id', None)]" in res + assert b"matchdict items are [('action', 'index'), ('controller', " + repr( + u'content').encode() + b"), ('id', None)]" in res def test_no_query(): map = Mapper(explicit=False) @@ -33,8 +34,8 @@ def test_no_query(): env = {'PATH_INFO': '/', 'REQUEST_METHOD': 'GET', 'HTTP_HOST': 'localhost'} def start_response_wrapper(status, headers, exc=None): pass - response = ''.join(app(env, start_response_wrapper)) - assert 'matchdict items are []' in response + response = b''.join(app(env, start_response_wrapper)) + assert b'matchdict items are []' in response def test_content_split(): map = Mapper(explicit=False) @@ -48,8 +49,8 @@ def test_content_split(): 'HTTP_HOST': 'localhost'} def start_response_wrapper(status, headers, exc=None): pass - response = ''.join(app(env, start_response_wrapper)) - assert 'matchdict items are []' in response + response = b''.join(app(env, start_response_wrapper)) + assert b'matchdict items are []' in response def test_no_singleton(): map = Mapper(explicit=False) @@ -62,15 +63,16 @@ def test_no_singleton(): env = {'PATH_INFO': '/', 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'text/plain;text/html'} def start_response_wrapper(status, headers, exc=None): pass - response = ''.join(app(env, start_response_wrapper)) - assert 'matchdict items are []' in response + response = b''.join(app(env, start_response_wrapper)) + assert b'matchdict items are []' in response # Now a match env = {'PATH_INFO': '/project/fred', 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'text/plain;text/html'} def start_response_wrapper(status, headers, exc=None): pass - response = ''.join(app(env, start_response_wrapper)) - assert "matchdict items are [('action', u'index'), ('controller', u'myapp'), ('path_info', 'fred')]" in response + response = b''.join(app(env, start_response_wrapper)) + assert b"matchdict items are [('action', " + repr(u'index').encode() + \ + b"), ('controller', " + repr(u'myapp').encode() + b"), ('path_info', 'fred')]" in response def test_path_info(): @@ -86,7 +88,8 @@ def test_path_info(): res = app.get('/myapp/some/other/url') print res - assert "matchdict items are [('action', u'index'), ('controller', u'myapp'), ('path_info', 'some/other/url')]" in res + assert b"matchdict items are [('action', " + repr(u'index').encode() + \ + b"), ('controller', " + repr(u'myapp').encode() + b"), ('path_info', 'some/other/url')]" in res assert "'SCRIPT_NAME': '/myapp'" in res assert "'PATH_INFO': '/some/other/url'" in res @@ -113,7 +116,9 @@ def test_redirect_middleware(): res = app.get('/myapp/some/other/url') print res - assert "matchdict items are [('action', u'index'), ('controller', u'myapp'), ('path_info', 'some/other/url')]" in res + assert b"matchdict items are [('action', " + repr(u'index').encode() + \ + b"), ('controller', " + repr(u'myapp').encode() + \ + b"), ('path_info', 'some/other/url')]" in res assert "'SCRIPT_NAME': '/myapp'" in res assert "'PATH_INFO': '/some/other/url'" in res @@ -132,15 +137,20 @@ def test_method_conversion(): assert 'matchdict items are []' in res res = app.get('/content') - assert "matchdict items are [('action', 'index'), ('controller', u'content'), ('id', None)]" in res + assert b"matchdict items are [('action', 'index'), ('controller', " + \ + repr(u'content').encode() + b"), ('id', None)]" in res res = app.get('/content/hopper', params={'_method':'DELETE'}) - assert "matchdict items are [('action', u'index'), ('controller', u'content'), ('type', u'hopper')]" in res + assert b"matchdict items are [('action', " + repr(u'index').encode() + \ + b"), ('controller', " + repr(u'content').encode() + \ + b"), ('type', " + repr(u'hopper').encode() + b")]" in res res = app.post('/content/grind', params={'_method':'DELETE', 'name':'smoth'}, headers={'Content-Type': 'application/x-www-form-urlencoded'}) - assert "matchdict items are [('action', u'index'), ('controller', u'content'), ('type', u'grind')]" in res + assert b"matchdict items are [('action', " + repr(u'index').encode() + \ + b"), ('controller', " + repr(u'content').encode() + \ + b"), ('type', " + repr(u'grind').encode() + b")]" in res assert "'REQUEST_METHOD': 'POST'" in res #res = app.post('/content/grind', diff --git a/tests/test_functional/test_recognition.py b/tests/test_functional/test_recognition.py index 798e678..2ea2016 100644 --- a/tests/test_functional/test_recognition.py +++ b/tests/test_functional/test_recognition.py @@ -37,12 +37,11 @@ class TestRecognition(unittest.TestCase): def test_unicode(self): hoge = u'\u30c6\u30b9\u30c8' # the word test in Japanese - hoge_enc = hoge.encode('utf-8') m = Mapper(explicit=False) m.minimization = True m.connect(':hoge') eq_({'controller': 'content', 'action': 'index', 'hoge': hoge}, - m.match('/' + hoge_enc)) + m.match('/' + hoge)) def test_disabling_unicode(self): hoge = u'\u30c6\u30b9\u30c8' # the word test in Japanese diff --git a/tests/test_functional/test_utils.py b/tests/test_functional/test_utils.py index 808ca1e..dfb1b90 100644 --- a/tests/test_functional/test_utils.py +++ b/tests/test_functional/test_utils.py @@ -49,7 +49,7 @@ class TestUtils(unittest.TestCase): url = URLGenerator(con.mapper, {}) for urlobj in [url_for, url]: def raise_url(): - return urlobj(u'/some/stirng') + return urlobj(u'/some/st\xc3rng') assert_raises(Exception, raise_url) def test_url_for(self): |