summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Sánchez <contacto@alsanchez.es>2013-05-03 19:08:39 +0200
committerAlejandro Sánchez <contacto@alsanchez.es>2013-05-03 19:08:39 +0200
commitafb19d7528f2dad00f8ea1190fceda941bf12296 (patch)
tree31d9249b5a1b65339b9cbb76635faa7f61f29b80
parenta8ee2d163ffff24c36c338e87d5e12d8a03ca024 (diff)
downloadroutes-afb19d7528f2dad00f8ea1190fceda941bf12296.tar.gz
Add Python 3 support.
-rw-r--r--.gitignore14
-rw-r--r--routes/mapper.py98
-rw-r--r--routes/route.py11
-rw-r--r--routes/util.py27
-rw-r--r--setup.cfg1
-rw-r--r--setup.py23
-rw-r--r--tests/test_functional/test_middleware.py42
-rw-r--r--tests/test_functional/test_recognition.py3
-rw-r--r--tests/test_functional/test_utils.py2
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
diff --git a/setup.cfg b/setup.cfg
index 7be406d..af8d50e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -12,3 +12,4 @@ cover-html=True
cover-html-dir=html_coverage
#cover-tests=True
cover-package=routes
+py3where=build
diff --git a/setup.py b/setup.py
index d43a593..600dc08 100644
--- a/setup.py
+++ b/setup.py
@@ -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):