0001"""Route and Mapper core classes"""
0002
0003import re
0004import sys
0005import urllib
0006from util import _url_quote as url_quote
0007from util import controller_scan, RouteException
0008from routes import request_config
0009
0010if sys.version < '2.4':
0011 from sets import ImmutableSet as frozenset
0012
0013import threadinglocal
0014
0015def strip_slashes(name):
0016 """Remove slashes from the beginning and end of a part/URL."""
0017 if name.startswith('/'):
0018 name = name[1:]
0019 if name.endswith('/'):
0020 name = name[:-1]
0021 return name
0022
0023class Route(object):
0024 """The Route object holds a route recognition and generation routine.
0025
0026 See Route.__init__ docs for usage.
0027 """
0028
0029 def __init__(self, routepath, **kargs):
0030 """Initialize a route, with a given routepath for matching/generation
0031
0032 The set of keyword args will be used as defaults.
0033
0034 Usage::
0035
0036 >>> from routes.base import Route
0037 >>> newroute = Route(':controller/:action/:id')
0038 >>> newroute.defaults
0039 {'action': 'index', 'id': None}
0040 >>> newroute = Route('date/:year/:month/:day', controller="blog",
0041 ... action="view")
0042 >>> newroute = Route('archives/:page', controller="blog",
0043 ... action="by_page", requirements = { 'page':'\d{1,2}' })
0044 >>> newroute.reqs
0045 {'page': '\\\d{1,2}'}
0046
0047 .. Note::
0048 Route is generally not called directly, a Mapper instance connect
0049 method should be used to add routes.
0050 """
0051
0052 self.routepath = routepath
0053 self.sub_domains = False
0054 self.prior = None
0055 self.encoding = kargs.pop('_encoding', 'utf-8')
0056 self.decode_errors = 'replace'
0057
0058
0059 self.static = kargs.get('_static', False)
0060 self.filter = kargs.pop('_filter', None)
0061 self.absolute = kargs.pop('_absolute', False)
0062
0063
0064
0065 self.member_name = kargs.pop('_member_name', None)
0066 self.collection_name = kargs.pop('_collection_name', None)
0067 self.parent_resource = kargs.pop('_parent_resource', None)
0068
0069
0070 self.conditions = kargs.pop('conditions', None)
0071
0072
0073 self.explicit = kargs.pop('_explicit', False)
0074
0075
0076 reserved_keys = ['requirements']
0077
0078
0079 self.done_chars = ('/', ',', ';', '.', '#')
0080
0081
0082 if routepath.startswith('/'):
0083 routepath = routepath[1:]
0084
0085
0086 self.routelist = routelist = self._pathkeys(routepath)
0087 routekeys = frozenset([key['name'] for key in routelist if isinstance(key, dict)])
0089
0090
0091 self.reqs = kargs.get('requirements', {})
0092 self.req_regs = {}
0093 for key, val in self.reqs.iteritems():
0094 self.req_regs[key] = re.compile('^' + val + '$')
0095
0096
0097 (self.defaults, defaultkeys) = self._defaults(routekeys,
0098 reserved_keys, kargs)
0099
0100 self.maxkeys = defaultkeys | routekeys
0101
0102
0103
0104 (self.minkeys, self.routebackwards) = self._minkeys(routelist[:])
0105
0106
0107
0108 self.hardcoded = frozenset([key for key in self.maxkeys if key not in routekeys and self.defaults[key] is not None])
0110
0111 def _pathkeys(self, routepath):
0112 """Utility function to walk the route, and pull out the valid
0113 dynamic/wildcard keys."""
0114 collecting = False
0115 current = ''
0116 done_on = ''
0117 var_type = ''
0118 just_started = False
0119 routelist = []
0120 for char in routepath:
0121 if char in [':', '*'] and not collecting:
0122 just_started = True
0123 collecting = True
0124 var_type = char
0125 if len(current) > 0:
0126 routelist.append(current)
0127 current = ''
0128 elif collecting and just_started:
0129 just_started = False
0130 if char == '(':
0131 done_on = ')'
0132 else:
0133 current = char
0134 done_on = self.done_chars + ('-',)
0135 elif collecting and char not in done_on:
0136 current += char
0137 elif collecting:
0138 collecting = False
0139 routelist.append(dict(type=var_type, name=current))
0140 if char in self.done_chars:
0141 routelist.append(char)
0142 done_on = var_type = current = ''
0143 else:
0144 current += char
0145 if collecting:
0146 routelist.append(dict(type=var_type, name=current))
0147 elif current:
0148 routelist.append(current)
0149 return routelist
0150
0151 def _minkeys(self, routelist):
0152 """Utility function to walk the route backwards
0153
0154 Will also determine the minimum keys we can handle to generate a
0155 working route.
0156
0157 routelist is a list of the '/' split route path
0158 defaults is a dict of all the defaults provided for the route
0159 """
0160 minkeys = []
0161 backcheck = routelist[:]
0162 gaps = False
0163 backcheck.reverse()
0164 for part in backcheck:
0165 if not isinstance(part, dict) and part not in self.done_chars:
0166 gaps = True
0167 continue
0168 elif not isinstance(part, dict):
0169 continue
0170 key = part['name']
0171 if self.defaults.has_key(key) and not gaps:
0172 continue
0173 minkeys.append(key)
0174 gaps = True
0175 return (frozenset(minkeys), backcheck)
0176
0177 def _defaults(self, routekeys, reserved_keys, kargs):
0178 """Creates default set with values stringified
0179
0180 Put together our list of defaults, stringify non-None values
0181 and add in our action/id default if they use it and didn't specify it
0182
0183 defaultkeys is a list of the currently assumed default keys
0184 routekeys is a list of the keys found in the route path
0185 reserved_keys is a list of keys that are not
0186
0187 """
0188 defaults = {}
0189
0190 if 'controller' not in routekeys and 'controller' not in kargs and not self.explicit:
0192 kargs['controller'] = 'content'
0193 if 'action' not in routekeys and 'action' not in kargs and not self.explicit:
0195 kargs['action'] = 'index'
0196 defaultkeys = frozenset([key for key in kargs.keys() if key not in reserved_keys])
0198 for key in defaultkeys:
0199 if kargs[key] is not None:
0200 defaults[key] = unicode(kargs[key])
0201 else:
0202 defaults[key] = None
0203 if 'action' in routekeys and not defaults.has_key('action') and not self.explicit:
0205 defaults['action'] = 'index'
0206 if 'id' in routekeys and not defaults.has_key('id') and not self.explicit:
0208 defaults['id'] = None
0209 newdefaultkeys = frozenset([key for key in defaults.keys() if key not in reserved_keys])
0211 return (defaults, newdefaultkeys)
0212
0213 def makeregexp(self, clist):
0214 """Create a regular expression for matching purposes
0215
0216 Note: This MUST be called before match can function properly.
0217
0218 clist should be a list of valid controller strings that can be
0219 matched, for this reason makeregexp should be called by the web
0220 framework after it knows all available controllers that can be
0221 utilized.
0222 """
0223 (reg, noreqs, allblank) = self.buildnextreg(self.routelist, clist)
0224
0225 if not reg:
0226 reg = '/'
0227 reg = reg + '(/)?' + '$'
0228 if not reg.startswith('/'):
0229 reg = '/' + reg
0230 reg = '^' + reg
0231
0232 self.regexp = reg
0233 self.regmatch = re.compile(reg)
0234
0235 def buildnextreg(self, path, clist):
0236 """Recursively build our regexp given a path, and a controller list.
0237
0238 Returns the regular expression string, and two booleans that can be
0239 ignored as they're only used internally by buildnextreg.
0240 """
0241 if path:
0242 part = path[0]
0243 else:
0244 part = ''
0245 reg = ''
0246
0247
0248
0249
0250 (rest, noreqs, allblank) = ('', True, True)
0251 if len(path[1:]) > 0:
0252 self.prior = part
0253 (rest, noreqs, allblank) = self.buildnextreg(path[1:], clist)
0254
0255 if isinstance(part, dict) and part['type'] == ':':
0256 var = part['name']
0257 partreg = ''
0258
0259
0260 if self.reqs.has_key(var):
0261 partreg = '(?P<' + var + '>' + self.reqs[var] + ')'
0262 elif var == 'controller':
0263 partreg = '(?P<' + var + '>' + '|'.join(map(re.escape, clist))
0264 partreg += ')'
0265 elif self.prior in ['/', '#']:
0266 partreg = '(?P<' + var + '>[^' + self.prior + ']+?)'
0267 else:
0268 if not rest:
0269 partreg = '(?P<' + var + '>[^%s]+?)' % '/'
0270 else:
0271 end = ''.join(self.done_chars)
0272 rem = rest
0273 if rem[0] == '\\' and len(rem) > 1:
0274 rem = rem[1]
0275 elif rem.startswith('(\\') and len(rem) > 2:
0276 rem = rem[2]
0277 else:
0278 rem = end
0279 rem = frozenset(rem) | frozenset(['/'])
0280 partreg = '(?P<' + var + '>[^%s]+?)' % ''.join(rem)
0281
0282 if self.reqs.has_key(var):
0283 noreqs = False
0284 if not self.defaults.has_key(var):
0285 allblank = False
0286 noreqs = False
0287
0288
0289
0290
0291
0292 if noreqs:
0293
0294
0295
0296
0297 if self.reqs.has_key(var) and self.defaults.has_key(var):
0298 reg = '(' + partreg + rest + ')?'
0299
0300
0301
0302 elif self.reqs.has_key(var):
0303 allblank = False
0304 reg = partreg + rest
0305
0306
0307
0308 elif self.defaults.has_key(var) and self.prior in (',', ';', '.'):
0310 reg = partreg + rest
0311
0312
0313 elif self.defaults.has_key(var):
0314 reg = partreg + '?' + rest
0315
0316
0317
0318 else:
0319 allblank = False
0320 reg = partreg + rest
0321
0322
0323 else:
0324
0325
0326
0327
0328 if allblank and self.defaults.has_key(var):
0329 reg = '(' + partreg + rest + ')?'
0330
0331
0332
0333 else:
0334 reg = partreg + rest
0335 elif isinstance(part, dict) and part['type'] == '*':
0336 var = part['name']
0337 if noreqs:
0338 if self.defaults.has_key(var):
0339 reg = '(?P<' + var + '>.*)' + rest
0340 else:
0341 reg = '(?P<' + var + '>.*)' + rest
0342 allblank = False
0343 noreqs = False
0344 else:
0345 if allblank and self.defaults.has_key(var):
0346 reg = '(?P<' + var + '>.*)' + rest
0347 elif self.defaults.has_key(var):
0348 reg = '(?P<' + var + '>.*)' + rest
0349 else:
0350 allblank = False
0351 noreqs = False
0352 reg = '(?P<' + var + '>.*)' + rest
0353 elif part and part[-1] in self.done_chars:
0354 if allblank:
0355 reg = re.escape(part[:-1]) + '(' + re.escape(part[-1]) + rest
0356 reg += ')?'
0357 else:
0358 allblank = False
0359 reg = re.escape(part) + rest
0360
0361
0362
0363 else:
0364 noreqs = False
0365 allblank = False
0366 reg = re.escape(part) + rest
0367
0368 return (reg, noreqs, allblank)
0369
0370 def match(self, url, environ=None, sub_domains=False,
0371 sub_domains_ignore=None, domain_match=''):
0372 """Match a url to our regexp.
0373
0374 While the regexp might match, this operation isn't
0375 guaranteed as there's other factors that can cause a match to fail
0376 even though the regexp succeeds (Default that was relied on wasn't
0377 given, requirement regexp doesn't pass, etc.).
0378
0379 Therefore the calling function shouldn't assume this will return a
0380 valid dict, the other possible return is False if a match doesn't work
0381 out.
0382 """
0383
0384 if self.static:
0385 return False
0386
0387 if url.endswith('/') and len(url) > 1:
0388 url = url[:-1]
0389 match = self.regmatch.match(url)
0390
0391 if not match:
0392 return False
0393
0394 if not environ:
0395 environ = {}
0396
0397 sub_domain = None
0398
0399 if environ.get('HTTP_HOST') and sub_domains:
0400 host = environ['HTTP_HOST'].split(':')[0]
0401 sub_match = re.compile('^(.+?)\.%s$' % domain_match)
0402 subdomain = re.sub(sub_match, r'\1', host)
0403 if subdomain not in sub_domains_ignore and host != subdomain:
0404 sub_domain = subdomain
0405
0406 if self.conditions:
0407 if self.conditions.has_key('method') and environ.get('REQUEST_METHOD') not in self.conditions['method']:
0409 return False
0410
0411
0412 use_sd = self.conditions.get('sub_domain')
0413 if use_sd and not sub_domain:
0414 return False
0415 if isinstance(use_sd, list) and sub_domain not in use_sd:
0416 return False
0417
0418 matchdict = match.groupdict()
0419 result = {}
0420 extras = frozenset(self.defaults.keys()) - frozenset(matchdict.keys())
0421 for key, val in matchdict.iteritems():
0422 if key != 'path_info' and self.encoding:
0423
0424
0425 try:
0426 val = val and urllib.unquote_plus(val).decode(self.encoding, self.decode_errors)
0427 except UnicodeDecodeError:
0428 return False
0429
0430 if not val and self.defaults.