Skip to content

Commit

Permalink
Port routes to Python 3
Browse files Browse the repository at this point in the history
The code now works on Python 2 and Python 3 without modifications: 2to3
is no more needed. Changes:

* Drop 2to3 from setup.py: the code is now directly compatible with
  Python 2 and Python 3
* Add dependency to six
* Drop support for Python 3.2: remove 3.2 from .travis.yml
* Replace "for key, value in dict.iteritems():" with "for key, value in
  six.iteritems(dict):"
* Use six.moves.urllib to get urllib functions
* Replace unicode() with six.text_type()
* Replace dict.keys() with list(dict.keys())
* Replace "dict.has_key(key)" with "key in dict"
* Remove "py3where=build" from notests section of setup.cfg
* Add parenthesis to print()
* Replace dict.items() with list(dict.items())
  • Loading branch information
vstinner committed Jun 18, 2015
1 parent f30dddc commit 10400ba
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 87 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Expand Up @@ -2,7 +2,6 @@ language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "pypy"
Expand Down
25 changes: 13 additions & 12 deletions routes/mapper.py
Expand Up @@ -3,6 +3,7 @@
import threading

from repoze.lru import LRUCache
import six

from routes import request_config
from routes.util import (
Expand Down Expand Up @@ -153,7 +154,7 @@ def __init__(self, obj, resource_name=None, collection_name=None,
def connect(self, *args, **kwargs):
newkargs = {}
newargs = args
for key, value in self.kwargs.items():
for key, value in six.iteritems(self.kwargs):
if key == 'path_prefix':
if len(args) > 1:
newargs = (args[0], self.kwargs[key] + args[1])
Expand Down Expand Up @@ -547,8 +548,8 @@ def _create_gens(self):
# Setup the lists of all controllers/actions we'll add each route
# to. We include the '*' in the case that a generate contains a
# controller/action that has no hardcodes
controllerlist = controllerlist.keys() + ['*']
actionlist = actionlist.keys() + ['*']
controllerlist = list(controllerlist.keys()) + ['*']
actionlist = list(actionlist.keys()) + ['*']

# Go through our list again, assemble the controllers/actions we'll
# add each route to. If its hardcoded, we only add it to that dict key.
Expand All @@ -562,7 +563,7 @@ def _create_gens(self):
if 'controller' in route.hardcoded:
clist = [route.defaults['controller']]
if 'action' in route.hardcoded:
alist = [unicode(route.defaults['action'])]
alist = [six.text_type(route.defaults['action'])]
for controller in clist:
for action in alist:
actiondict = gendict.setdefault(controller, {})
Expand Down Expand Up @@ -592,7 +593,7 @@ def _create_regs(self, clist=None):
else:
clist = self.controller_scan

for key, val in self.maxkeys.iteritems():
for key, val in six.iteritems(self.maxkeys):
for route in val:
route.makeregexp(clist)

Expand Down Expand Up @@ -758,8 +759,8 @@ def generate(self, *args, **kargs):
# If the URL didn't depend on the SCRIPT_NAME, we'll cache it
# keyed by just by kargs; otherwise we need to cache it with
# both SCRIPT_NAME and kargs:
cache_key = unicode(args).encode('utf8') + \
unicode(kargs).encode('utf8')
cache_key = six.text_type(args).encode('utf8') + \
six.text_type(kargs).encode('utf8')

if self.urlcache is not None:
cache_key_script_name = '%s:%s' % (script_name, cache_key)
Expand All @@ -782,7 +783,7 @@ def generate(self, *args, **kargs):

keys = frozenset(kargs.keys())
cacheset = False
cachekey = unicode(keys)
cachekey = six.text_type(keys)
cachelist = sortcache.get(cachekey)
if args:
keylist = args
Expand Down Expand Up @@ -1047,7 +1048,7 @@ def resource(self, member_name, collection_name, **kwargs):
def swap(dct, newdct):
"""Swap the keys and values in the dict, and uppercase the values
from the dict during the swap."""
for key, val in dct.iteritems():
for key, val in six.iteritems(dct):
newdct.setdefault(val.upper(), []).append(key)
return newdct
collection_methods = swap(collection, {})
Expand Down Expand Up @@ -1088,7 +1089,7 @@ def requirements_for(meth):
return opts

# Add the routes for handling collection methods
for method, lst in collection_methods.iteritems():
for method, lst in six.iteritems(collection_methods):
primary = (method != 'GET' and lst.pop(0)) or None
route_options = requirements_for(method)
for action in lst:
Expand All @@ -1112,7 +1113,7 @@ def requirements_for(meth):
action='index', conditions={'method': ['GET']}, **options)

# Add the routes that deal with new resource methods
for method, lst in new_methods.iteritems():
for method, lst in six.iteritems(new_methods):
route_options = requirements_for(method)
for action in lst:
name = "new_" + member_name
Expand All @@ -1131,7 +1132,7 @@ def requirements_for(meth):
requirements_regexp = '[^\/]+(?<!\\\)'

# Add the routes that deal with member methods of a resource
for method, lst in member_methods.iteritems():
for method, lst in six.iteritems(member_methods):
route_options = requirements_for(method)
route_options['requirements'] = {'id': requirements_regexp}
if method not in ['POST', 'GET', 'any']:
Expand Down
40 changes: 21 additions & 19 deletions routes/route.py
@@ -1,10 +1,12 @@
import re
import sys
import urllib

from six.moves import urllib
if sys.version < '2.4':
from sets import ImmutableSet as frozenset

import six
from six.moves.urllib import parse as urlparse

from routes.util import _url_quote as url_quote, _str_encode, as_unicode


Expand Down Expand Up @@ -87,18 +89,18 @@ def __init__(self, name, routepath, **kargs):
def _setup_route(self):
# Build our routelist, and the keys used in the route
self.routelist = routelist = self._pathkeys(self.routepath)
routekeys = frozenset([key['name'] for key in routelist
if isinstance(key, dict)])
self.dotkeys = frozenset([key['name'] for key in routelist
if isinstance(key, dict) and
key['type'] == '.'])
routekeys = frozenset(key['name'] for key in routelist
if isinstance(key, dict))
self.dotkeys = frozenset(key['name'] for key in routelist
if isinstance(key, dict) and
key['type'] == '.')

if not self.minimization:
self.make_full_route()

# Build a req list with all the regexp requirements for our args
self.req_regs = {}
for key, val in self.reqs.iteritems():
for key, val in six.iteritems(self.reqs):
self.req_regs[key] = re.compile('^' + val + '$')
# Update our defaults and set new default keys if needed. defaults
# needs to be saved
Expand All @@ -114,9 +116,9 @@ def _setup_route(self):

# Populate our hardcoded keys, these are ones that are set and don't
# exist in the route
self.hardcoded = frozenset(
[key for key in self.maxkeys if key not in routekeys and
self.defaults[key] is not None])
self.hardcoded = frozenset(key for key in self.maxkeys
if key not in routekeys
and self.defaults[key] is not None)

# Cache our default keys
self._default_keys = frozenset(self.defaults.keys())
Expand All @@ -134,14 +136,14 @@ def make_full_route(self):

def make_unicode(self, s):
"""Transform the given argument into a unicode string."""
if isinstance(s, unicode):
if isinstance(s, six.text_type):
return s
elif isinstance(s, bytes):
return s.decode(self.encoding)
elif callable(s):
return s
else:
return unicode(s)
return six.text_type(s)

def _pathkeys(self, routepath):
"""Utility function to walk the route, and pull out the valid
Expand Down Expand Up @@ -253,8 +255,8 @@ def _defaults(self, routekeys, reserved_keys, kargs):
if 'action' not in routekeys and 'action' not in kargs \
and not self.explicit:
kargs['action'] = 'index'
defaultkeys = frozenset([key for key in kargs.keys()
if key not in reserved_keys])
defaultkeys = frozenset(key for key in kargs.keys()
if key not in reserved_keys)
for key in defaultkeys:
if kargs[key] is not None:
defaults[key] = self.make_unicode(kargs[key])
Expand All @@ -266,8 +268,8 @@ def _defaults(self, routekeys, reserved_keys, kargs):
if 'id' in routekeys and 'id' not in defaults \
and not self.explicit:
defaults['id'] = None
newdefaultkeys = frozenset([key for key in defaults.keys()
if key not in reserved_keys])
newdefaultkeys = frozenset(key for key in defaults.keys()
if key not in reserved_keys)

return (defaults, newdefaultkeys)

Expand Down Expand Up @@ -554,7 +556,7 @@ def match(self, url, environ=None, sub_domains=False,
matchdict = match.groupdict()
result = {}
extras = self._default_keys - frozenset(matchdict.keys())
for key, val in matchdict.iteritems():
for key, val in six.iteritems(matchdict):
if key != 'path_info' and self.encoding:
# change back into python unicode objects from the URL
# representation
Expand Down Expand Up @@ -745,7 +747,7 @@ def generate(self, _ignore_req_list=False, _append_slash=False, **kargs):
fragments.append((key, _str_encode(val, self.encoding)))
if fragments:
url += '?'
url += urllib.urlencode(fragments)
url += urlparse.urlencode(fragments)
elif _append_slash and not url.endswith('/'):
url += '/'
return url
53 changes: 28 additions & 25 deletions routes/util.py
Expand Up @@ -7,7 +7,10 @@
"""
import os
import re
import urllib

import six
from six.moves import urllib

from routes import request_config


Expand All @@ -31,8 +34,8 @@ def _screenargs(kargs, mapper, environ, force_explicit=False):
"""
# Coerce any unicode args with the encoding
encoding = mapper.encoding
for key, val in kargs.iteritems():
if isinstance(val, unicode):
for key, val in six.iteritems(kargs):
if isinstance(val, six.text_type):
kargs[key] = val.encode(encoding)

if mapper.explicit and mapper.sub_domains and not force_explicit:
Expand All @@ -46,7 +49,7 @@ def _screenargs(kargs, mapper, environ, force_explicit=False):
# If the controller name starts with '/', ignore route memory
kargs['controller'] = kargs['controller'][1:]
return kargs
elif controller_name and not kargs.has_key('action'):
elif controller_name and 'action' not in kargs:
# Fill in an action if we don't have one, but have a controller
kargs['action'] = 'index'

Expand All @@ -57,10 +60,10 @@ def _screenargs(kargs, mapper, environ, force_explicit=False):
memory_kargs = {}

# Remove keys from memory and kargs if kargs has them as None
for key in [key for key in kargs.keys() if kargs[key] is None]:
empty_keys = [key for key, value in six.iteritems(kargs) if value is None]
for key in empty_keys:
del kargs[key]
if memory_kargs.has_key(key):
del memory_kargs[key]
memory_kargs.pop(key, None)

# Merge the new args on top of the memory args
memory_kargs.update(kargs)
Expand All @@ -76,7 +79,7 @@ def _subdomain_check(kargs, mapper, environ):
on the current subdomain or lack therof."""
if mapper.sub_domains:
subdomain = kargs.pop('sub_domain', None)
if isinstance(subdomain, unicode):
if isinstance(subdomain, six.text_type):
subdomain = str(subdomain)

fullhost = environ.get('HTTP_HOST') or environ.get('SERVER_NAME')
Expand Down Expand Up @@ -107,27 +110,27 @@ def _subdomain_check(kargs, mapper, environ):
def _url_quote(string, encoding):
"""A Unicode handling version of urllib.quote."""
if encoding:
if isinstance(string, unicode):
if isinstance(string, six.text_type):
s = string.encode(encoding)
elif isinstance(string, str):
elif isinstance(string, six.text_type):
# assume the encoding is already correct
s = string
else:
s = unicode(string).encode(encoding)
s = six.text_type(string).encode(encoding)
else:
s = str(string)
return urllib.quote(s, '/')
return urllib.parse.quote(s, '/')


def _str_encode(string, encoding):
if encoding:
if isinstance(string, unicode):
if isinstance(string, six.text_type):
s = string.encode(encoding)
elif isinstance(string, str):
elif isinstance(string, six.text_type):
# assume the encoding is already correct
s = string
else:
s = unicode(string).encode(encoding)
s = six.text_type(string).encode(encoding)
return s


Expand Down Expand Up @@ -207,16 +210,16 @@ def url_for(*args, **kargs):
if kargs:
url += '?'
query_args = []
for key, val in kargs.iteritems():
for key, val in six.iteritems(kargs):
if isinstance(val, (list, tuple)):
for value in val:
query_args.append("%s=%s" % (
urllib.quote(unicode(key).encode(encoding)),
urllib.quote(unicode(value).encode(encoding))))
urllib.parse.quote(six.text_type(key).encode(encoding)),
urllib.parse.quote(six.text_type(value).encode(encoding))))
else:
query_args.append("%s=%s" % (
urllib.quote(unicode(key).encode(encoding)),
urllib.quote(unicode(val).encode(encoding))))
urllib.parse.quote(six.text_type(key).encode(encoding)),
urllib.parse.quote(six.text_type(val).encode(encoding))))
url += '&'.join(query_args)
environ = getattr(config, 'environ', {})
if 'wsgiorg.routing_args' not in environ:
Expand Down Expand Up @@ -352,16 +355,16 @@ def __call__(self, *args, **kargs):
if kargs:
url += '?'
query_args = []
for key, val in kargs.iteritems():
for key, val in six.iteritems(kargs):
if isinstance(val, (list, tuple)):
for value in val:
query_args.append("%s=%s" % (
urllib.quote(unicode(key).encode(encoding)),
urllib.quote(unicode(value).encode(encoding))))
urllib.parse.quote(six.text_type(key).encode(encoding)),
urllib.parse.quote(six.text_type(value).encode(encoding))))
else:
query_args.append("%s=%s" % (
urllib.quote(unicode(key).encode(encoding)),
urllib.quote(unicode(val).encode(encoding))))
urllib.parse.quote(six.text_type(key).encode(encoding)),
urllib.parse.quote(six.text_type(val).encode(encoding))))
url += '&'.join(query_args)
if not static:
route_args = []
Expand Down
1 change: 0 additions & 1 deletion setup.cfg
Expand Up @@ -15,4 +15,3 @@ cover-html=True
cover-html-dir=html_coverage
#cover-tests=True
cover-package=routes
py3where=build
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -18,7 +18,6 @@
}

if PY3:
extra_options["use_2to3"] = True
if "test" in sys.argv or "develop" in sys.argv:
for root, directories, files in os.walk("tests"):
for directory in directories:
Expand Down Expand Up @@ -54,6 +53,7 @@
zip_safe=False,
tests_require=['nose', 'webtest', 'webob', 'coverage'],
install_requires=[
"six",
"repoze.lru>=0.3"
],
**extra_options
Expand Down

0 comments on commit 10400ba

Please sign in to comment.