1278 lines
49 KiB
Python
1278 lines
49 KiB
Python
"""Mapper and Sub-Mapper"""
|
|
import collections
|
|
import itertools as it
|
|
import re
|
|
import threading
|
|
|
|
from repoze.lru import LRUCache
|
|
import six
|
|
|
|
from routes import request_config
|
|
from routes.util import (
|
|
controller_scan,
|
|
RoutesException,
|
|
as_unicode
|
|
)
|
|
from routes.route import Route
|
|
|
|
|
|
COLLECTION_ACTIONS = ['index', 'create', 'new']
|
|
MEMBER_ACTIONS = ['show', 'update', 'delete', 'edit']
|
|
|
|
|
|
def strip_slashes(name):
|
|
"""Remove slashes from the beginning and end of a part/URL."""
|
|
if name.startswith('/'):
|
|
name = name[1:]
|
|
if name.endswith('/'):
|
|
name = name[:-1]
|
|
return name
|
|
|
|
|
|
class SubMapperParent(object):
|
|
"""Base class for Mapper and SubMapper, both of which may be the parent
|
|
of SubMapper objects
|
|
"""
|
|
|
|
def submapper(self, **kargs):
|
|
"""Create a partial version of the Mapper with the designated
|
|
options set
|
|
|
|
This results in a :class:`routes.mapper.SubMapper` object.
|
|
|
|
If keyword arguments provided to this method also exist in the
|
|
keyword arguments provided to the submapper, their values will
|
|
be merged with the saved options going first.
|
|
|
|
In addition to :class:`routes.route.Route` arguments, submapper
|
|
can also take a ``path_prefix`` argument which will be
|
|
prepended to the path of all routes that are connected.
|
|
|
|
Example::
|
|
|
|
>>> map = Mapper(controller_scan=None)
|
|
>>> map.connect('home', '/', controller='home', action='splash')
|
|
>>> map.matchlist[0].name == 'home'
|
|
True
|
|
>>> m = map.submapper(controller='home')
|
|
>>> m.connect('index', '/index', action='index')
|
|
>>> map.matchlist[1].name == 'index'
|
|
True
|
|
>>> map.matchlist[1].defaults['controller'] == 'home'
|
|
True
|
|
|
|
Optional ``collection_name`` and ``resource_name`` arguments are
|
|
used in the generation of route names by the ``action`` and
|
|
``link`` methods. These in turn are used by the ``index``,
|
|
``new``, ``create``, ``show``, ``edit``, ``update`` and
|
|
``delete`` methods which may be invoked indirectly by listing
|
|
them in the ``actions`` argument. If the ``formatted`` argument
|
|
is set to ``True`` (the default), generated paths are given the
|
|
suffix '{.format}' which matches or generates an optional format
|
|
extension.
|
|
|
|
Example::
|
|
|
|
>>> from routes.util import url_for
|
|
>>> map = Mapper(controller_scan=None)
|
|
>>> m = map.submapper(path_prefix='/entries', collection_name='entries', resource_name='entry', actions=['index', 'new'])
|
|
>>> url_for('entries') == '/entries'
|
|
True
|
|
>>> url_for('new_entry', format='xml') == '/entries/new.xml'
|
|
True
|
|
|
|
"""
|
|
return SubMapper(self, **kargs)
|
|
|
|
def collection(self, collection_name, resource_name, path_prefix=None,
|
|
member_prefix='/{id}', controller=None,
|
|
collection_actions=COLLECTION_ACTIONS,
|
|
member_actions=MEMBER_ACTIONS, member_options=None,
|
|
**kwargs):
|
|
"""Create a submapper that represents a collection.
|
|
|
|
This results in a :class:`routes.mapper.SubMapper` object, with a
|
|
``member`` property of the same type that represents the collection's
|
|
member resources.
|
|
|
|
Its interface is the same as the ``submapper`` together with
|
|
``member_prefix``, ``member_actions`` and ``member_options``
|
|
which are passed to the ``member`` submapper as ``path_prefix``,
|
|
``actions`` and keyword arguments respectively.
|
|
|
|
Example::
|
|
|
|
>>> from routes.util import url_for
|
|
>>> map = Mapper(controller_scan=None)
|
|
>>> c = map.collection('entries', 'entry')
|
|
>>> c.member.link('ping', method='POST')
|
|
>>> url_for('entries') == '/entries'
|
|
True
|
|
>>> url_for('edit_entry', id=1) == '/entries/1/edit'
|
|
True
|
|
>>> url_for('ping_entry', id=1) == '/entries/1/ping'
|
|
True
|
|
|
|
"""
|
|
if controller is None:
|
|
controller = resource_name or collection_name
|
|
|
|
if path_prefix is None:
|
|
if collection_name is None:
|
|
path_prefix_str = ''
|
|
else:
|
|
path_prefix_str = '/{collection_name}'
|
|
else:
|
|
if collection_name is None:
|
|
path_prefix_str = "{pre}"
|
|
else:
|
|
path_prefix_str = "{pre}/{collection_name}"
|
|
|
|
# generate what will be the path prefix for the collection
|
|
path_prefix = path_prefix_str.format(pre=path_prefix,
|
|
collection_name=collection_name)
|
|
|
|
collection = SubMapper(self, collection_name=collection_name,
|
|
resource_name=resource_name,
|
|
path_prefix=path_prefix, controller=controller,
|
|
actions=collection_actions, **kwargs)
|
|
|
|
collection.member = SubMapper(collection, path_prefix=member_prefix,
|
|
actions=member_actions,
|
|
**(member_options or {}))
|
|
|
|
return collection
|
|
|
|
|
|
class SubMapper(SubMapperParent):
|
|
"""Partial mapper for use with_options"""
|
|
def __init__(self, obj, resource_name=None, collection_name=None,
|
|
actions=None, formatted=None, **kwargs):
|
|
self.kwargs = kwargs
|
|
self.obj = obj
|
|
self.collection_name = collection_name
|
|
self.member = None
|
|
self.resource_name = resource_name \
|
|
or getattr(obj, 'resource_name', None) \
|
|
or kwargs.get('controller', None) \
|
|
or getattr(obj, 'controller', None)
|
|
if formatted is not None:
|
|
self.formatted = formatted
|
|
else:
|
|
self.formatted = getattr(obj, 'formatted', None)
|
|
if self.formatted is None:
|
|
self.formatted = True
|
|
self.add_actions(actions or [], **kwargs)
|
|
|
|
def connect(self, routename, path=None, **kwargs):
|
|
newkargs = {}
|
|
_routename = routename
|
|
_path = path
|
|
for key, value in six.iteritems(self.kwargs):
|
|
if key == 'path_prefix':
|
|
if path is not None:
|
|
# if there's a name_prefix, add it to the route name
|
|
# and if there's a path_prefix
|
|
_path = ''.join((self.kwargs[key], path))
|
|
else:
|
|
_path = ''.join((self.kwargs[key], routename))
|
|
elif key == 'name_prefix':
|
|
if path is not None:
|
|
# if there's a name_prefix, add it to the route name
|
|
# and if there's a path_prefix
|
|
_routename = ''.join((self.kwargs[key], routename))
|
|
else:
|
|
_routename = None
|
|
elif key in kwargs:
|
|
if isinstance(value, dict):
|
|
newkargs[key] = dict(value, **kwargs[key]) # merge dicts
|
|
else:
|
|
# Originally used this form:
|
|
# newkargs[key] = value + kwargs[key]
|
|
# New version avoids the inheritance concatenation issue
|
|
# with submappers. Only prefixes concatenate, everything
|
|
# else overrides in submappers.
|
|
newkargs[key] = kwargs[key]
|
|
else:
|
|
newkargs[key] = self.kwargs[key]
|
|
for key in kwargs:
|
|
if key not in self.kwargs:
|
|
newkargs[key] = kwargs[key]
|
|
|
|
newargs = (_routename, _path)
|
|
return self.obj.connect(*newargs, **newkargs)
|
|
|
|
def link(self, rel=None, name=None, action=None, method='GET',
|
|
formatted=None, **kwargs):
|
|
"""Generates a named route for a subresource.
|
|
|
|
Example::
|
|
|
|
>>> from routes.util import url_for
|
|
>>> map = Mapper(controller_scan=None)
|
|
>>> c = map.collection('entries', 'entry')
|
|
>>> c.link('recent', name='recent_entries')
|
|
>>> c.member.link('ping', method='POST', formatted=True)
|
|
>>> url_for('entries') == '/entries'
|
|
True
|
|
>>> url_for('recent_entries') == '/entries/recent'
|
|
True
|
|
>>> url_for('ping_entry', id=1) == '/entries/1/ping'
|
|
True
|
|
>>> url_for('ping_entry', id=1, format='xml') == '/entries/1/ping.xml'
|
|
True
|
|
|
|
"""
|
|
if formatted or (formatted is None and self.formatted):
|
|
suffix = '{.format}'
|
|
else:
|
|
suffix = ''
|
|
|
|
return self.connect(name or (rel + '_' + self.resource_name),
|
|
'/' + (rel or name) + suffix,
|
|
action=action or rel or name,
|
|
**_kwargs_with_conditions(kwargs, method))
|
|
|
|
def new(self, **kwargs):
|
|
"""Generates the "new" link for a collection submapper."""
|
|
return self.link(rel='new', **kwargs)
|
|
|
|
def edit(self, **kwargs):
|
|
"""Generates the "edit" link for a collection member submapper."""
|
|
return self.link(rel='edit', **kwargs)
|
|
|
|
def action(self, name=None, action=None, method='GET', formatted=None,
|
|
**kwargs):
|
|
"""Generates a named route at the base path of a submapper.
|
|
|
|
Example::
|
|
|
|
>>> from routes import url_for
|
|
>>> map = Mapper(controller_scan=None)
|
|
>>> c = map.submapper(path_prefix='/entries', controller='entry')
|
|
>>> c.action(action='index', name='entries', formatted=True)
|
|
>>> c.action(action='create', method='POST')
|
|
>>> url_for(controller='entry', action='index', method='GET') == '/entries'
|
|
True
|
|
>>> url_for(controller='entry', action='index', method='GET', format='xml') == '/entries.xml'
|
|
True
|
|
>>> url_for(controller='entry', action='create', method='POST') == '/entries'
|
|
True
|
|
|
|
"""
|
|
if formatted or (formatted is None and self.formatted):
|
|
suffix = '{.format}'
|
|
else:
|
|
suffix = ''
|
|
return self.connect(name or (action + '_' + self.resource_name),
|
|
suffix,
|
|
action=action or name,
|
|
**_kwargs_with_conditions(kwargs, method))
|
|
|
|
def index(self, name=None, **kwargs):
|
|
"""Generates the "index" action for a collection submapper."""
|
|
return self.action(name=name or self.collection_name,
|
|
action='index', method='GET', **kwargs)
|
|
|
|
def show(self, name=None, **kwargs):
|
|
"""Generates the "show" action for a collection member submapper."""
|
|
return self.action(name=name or self.resource_name,
|
|
action='show', method='GET', **kwargs)
|
|
|
|
def create(self, **kwargs):
|
|
"""Generates the "create" action for a collection submapper."""
|
|
return self.action(action='create', method='POST', **kwargs)
|
|
|
|
def update(self, **kwargs):
|
|
"""Generates the "update" action for a collection member submapper."""
|
|
return self.action(action='update', method='PUT', **kwargs)
|
|
|
|
def delete(self, **kwargs):
|
|
"""Generates the "delete" action for a collection member submapper."""
|
|
return self.action(action='delete', method='DELETE', **kwargs)
|
|
|
|
def add_actions(self, actions, **kwargs):
|
|
[getattr(self, action)(**kwargs) for action in actions]
|
|
|
|
# Provided for those who prefer using the 'with' syntax in Python 2.5+
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, type, value, tb):
|
|
pass
|
|
|
|
|
|
# Create kwargs with a 'conditions' member generated for the given method
|
|
def _kwargs_with_conditions(kwargs, method):
|
|
if method and 'conditions' not in kwargs:
|
|
newkwargs = kwargs.copy()
|
|
newkwargs['conditions'] = {'method': method}
|
|
return newkwargs
|
|
else:
|
|
return kwargs
|
|
|
|
|
|
class Mapper(SubMapperParent):
|
|
"""Mapper handles URL generation and URL recognition in a web
|
|
application.
|
|
|
|
Mapper is built handling dictionary's. It is assumed that the web
|
|
application will handle the dictionary returned by URL recognition
|
|
to dispatch appropriately.
|
|
|
|
URL generation is done by passing keyword parameters into the
|
|
generate function, a URL is then returned.
|
|
|
|
"""
|
|
def __init__(self, controller_scan=controller_scan, directory=None,
|
|
always_scan=False, register=True, explicit=True):
|
|
"""Create a new Mapper instance
|
|
|
|
All keyword arguments are optional.
|
|
|
|
``controller_scan``
|
|
Function reference that will be used to return a list of
|
|
valid controllers used during URL matching. If
|
|
``directory`` keyword arg is present, it will be passed
|
|
into the function during its call. This option defaults to
|
|
a function that will scan a directory for controllers.
|
|
|
|
Alternatively, a list of controllers or None can be passed
|
|
in which are assumed to be the definitive list of
|
|
controller names valid when matching 'controller'.
|
|
|
|
``directory``
|
|
Passed into controller_scan for the directory to scan. It
|
|
should be an absolute path if using the default
|
|
``controller_scan`` function.
|
|
|
|
``always_scan``
|
|
Whether or not the ``controller_scan`` function should be
|
|
run during every URL match. This is typically a good idea
|
|
during development so the server won't need to be restarted
|
|
anytime a controller is added.
|
|
|
|
``register``
|
|
Boolean used to determine if the Mapper should use
|
|
``request_config`` to register itself as the mapper. Since
|
|
it's done on a thread-local basis, this is typically best
|
|
used during testing though it won't hurt in other cases.
|
|
|
|
``explicit``
|
|
Boolean used to determine if routes should be connected
|
|
with implicit defaults of::
|
|
|
|
{'controller':'content','action':'index','id':None}
|
|
|
|
When set to True, these defaults will not be added to route
|
|
connections and ``url_for`` will not use Route memory.
|
|
|
|
Additional attributes that may be set after mapper
|
|
initialization (ie, map.ATTRIBUTE = 'something'):
|
|
|
|
``encoding``
|
|
Used to indicate alternative encoding/decoding systems to
|
|
use with both incoming URL's, and during Route generation
|
|
when passed a Unicode string. Defaults to 'utf-8'.
|
|
|
|
``decode_errors``
|
|
How to handle errors in the encoding, generally ignoring
|
|
any chars that don't convert should be sufficient. Defaults
|
|
to 'ignore'.
|
|
|
|
``minimization``
|
|
Boolean used to indicate whether or not Routes should
|
|
minimize URL's and the generated URL's, or require every
|
|
part where it appears in the path. Defaults to False.
|
|
|
|
``hardcode_names``
|
|
Whether or not Named Routes result in the default options
|
|
for the route being used *or* if they actually force url
|
|
generation to use the route. Defaults to False.
|
|
|
|
"""
|
|
self.matchlist = []
|
|
self.maxkeys = {}
|
|
self.minkeys = {}
|
|
self.urlcache = LRUCache(1600)
|
|
self._created_regs = False
|
|
self._created_gens = False
|
|
self._master_regexp = None
|
|
self.prefix = None
|
|
self.req_data = threading.local()
|
|
self.directory = directory
|
|
self.always_scan = always_scan
|
|
self.controller_scan = controller_scan
|
|
self._regprefix = None
|
|
self._routenames = {}
|
|
self.debug = False
|
|
self.append_slash = False
|
|
self.sub_domains = False
|
|
self.sub_domains_ignore = []
|
|
self.domain_match = r'[^\.\/]+?\.[^\.\/]+'
|
|
self.explicit = explicit
|
|
self.encoding = 'utf-8'
|
|
self.decode_errors = 'ignore'
|
|
self.hardcode_names = True
|
|
self.minimization = False
|
|
self.create_regs_lock = threading.Lock()
|
|
if register:
|
|
config = request_config()
|
|
config.mapper = self
|
|
|
|
def __str__(self):
|
|
"""Generates a tabular string representation."""
|
|
def format_methods(r):
|
|
if r.conditions:
|
|
method = r.conditions.get('method', '')
|
|
return type(method) is str and method or ', '.join(method)
|
|
else:
|
|
return ''
|
|
|
|
table = [('Route name', 'Methods', 'Path', 'Controller', 'action')] + \
|
|
[(r.name or '', format_methods(r), r.routepath or '',
|
|
r.defaults.get('controller', ''), r.defaults.get('action', ''))
|
|
for r in self.matchlist]
|
|
|
|
widths = [max(len(row[col]) for row in table)
|
|
for col in range(len(table[0]))]
|
|
|
|
return '\n'.join(
|
|
' '.join(row[col].ljust(widths[col])
|
|
for col in range(len(widths)))
|
|
for row in table)
|
|
|
|
def _envget(self):
|
|
try:
|
|
return self.req_data.environ
|
|
except AttributeError:
|
|
return None
|
|
|
|
def _envset(self, env):
|
|
self.req_data.environ = env
|
|
|
|
def _envdel(self):
|
|
del self.req_data.environ
|
|
environ = property(_envget, _envset, _envdel)
|
|
|
|
def extend(self, routes, path_prefix=''):
|
|
"""Extends the mapper routes with a list of Route objects
|
|
|
|
If a path_prefix is provided, all the routes will have their
|
|
path prepended with the path_prefix.
|
|
|
|
Example::
|
|
|
|
>>> map = Mapper(controller_scan=None)
|
|
>>> map.connect('home', '/', controller='home', action='splash')
|
|
>>> map.matchlist[0].name == 'home'
|
|
True
|
|
>>> routes = [Route('index', '/index.htm', controller='home',
|
|
... action='index')]
|
|
>>> map.extend(routes)
|
|
>>> len(map.matchlist) == 2
|
|
True
|
|
>>> map.extend(routes, path_prefix='/subapp')
|
|
>>> len(map.matchlist) == 3
|
|
True
|
|
>>> map.matchlist[2].routepath == '/subapp/index.htm'
|
|
True
|
|
|
|
.. note::
|
|
|
|
This function does not merely extend the mapper with the
|
|
given list of routes, it actually creates new routes with
|
|
identical calling arguments.
|
|
|
|
"""
|
|
for route in routes:
|
|
if path_prefix and route.minimization:
|
|
routepath = '/'.join([path_prefix, route.routepath])
|
|
elif path_prefix:
|
|
routepath = path_prefix + route.routepath
|
|
else:
|
|
routepath = route.routepath
|
|
self.connect(route.name,
|
|
routepath,
|
|
conditions=route.conditions,
|
|
**route._kargs
|
|
)
|
|
|
|
def make_route(self, *args, **kargs):
|
|
"""Make a new Route object
|
|
|
|
A subclass can override this method to use a custom Route class.
|
|
"""
|
|
return Route(*args, **kargs)
|
|
|
|
def connect(self, *args, **kargs):
|
|
"""Create and connect a new Route to the Mapper.
|
|
|
|
Usage:
|
|
|
|
.. code-block:: python
|
|
|
|
m = Mapper()
|
|
m.connect(':controller/:action/:id')
|
|
m.connect('date/:year/:month/:day', controller="blog",
|
|
action="view")
|
|
m.connect('archives/:page', controller="blog", action="by_page",
|
|
requirements = { 'page':'\\d{1,2}' })
|
|
m.connect('category_list', 'archives/category/:section',
|
|
controller='blog', action='category',
|
|
section='home', type='list')
|
|
m.connect('home', '', controller='blog', action='view',
|
|
section='home')
|
|
|
|
"""
|
|
routename = None
|
|
if len(args) > 1:
|
|
routename = args[0]
|
|
else:
|
|
args = (None,) + args
|
|
if '_explicit' not in kargs:
|
|
kargs['_explicit'] = self.explicit
|
|
if '_minimize' not in kargs:
|
|
kargs['_minimize'] = self.minimization
|
|
route = self.make_route(*args, **kargs)
|
|
|
|
# Apply encoding and errors if its not the defaults and the route
|
|
# didn't have one passed in.
|
|
if (self.encoding != 'utf-8' or self.decode_errors != 'ignore') and \
|
|
'_encoding' not in kargs:
|
|
route.encoding = self.encoding
|
|
route.decode_errors = self.decode_errors
|
|
|
|
if not route.static:
|
|
self.matchlist.append(route)
|
|
|
|
if routename:
|
|
self._routenames[routename] = route
|
|
route.name = routename
|
|
if route.static:
|
|
return
|
|
exists = False
|
|
for key in self.maxkeys:
|
|
if key == route.maxkeys:
|
|
self.maxkeys[key].append(route)
|
|
exists = True
|
|
break
|
|
if not exists:
|
|
self.maxkeys[route.maxkeys] = [route]
|
|
self._created_gens = False
|
|
|
|
def _create_gens(self):
|
|
"""Create the generation hashes for route lookups"""
|
|
# Use keys temporailly to assemble the list to avoid excessive
|
|
# list iteration testing with "in"
|
|
controllerlist = {}
|
|
actionlist = {}
|
|
|
|
# Assemble all the hardcoded/defaulted actions/controllers used
|
|
for route in self.matchlist:
|
|
if route.static:
|
|
continue
|
|
if 'controller' in route.defaults:
|
|
controllerlist[route.defaults['controller']] = True
|
|
if 'action' in route.defaults:
|
|
actionlist[route.defaults['action']] = True
|
|
|
|
# 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 = 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.
|
|
# Otherwise we add it to every hardcode since it can be changed.
|
|
gendict = {} # Our generated two-deep hash
|
|
for route in self.matchlist:
|
|
if route.static:
|
|
continue
|
|
clist = controllerlist
|
|
alist = actionlist
|
|
if 'controller' in route.hardcoded:
|
|
clist = [route.defaults['controller']]
|
|
if 'action' in route.hardcoded:
|
|
alist = [six.text_type(route.defaults['action'])]
|
|
for controller in clist:
|
|
for action in alist:
|
|
actiondict = gendict.setdefault(controller, {})
|
|
actiondict.setdefault(action, ([], {}))[0].append(route)
|
|
self._gendict = gendict
|
|
self._created_gens = True
|
|
|
|
def create_regs(self, *args, **kwargs):
|
|
"""Atomically creates regular expressions for all connected
|
|
routes
|
|
"""
|
|
self.create_regs_lock.acquire()
|
|
try:
|
|
self._create_regs(*args, **kwargs)
|
|
finally:
|
|
self.create_regs_lock.release()
|
|
|
|
def _create_regs(self, clist=None):
|
|
"""Creates regular expressions for all connected routes"""
|
|
if clist is None:
|
|
if self.directory:
|
|
clist = self.controller_scan(self.directory)
|
|
elif callable(self.controller_scan):
|
|
clist = self.controller_scan()
|
|
elif not self.controller_scan:
|
|
clist = []
|
|
else:
|
|
clist = self.controller_scan
|
|
|
|
for key, val in six.iteritems(self.maxkeys):
|
|
for route in val:
|
|
route.makeregexp(clist)
|
|
|
|
regexps = []
|
|
prefix2routes = collections.defaultdict(list)
|
|
for route in self.matchlist:
|
|
if not route.static:
|
|
regexps.append(route.makeregexp(clist, include_names=False))
|
|
# Group the routes by static prefix
|
|
prefix = ''.join(it.takewhile(lambda p: isinstance(p, str),
|
|
route.routelist))
|
|
if route.minimization and not prefix.startswith('/'):
|
|
prefix = '/' + prefix
|
|
prefix2routes[prefix.rstrip("/")].append(route)
|
|
self._prefix2routes = prefix2routes
|
|
# Keep track of all possible prefix lengths in decreasing order
|
|
self._prefix_lens = sorted(set(len(p) for p in prefix2routes),
|
|
reverse=True)
|
|
|
|
# Create our regexp to strip the prefix
|
|
if self.prefix:
|
|
self._regprefix = re.compile(self.prefix + '(.*)')
|
|
|
|
# Save the master regexp
|
|
regexp = '|'.join(['(?:%s)' % x for x in regexps])
|
|
self._master_reg = regexp
|
|
try:
|
|
self._master_regexp = re.compile(regexp)
|
|
except OverflowError:
|
|
self._master_regexp = None
|
|
self._created_regs = True
|
|
|
|
def _match(self, url, environ):
|
|
"""Internal Route matcher
|
|
|
|
Matches a URL against a route, and returns a tuple of the match
|
|
dict and the route object if a match is successfull, otherwise
|
|
it returns empty.
|
|
|
|
For internal use only.
|
|
|
|
"""
|
|
if not self._created_regs and self.controller_scan:
|
|
self.create_regs()
|
|
elif not self._created_regs:
|
|
raise RoutesException("You must generate the regular expressions"
|
|
" before matching.")
|
|
|
|
if self.always_scan:
|
|
self.create_regs()
|
|
|
|
matchlog = []
|
|
if self.prefix:
|
|
if re.match(self._regprefix, url):
|
|
url = re.sub(self._regprefix, r'\1', url)
|
|
if not url:
|
|
url = '/'
|
|
else:
|
|
return (None, None, matchlog)
|
|
|
|
environ = environ or self.environ
|
|
sub_domains = self.sub_domains
|
|
sub_domains_ignore = self.sub_domains_ignore
|
|
domain_match = self.domain_match
|
|
debug = self.debug
|
|
|
|
if self._master_regexp is not None:
|
|
# Check to see if its a valid url against the main regexp
|
|
# Done for faster invalid URL elimination
|
|
valid_url = re.match(self._master_regexp, url)
|
|
else:
|
|
# Regex is None due to OverflowError caused by too many routes.
|
|
# This will allow larger projects to work but might increase time
|
|
# spent invalidating URLs in the loop below.
|
|
valid_url = True
|
|
if not valid_url:
|
|
return (None, None, matchlog)
|
|
|
|
matchlist = it.chain.from_iterable(self._prefix2routes.get(url[:prefix_len], ())
|
|
for prefix_len in self._prefix_lens)
|
|
for route in matchlist:
|
|
if route.static:
|
|
if debug:
|
|
matchlog.append(dict(route=route, static=True))
|
|
continue
|
|
match = route.match(url, environ, sub_domains, sub_domains_ignore,
|
|
domain_match)
|
|
if debug:
|
|
matchlog.append(dict(route=route, regexp=bool(match)))
|
|
if isinstance(match, dict) or match:
|
|
return (match, route, matchlog)
|
|
return (None, None, matchlog)
|
|
|
|
def match(self, url=None, environ=None):
|
|
"""Match a URL against against one of the routes contained.
|
|
|
|
Will return None if no valid match is found.
|
|
|
|
.. code-block:: python
|
|
|
|
resultdict = m.match('/joe/sixpack')
|
|
|
|
"""
|
|
if url is None and not environ:
|
|
raise RoutesException('URL or environ must be provided')
|
|
|
|
if url is None:
|
|
url = environ['PATH_INFO']
|
|
|
|
result = self._match(url, environ)
|
|
if self.debug:
|
|
return result[0], result[1], result[2]
|
|
if isinstance(result[0], dict) or result[0]:
|
|
return result[0]
|
|
return None
|
|
|
|
def routematch(self, url=None, environ=None):
|
|
"""Match a URL against against one of the routes contained.
|
|
|
|
Will return None if no valid match is found, otherwise a
|
|
result dict and a route object is returned.
|
|
|
|
.. code-block:: python
|
|
|
|
resultdict, route_obj = m.match('/joe/sixpack')
|
|
|
|
"""
|
|
if url is None and not environ:
|
|
raise RoutesException('URL or environ must be provided')
|
|
|
|
if url is None:
|
|
url = environ['PATH_INFO']
|
|
result = self._match(url, environ)
|
|
if self.debug:
|
|
return result[0], result[1], result[2]
|
|
if isinstance(result[0], dict) or result[0]:
|
|
return result[0], result[1]
|
|
return None
|
|
|
|
def generate(self, *args, **kargs):
|
|
"""Generate a route from a set of keywords
|
|
|
|
Returns the url text, or None if no URL could be generated.
|
|
|
|
.. code-block:: python
|
|
|
|
m.generate(controller='content',action='view',id=10)
|
|
|
|
"""
|
|
# Generate ourself if we haven't already
|
|
if not self._created_gens:
|
|
self._create_gens()
|
|
|
|
if self.append_slash:
|
|
kargs['_append_slash'] = True
|
|
|
|
if not self.explicit:
|
|
if 'controller' not in kargs:
|
|
kargs['controller'] = 'content'
|
|
if 'action' not in kargs:
|
|
kargs['action'] = 'index'
|
|
|
|
environ = kargs.pop('_environ', self.environ) or {}
|
|
if 'SCRIPT_NAME' in environ:
|
|
script_name = environ['SCRIPT_NAME']
|
|
elif self.environ and 'SCRIPT_NAME' in self.environ:
|
|
script_name = self.environ['SCRIPT_NAME']
|
|
else:
|
|
script_name = ""
|
|
controller = kargs.get('controller', None)
|
|
action = kargs.get('action', None)
|
|
|
|
# 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 = six.text_type(args).encode('utf8') + \
|
|
six.text_type(kargs).encode('utf8')
|
|
|
|
if self.urlcache is not None:
|
|
if six.PY3:
|
|
cache_key_script_name = b':'.join((script_name.encode('utf-8'),
|
|
cache_key))
|
|
else:
|
|
cache_key_script_name = '%s:%s' % (script_name, cache_key)
|
|
|
|
# Check the url cache to see if it exists, use it if it does
|
|
val = self.urlcache.get(cache_key_script_name, self)
|
|
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
|
|
(keylist, sortcache) = actionlist.get(action) or \
|
|
actionlist.get('*', (None, {}))
|
|
if not keylist and not args:
|
|
return None
|
|
|
|
keys = frozenset(kargs.keys())
|
|
cacheset = False
|
|
cachekey = six.text_type(keys)
|
|
cachelist = sortcache.get(cachekey)
|
|
if args:
|
|
keylist = args
|
|
elif cachelist:
|
|
keylist = cachelist
|
|
else:
|
|
cacheset = True
|
|
newlist = []
|
|
for route in keylist:
|
|
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(self, a, b):
|
|
"""Sorts two sets of sets, to order them ideally for
|
|
matching."""
|
|
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(key=KeySorter)
|
|
if cacheset:
|
|
sortcache[cachekey] = keylist
|
|
|
|
# Iterate through the keylist of sorted routes (or a single route if
|
|
# it was passed in explicitly for hardcoded named routes)
|
|
for route in keylist:
|
|
fail = False
|
|
for key in route.hardcoded:
|
|
kval = kargs.get(key)
|
|
if not kval:
|
|
continue
|
|
kval = as_unicode(kval, self.encoding)
|
|
if kval != route.defaults[key] and \
|
|
not callable(route.defaults[key]):
|
|
fail = True
|
|
break
|
|
if fail:
|
|
continue
|
|
path = route.generate(**kargs)
|
|
if path:
|
|
if self.prefix:
|
|
path = self.prefix + path
|
|
external_static = route.static and route.external
|
|
if not route.absolute and not external_static:
|
|
path = script_name + path
|
|
key = cache_key_script_name
|
|
else:
|
|
key = cache_key
|
|
if self.urlcache is not None:
|
|
self.urlcache.put(key, str(path))
|
|
return str(path)
|
|
else:
|
|
continue
|
|
return None
|
|
|
|
def resource(self, member_name, collection_name, **kwargs):
|
|
"""Generate routes for a controller resource
|
|
|
|
The member_name name should be the appropriate singular version
|
|
of the resource given your locale and used with members of the
|
|
collection. The collection_name name will be used to refer to
|
|
the resource collection methods and should be a plural version
|
|
of the member_name argument. By default, the member_name name
|
|
will also be assumed to map to a controller you create.
|
|
|
|
The concept of a web resource maps somewhat directly to 'CRUD'
|
|
operations. The overlying things to keep in mind is that
|
|
mapping a resource is about handling creating, viewing, and
|
|
editing that resource.
|
|
|
|
All keyword arguments are optional.
|
|
|
|
``controller``
|
|
If specified in the keyword args, the controller will be
|
|
the actual controller used, but the rest of the naming
|
|
conventions used for the route names and URL paths are
|
|
unchanged.
|
|
|
|
``collection``
|
|
Additional action mappings used to manipulate/view the
|
|
entire set of resources provided by the controller.
|
|
|
|
Example::
|
|
|
|
map.resource('message', 'messages', collection={'rss':'GET'})
|
|
# GET /message/rss (maps to the rss action)
|
|
# also adds named route "rss_message"
|
|
|
|
``member``
|
|
Additional action mappings used to access an individual
|
|
'member' of this controllers resources.
|
|
|
|
Example::
|
|
|
|
map.resource('message', 'messages', member={'mark':'POST'})
|
|
# POST /message/1/mark (maps to the mark action)
|
|
# also adds named route "mark_message"
|
|
|
|
``new``
|
|
Action mappings that involve dealing with a new member in
|
|
the controller resources.
|
|
|
|
Example::
|
|
|
|
map.resource('message', 'messages', new={'preview':'POST'})
|
|
# POST /message/new/preview (maps to the preview action)
|
|
# also adds a url named "preview_new_message"
|
|
|
|
``path_prefix``
|
|
Prepends the URL path for the Route with the path_prefix
|
|
given. This is most useful for cases where you want to mix
|
|
resources or relations between resources.
|
|
|
|
``name_prefix``
|
|
Perpends the route names that are generated with the
|
|
name_prefix given. Combined with the path_prefix option,
|
|
it's easy to generate route names and paths that represent
|
|
resources that are in relations.
|
|
|
|
Example::
|
|
|
|
map.resource('message', 'messages', controller='categories',
|
|
path_prefix='/category/:category_id',
|
|
name_prefix="category_")
|
|
# GET /category/7/message/1
|
|
# has named route "category_message"
|
|
|
|
``requirements``
|
|
|
|
A dictionary that restricts the matching of a
|
|
variable. Can be used when matching variables with path_prefix.
|
|
|
|
Example::
|
|
|
|
map.resource('message', 'messages',
|
|
path_prefix='{project_id}/',
|
|
requirements={"project_id": R"\\d+"})
|
|
# POST /01234/message
|
|
# success, project_id is set to "01234"
|
|
# POST /foo/message
|
|
# 404 not found, won't be matched by this route
|
|
|
|
|
|
``parent_resource``
|
|
A ``dict`` containing information about the parent
|
|
resource, for creating a nested resource. It should contain
|
|
the ``member_name`` and ``collection_name`` of the parent
|
|
resource. This ``dict`` will
|
|
be available via the associated ``Route`` object which can
|
|
be accessed during a request via
|
|
``request.environ['routes.route']``
|
|
|
|
If ``parent_resource`` is supplied and ``path_prefix``
|
|
isn't, ``path_prefix`` will be generated from
|
|
``parent_resource`` as
|
|
"<parent collection name>/:<parent member name>_id".
|
|
|
|
If ``parent_resource`` is supplied and ``name_prefix``
|
|
isn't, ``name_prefix`` will be generated from
|
|
``parent_resource`` as "<parent member name>_".
|
|
|
|
Example::
|
|
|
|
>>> from routes.util import url_for
|
|
>>> m = Mapper()
|
|
>>> m.resource('location', 'locations',
|
|
... parent_resource=dict(member_name='region',
|
|
... collection_name='regions'))
|
|
>>> # path_prefix is "regions/:region_id"
|
|
>>> # name prefix is "region_"
|
|
>>> url_for('region_locations', region_id=13)
|
|
'/regions/13/locations'
|
|
>>> url_for('region_new_location', region_id=13)
|
|
'/regions/13/locations/new'
|
|
>>> url_for('region_location', region_id=13, id=60)
|
|
'/regions/13/locations/60'
|
|
>>> url_for('region_edit_location', region_id=13, id=60)
|
|
'/regions/13/locations/60/edit'
|
|
|
|
Overriding generated ``path_prefix``::
|
|
|
|
>>> m = Mapper()
|
|
>>> m.resource('location', 'locations',
|
|
... parent_resource=dict(member_name='region',
|
|
... collection_name='regions'),
|
|
... path_prefix='areas/:area_id')
|
|
>>> # name prefix is "region_"
|
|
>>> url_for('region_locations', area_id=51)
|
|
'/areas/51/locations'
|
|
|
|
Overriding generated ``name_prefix``::
|
|
|
|
>>> m = Mapper()
|
|
>>> m.resource('location', 'locations',
|
|
... parent_resource=dict(member_name='region',
|
|
... collection_name='regions'),
|
|
... name_prefix='')
|
|
>>> # path_prefix is "regions/:region_id"
|
|
>>> url_for('locations', region_id=51)
|
|
'/regions/51/locations'
|
|
|
|
"""
|
|
collection = kwargs.pop('collection', {})
|
|
member = kwargs.pop('member', {})
|
|
new = kwargs.pop('new', {})
|
|
path_prefix = kwargs.pop('path_prefix', None)
|
|
name_prefix = kwargs.pop('name_prefix', None)
|
|
parent_resource = kwargs.pop('parent_resource', None)
|
|
|
|
# Generate ``path_prefix`` if ``path_prefix`` wasn't specified and
|
|
# ``parent_resource`` was. Likewise for ``name_prefix``. Make sure
|
|
# that ``path_prefix`` and ``name_prefix`` *always* take precedence if
|
|
# they are specified--in particular, we need to be careful when they
|
|
# are explicitly set to "".
|
|
if parent_resource is not None:
|
|
if path_prefix is None:
|
|
path_prefix = '%s/:%s_id' % (parent_resource['collection_name'],
|
|
parent_resource['member_name'])
|
|
if name_prefix is None:
|
|
name_prefix = '%s_' % parent_resource['member_name']
|
|
else:
|
|
if path_prefix is None:
|
|
path_prefix = ''
|
|
if name_prefix is None:
|
|
name_prefix = ''
|
|
|
|
# Ensure the edit and new actions are in and GET
|
|
member['edit'] = 'GET'
|
|
new.update({'new': 'GET'})
|
|
|
|
# Make new dict's based off the old, except the old values become keys,
|
|
# and the old keys become items in a list as the value
|
|
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 six.iteritems(dct):
|
|
newdct.setdefault(val.upper(), []).append(key)
|
|
return newdct
|
|
collection_methods = swap(collection, {})
|
|
member_methods = swap(member, {})
|
|
new_methods = swap(new, {})
|
|
|
|
# Insert create, update, and destroy methods
|
|
collection_methods.setdefault('POST', []).insert(0, 'create')
|
|
member_methods.setdefault('PUT', []).insert(0, 'update')
|
|
member_methods.setdefault('DELETE', []).insert(0, 'delete')
|
|
|
|
# If there's a path prefix option, use it with the controller
|
|
controller = strip_slashes(collection_name)
|
|
path_prefix = strip_slashes(path_prefix)
|
|
path_prefix = '/' + path_prefix
|
|
if path_prefix and path_prefix != '/':
|
|
path = path_prefix + '/' + controller
|
|
else:
|
|
path = '/' + controller
|
|
collection_path = path
|
|
new_path = path + "/new"
|
|
member_path = path + "/:(id)"
|
|
|
|
options = {
|
|
'controller': kwargs.get('controller', controller),
|
|
'_member_name': member_name,
|
|
'_collection_name': collection_name,
|
|
'_parent_resource': parent_resource,
|
|
'_filter': kwargs.get('_filter')
|
|
}
|
|
if 'requirements' in kwargs:
|
|
options['requirements'] = kwargs['requirements']
|
|
|
|
def requirements_for(meth):
|
|
"""Returns a new dict to be used for all route creation as the
|
|
route options"""
|
|
opts = options.copy()
|
|
if method != 'any':
|
|
opts['conditions'] = {'method': [meth.upper()]}
|
|
return opts
|
|
|
|
# Add the routes for handling collection methods
|
|
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:
|
|
route_options['action'] = action
|
|
route_name = "%s%s_%s" % (name_prefix, action, collection_name)
|
|
self.connect("formatted_" + route_name, "%s/%s.:(format)" %
|
|
(collection_path, action), **route_options)
|
|
self.connect(route_name, "%s/%s" % (collection_path, action),
|
|
**route_options)
|
|
if primary:
|
|
route_options['action'] = primary
|
|
self.connect("%s.:(format)" % collection_path, **route_options)
|
|
self.connect(collection_path, **route_options)
|
|
|
|
# Specifically add in the built-in 'index' collection method and its
|
|
# formatted version
|
|
self.connect("formatted_" + name_prefix + collection_name,
|
|
collection_path + ".:(format)", action='index',
|
|
conditions={'method': ['GET']}, **options)
|
|
self.connect(name_prefix + collection_name, collection_path,
|
|
action='index', conditions={'method': ['GET']}, **options)
|
|
|
|
# Add the routes that deal with new resource methods
|
|
for method, lst in six.iteritems(new_methods):
|
|
route_options = requirements_for(method)
|
|
for action in lst:
|
|
name = "new_" + member_name
|
|
route_options['action'] = action
|
|
if action == 'new':
|
|
path = new_path
|
|
formatted_path = new_path + '.:(format)'
|
|
else:
|
|
path = "%s/%s" % (new_path, action)
|
|
name = action + "_" + name
|
|
formatted_path = "%s/%s.:(format)" % (new_path, action)
|
|
self.connect("formatted_" + name_prefix + name, formatted_path,
|
|
**route_options)
|
|
self.connect(name_prefix + name, path, **route_options)
|
|
|
|
requirements_regexp = '[^\\/]+(?<!\\\\)'
|
|
|
|
# Add the routes that deal with member methods of a resource
|
|
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']:
|
|
primary = lst.pop(0)
|
|
else:
|
|
primary = None
|
|
for action in lst:
|
|
route_options['action'] = action
|
|
self.connect("formatted_%s%s_%s" % (name_prefix, action,
|
|
member_name),
|
|
"%s/%s.:(format)" % (member_path, action),
|
|
**route_options)
|
|
self.connect("%s%s_%s" % (name_prefix, action, member_name),
|
|
"%s/%s" % (member_path, action), **route_options)
|
|
if primary:
|
|
route_options['action'] = primary
|
|
self.connect("%s.:(format)" % member_path, **route_options)
|
|
self.connect(member_path, **route_options)
|
|
|
|
# Specifically add the member 'show' method
|
|
route_options = requirements_for('GET')
|
|
route_options['action'] = 'show'
|
|
route_options['requirements'] = {'id': requirements_regexp}
|
|
self.connect("formatted_" + name_prefix + member_name,
|
|
member_path + ".:(format)", **route_options)
|
|
self.connect(name_prefix + member_name, member_path, **route_options)
|
|
|
|
def redirect(self, match_path, destination_path, *args, **kwargs):
|
|
"""Add a redirect route to the mapper
|
|
|
|
Redirect routes bypass the wrapped WSGI application and instead
|
|
result in a redirect being issued by the RoutesMiddleware. As
|
|
such, this method is only meaningful when using
|
|
RoutesMiddleware.
|
|
|
|
By default, a 302 Found status code is used, this can be
|
|
changed by providing a ``_redirect_code`` keyword argument
|
|
which will then be used instead. Note that the entire status
|
|
code string needs to be present.
|
|
|
|
When using keyword arguments, all arguments that apply to
|
|
matching will be used for the match, while generation specific
|
|
options will be used during generation. Thus all options
|
|
normally available to connected Routes may be used with
|
|
redirect routes as well.
|
|
|
|
Example::
|
|
|
|
map = Mapper()
|
|
map.redirect('/legacyapp/archives/{url:.*}', '/archives/{url}')
|
|
map.redirect('/home/index', '/',
|
|
_redirect_code='301 Moved Permanently')
|
|
|
|
"""
|
|
both_args = ['_encoding', '_explicit', '_minimize']
|
|
gen_args = ['_filter']
|
|
|
|
status_code = kwargs.pop('_redirect_code', '302 Found')
|
|
gen_dict, match_dict = {}, {}
|
|
|
|
# Create the dict of args for the generation route
|
|
for key in both_args + gen_args:
|
|
if key in kwargs:
|
|
gen_dict[key] = kwargs[key]
|
|
gen_dict['_static'] = True
|
|
|
|
# Create the dict of args for the matching route
|
|
for key in kwargs:
|
|
if key not in gen_args:
|
|
match_dict[key] = kwargs[key]
|
|
|
|
self.connect(match_path, **match_dict)
|
|
match_route = self.matchlist[-1]
|
|
|
|
self.connect('_redirect_%s' % id(match_route), destination_path,
|
|
**gen_dict)
|
|
match_route.redirect = True
|
|
match_route.redirect_status = status_code
|