| 1 | import re |
|---|
| 2 | import sys |
|---|
| 3 | import urllib |
|---|
| 4 | |
|---|
| 5 | if sys.version < '2.4': |
|---|
| 6 | from sets import ImmutableSet as frozenset |
|---|
| 7 | |
|---|
| 8 | from routes.util import _url_quote as url_quote, _str_encode |
|---|
| 9 | |
|---|
| 10 | |
|---|
| 11 | class Route(object): |
|---|
| 12 | """The Route object holds a route recognition and generation |
|---|
| 13 | routine. |
|---|
| 14 | |
|---|
| 15 | See Route.__init__ docs for usage. |
|---|
| 16 | |
|---|
| 17 | """ |
|---|
| 18 | # reserved keys that don't count |
|---|
| 19 | reserved_keys = ['requirements'] |
|---|
| 20 | |
|---|
| 21 | # special chars to indicate a natural split in the URL |
|---|
| 22 | done_chars = ('/', ',', ';', '.', '#') |
|---|
| 23 | |
|---|
| 24 | def __init__(self, name, routepath, **kargs): |
|---|
| 25 | """Initialize a route, with a given routepath for |
|---|
| 26 | matching/generation |
|---|
| 27 | |
|---|
| 28 | The set of keyword args will be used as defaults. |
|---|
| 29 | |
|---|
| 30 | Usage:: |
|---|
| 31 | |
|---|
| 32 | >>> from routes.base import Route |
|---|
| 33 | >>> newroute = Route(None, ':controller/:action/:id') |
|---|
| 34 | >>> sorted(newroute.defaults.items()) |
|---|
| 35 | [('action', 'index'), ('id', None)] |
|---|
| 36 | >>> newroute = Route(None, 'date/:year/:month/:day', |
|---|
| 37 | ... controller="blog", action="view") |
|---|
| 38 | >>> newroute = Route(None, 'archives/:page', controller="blog", |
|---|
| 39 | ... action="by_page", requirements = { 'page':'\d{1,2}' }) |
|---|
| 40 | >>> newroute.reqs |
|---|
| 41 | {'page': '\\\d{1,2}'} |
|---|
| 42 | |
|---|
| 43 | .. Note:: |
|---|
| 44 | Route is generally not called directly, a Mapper instance |
|---|
| 45 | connect method should be used to add routes. |
|---|
| 46 | |
|---|
| 47 | """ |
|---|
| 48 | self.routepath = routepath |
|---|
| 49 | self.sub_domains = False |
|---|
| 50 | self.prior = None |
|---|
| 51 | self.redirect = False |
|---|
| 52 | self.name = name |
|---|
| 53 | self._kargs = kargs |
|---|
| 54 | self.minimization = kargs.pop('_minimize', False) |
|---|
| 55 | self.encoding = kargs.pop('_encoding', 'utf-8') |
|---|
| 56 | self.reqs = kargs.get('requirements', {}) |
|---|
| 57 | self.decode_errors = 'replace' |
|---|
| 58 | |
|---|
| 59 | # Don't bother forming stuff we don't need if its a static route |
|---|
| 60 | self.static = kargs.pop('_static', False) |
|---|
| 61 | self.filter = kargs.pop('_filter', None) |
|---|
| 62 | self.absolute = kargs.pop('_absolute', False) |
|---|
| 63 | |
|---|
| 64 | # Pull out the member/collection name if present, this applies only to |
|---|
| 65 | # map.resource |
|---|
| 66 | self.member_name = kargs.pop('_member_name', None) |
|---|
| 67 | self.collection_name = kargs.pop('_collection_name', None) |
|---|
| 68 | self.parent_resource = kargs.pop('_parent_resource', None) |
|---|
| 69 | |
|---|
| 70 | # Pull out route conditions |
|---|
| 71 | self.conditions = kargs.pop('conditions', None) |
|---|
| 72 | |
|---|
| 73 | # Determine if explicit behavior should be used |
|---|
| 74 | self.explicit = kargs.pop('_explicit', False) |
|---|
| 75 | |
|---|
| 76 | # Since static need to be generated exactly, treat them as |
|---|
| 77 | # non-minimized |
|---|
| 78 | if self.static: |
|---|
| 79 | self.external = '://' in self.routepath |
|---|
| 80 | self.minimization = False |
|---|
| 81 | |
|---|
| 82 | # Strip preceding '/' if present, and not minimizing |
|---|
| 83 | if routepath.startswith('/') and self.minimization: |
|---|
| 84 | self.routepath = routepath[1:] |
|---|
| 85 | self._setup_route() |
|---|
| 86 | |
|---|
| 87 | def _setup_route(self): |
|---|
| 88 | # Build our routelist, and the keys used in the route |
|---|
| 89 | self.routelist = routelist = self._pathkeys(self.routepath) |
|---|
| 90 | routekeys = frozenset([key['name'] for key in routelist |
|---|
| 91 | if isinstance(key, dict)]) |
|---|
| 92 | self.dotkeys = frozenset([key['name'] for key in routelist |
|---|
| 93 | if isinstance(key, dict) and |
|---|
| 94 | key['type'] == '.']) |
|---|
| 95 | |
|---|
| 96 | if not self.minimization: |
|---|
| 97 | self.make_full_route() |
|---|
| 98 | |
|---|
| 99 | # Build a req list with all the regexp requirements for our args |
|---|
| 100 | self.req_regs = {} |
|---|
| 101 | for key, val in self.reqs.iteritems(): |
|---|
| 102 | self.req_regs[key] = re.compile('^' + val + '$') |
|---|
| 103 | # Update our defaults and set new default keys if needed. defaults |
|---|
| 104 | # needs to be saved |
|---|
| 105 | (self.defaults, defaultkeys) = self._defaults(routekeys, |
|---|
| 106 | self.reserved_keys, |
|---|
| 107 | self._kargs.copy()) |
|---|
| 108 | # Save the maximum keys we could utilize |
|---|
| 109 | self.maxkeys = defaultkeys | routekeys |
|---|
| 110 | |
|---|
| 111 | # Populate our minimum keys, and save a copy of our backward keys for |
|---|
| 112 | # quicker generation later |
|---|
| 113 | (self.minkeys, self.routebackwards) = self._minkeys(routelist[:]) |
|---|
| 114 | |
|---|
| 115 | # Populate our hardcoded keys, these are ones that are set and don't |
|---|
| 116 | # exist in the route |
|---|
| 117 | self.hardcoded = frozenset([key for key in self.maxkeys \ |
|---|
| 118 | if key not in routekeys and self.defaults[key] is not None]) |
|---|
| 119 | |
|---|
| 120 | # Cache our default keys |
|---|
| 121 | self._default_keys = frozenset(self.defaults.keys()) |
|---|
| 122 | |
|---|
| 123 | def make_full_route(self): |
|---|
| 124 | """Make a full routelist string for use with non-minimized |
|---|
| 125 | generation""" |
|---|
| 126 | regpath = '' |
|---|
| 127 | for part in self.routelist: |
|---|
| 128 | if isinstance(part, dict): |
|---|
| 129 | regpath += '%(' + part['name'] + ')s' |
|---|
| 130 | else: |
|---|
| 131 | regpath += part |
|---|
| 132 | self.regpath = regpath |
|---|
| 133 | |
|---|
| 134 | def make_unicode(self, s): |
|---|
| 135 | """Transform the given argument into a unicode string.""" |
|---|
| 136 | if isinstance(s, unicode): |
|---|
| 137 | return s |
|---|
| 138 | elif isinstance(s, str): |
|---|
| 139 | return s.decode(self.encoding) |
|---|
| 140 | elif callable(s): |
|---|
| 141 | return s |
|---|
| 142 | else: |
|---|
| 143 | return unicode(s) |
|---|
| 144 | |
|---|
| 145 | def _pathkeys(self, routepath): |
|---|
| 146 | """Utility function to walk the route, and pull out the valid |
|---|
| 147 | dynamic/wildcard keys.""" |
|---|
| 148 | collecting = False |
|---|
| 149 | current = '' |
|---|
| 150 | done_on = '' |
|---|
| 151 | var_type = '' |
|---|
| 152 | just_started = False |
|---|
| 153 | routelist = [] |
|---|
| 154 | for char in routepath: |
|---|
| 155 | if char in [':', '*', '{'] and not collecting and not self.static \ |
|---|
| 156 | or char in ['{'] and not collecting: |
|---|
| 157 | just_started = True |
|---|
| 158 | collecting = True |
|---|
| 159 | var_type = char |
|---|
| 160 | if char == '{': |
|---|
| 161 | done_on = '}' |
|---|
| 162 | just_started = False |
|---|
| 163 | if len(current) > 0: |
|---|
| 164 | routelist.append(current) |
|---|
| 165 | current = '' |
|---|
| 166 | elif collecting and just_started: |
|---|
| 167 | just_started = False |
|---|
| 168 | if char == '(': |
|---|
| 169 | done_on = ')' |
|---|
| 170 | else: |
|---|
| 171 | current = char |
|---|
| 172 | done_on = self.done_chars + ('-',) |
|---|
| 173 | elif collecting and char not in done_on: |
|---|
| 174 | current += char |
|---|
| 175 | elif collecting: |
|---|
| 176 | collecting = False |
|---|
| 177 | if var_type == '{': |
|---|
| 178 | if current[0] == '.': |
|---|
| 179 | var_type = '.' |
|---|
| 180 | current = current[1:] |
|---|
| 181 | else: |
|---|
| 182 | var_type = ':' |
|---|
| 183 | opts = current.split(':') |
|---|
| 184 | if len(opts) > 1: |
|---|
| 185 | current = opts[0] |
|---|
| 186 | self.reqs[current] = opts[1] |
|---|
| 187 | routelist.append(dict(type=var_type, name=current)) |
|---|
| 188 | if char in self.done_chars: |
|---|
| 189 | routelist.append(char) |
|---|
| 190 | done_on = var_type = current = '' |
|---|
| 191 | else: |
|---|
| 192 | current += char |
|---|
| 193 | if collecting: |
|---|
| 194 | routelist.append(dict(type=var_type, name=current)) |
|---|
| 195 | elif current: |
|---|
| 196 | routelist.append(current) |
|---|
| 197 | return routelist |
|---|
| 198 | |
|---|
| 199 | def _minkeys(self, routelist): |
|---|
| 200 | """Utility function to walk the route backwards |
|---|
| 201 | |
|---|
| 202 | Will also determine the minimum keys we can handle to generate |
|---|
| 203 | a working route. |
|---|
| 204 | |
|---|
| 205 | routelist is a list of the '/' split route path |
|---|
| 206 | defaults is a dict of all the defaults provided for the route |
|---|
| 207 | |
|---|
| 208 | """ |
|---|
| 209 | minkeys = [] |
|---|
| 210 | backcheck = routelist[:] |
|---|
| 211 | |
|---|
| 212 | # If we don't honor minimization, we need all the keys in the |
|---|
| 213 | # route path |
|---|
| 214 | if not self.minimization: |
|---|
| 215 | for part in backcheck: |
|---|
| 216 | if isinstance(part, dict): |
|---|
| 217 | minkeys.append(part['name']) |
|---|
| 218 | return (frozenset(minkeys), backcheck) |
|---|
| 219 | |
|---|
| 220 | gaps = False |
|---|
| 221 | backcheck.reverse() |
|---|
| 222 | for part in backcheck: |
|---|
| 223 | if not isinstance(part, dict) and part not in self.done_chars: |
|---|
| 224 | gaps = True |
|---|
| 225 | continue |
|---|
| 226 | elif not isinstance(part, dict): |
|---|
| 227 | continue |
|---|
| 228 | key = part['name'] |
|---|
| 229 | if self.defaults.has_key(key) and not gaps: |
|---|
| 230 | continue |
|---|
| 231 | minkeys.append(key) |
|---|
| 232 | gaps = True |
|---|
| 233 | return (frozenset(minkeys), backcheck) |
|---|
| 234 | |
|---|
| 235 | def _defaults(self, routekeys, reserved_keys, kargs): |
|---|
| 236 | """Creates default set with values stringified |
|---|
| 237 | |
|---|
| 238 | Put together our list of defaults, stringify non-None values |
|---|
| 239 | and add in our action/id default if they use it and didn't |
|---|
| 240 | specify it. |
|---|
| 241 | |
|---|
| 242 | defaultkeys is a list of the currently assumed default keys |
|---|
| 243 | routekeys is a list of the keys found in the route path |
|---|
| 244 | reserved_keys is a list of keys that are not |
|---|
| 245 | |
|---|
| 246 | """ |
|---|
| 247 | defaults = {} |
|---|
| 248 | # Add in a controller/action default if they don't exist |
|---|
| 249 | if 'controller' not in routekeys and 'controller' not in kargs \ |
|---|
| 250 | and not self.explicit: |
|---|
| 251 | kargs['controller'] = 'content' |
|---|
| 252 | if 'action' not in routekeys and 'action' not in kargs \ |
|---|
| 253 | and not self.explicit: |
|---|
| 254 | kargs['action'] = 'index' |
|---|
| 255 | defaultkeys = frozenset([key for key in kargs.keys() \ |
|---|
| 256 | if key not in reserved_keys]) |
|---|
| 257 | for key in defaultkeys: |
|---|
| 258 | if kargs[key] is not None: |
|---|
| 259 | defaults[key] = self.make_unicode(kargs[key]) |
|---|
| 260 | else: |
|---|
| 261 | defaults[key] = None |
|---|
| 262 | if 'action' in routekeys and not defaults.has_key('action') \ |
|---|
| 263 | and not self.explicit: |
|---|
| 264 | defaults['action'] = 'index' |
|---|
| 265 | if 'id' in routekeys and not defaults.has_key('id') \ |
|---|
| 266 | and not self.explicit: |
|---|
| 267 | defaults['id'] = None |
|---|
| 268 | newdefaultkeys = frozenset([key for key in defaults.keys() \ |
|---|
| 269 | if key not in reserved_keys]) |
|---|
| 270 | |
|---|
| 271 | return (defaults, newdefaultkeys) |
|---|
| 272 | |
|---|
| 273 | def makeregexp(self, clist, include_names=True): |
|---|
| 274 | """Create a regular expression for matching purposes |
|---|
| 275 | |
|---|
| 276 | Note: This MUST be called before match can function properly. |
|---|
| 277 | |
|---|
| 278 | clist should be a list of valid controller strings that can be |
|---|
| 279 | matched, for this reason makeregexp should be called by the web |
|---|
| 280 | framework after it knows all available controllers that can be |
|---|
| 281 | utilized. |
|---|
| 282 | |
|---|
| 283 | include_names indicates whether this should be a match regexp |
|---|
| 284 | assigned to itself using regexp grouping names, or if names |
|---|
| 285 | should be excluded for use in a single larger regexp to |
|---|
| 286 | determine if any routes match |
|---|
| 287 | |
|---|
| 288 | """ |
|---|
| 289 | if self.minimization: |
|---|
| 290 | reg = self.buildnextreg(self.routelist, clist, include_names)[0] |
|---|
| 291 | if not reg: |
|---|
| 292 | reg = '/' |
|---|
| 293 | reg = reg + '/?' + '$' |
|---|
| 294 | |
|---|
| 295 | if not reg.startswith('/'): |
|---|
| 296 | reg = '/' + reg |
|---|
| 297 | else: |
|---|
| 298 | reg = self.buildfullreg(clist, include_names) |
|---|
| 299 | |
|---|
| 300 | reg = '^' + reg |
|---|
| 301 | |
|---|
| 302 | if not include_names: |
|---|
| 303 | return reg |
|---|
| 304 | |
|---|
| 305 | self.regexp = reg |
|---|
| 306 | self.regmatch = re.compile(reg) |
|---|
| 307 | |
|---|
| 308 | def buildfullreg(self, clist, include_names=True): |
|---|
| 309 | """Build the regexp by iterating through the routelist and |
|---|
| 310 | replacing dicts with the appropriate regexp match""" |
|---|
| 311 | regparts = [] |
|---|
| 312 | for part in self.routelist: |
|---|
| 313 | if isinstance(part, dict): |
|---|
| 314 | var = part['name'] |
|---|
| 315 | if var == 'controller': |
|---|
| 316 | partmatch = '|'.join(map(re.escape, clist)) |
|---|
| 317 | elif part['type'] == ':': |
|---|
| 318 | partmatch = self.reqs.get(var) or '[^/]+?' |
|---|
| 319 | elif part['type'] == '.': |
|---|
| 320 | partmatch = self.reqs.get(var) or '[^/.]+?' |
|---|
| 321 | else: |
|---|
| 322 | partmatch = self.reqs.get(var) or '.+?' |
|---|
| 323 | if include_names: |
|---|
| 324 | regpart = '(?P<%s>%s)' % (var, partmatch) |
|---|
| 325 | else: |
|---|
| 326 | regpart = '(?:%s)' % partmatch |
|---|
| 327 | if part['type'] == '.': |
|---|
| 328 | regparts.append('(?:\.%s)??' % regpart) |
|---|
| 329 | else: |
|---|
| 330 | regparts.append(regpart) |
|---|
| 331 | else: |
|---|
| 332 | regparts.append(re.escape(part)) |
|---|
| 333 | regexp = ''.join(regparts) + '$' |
|---|
| 334 | return regexp |
|---|
| 335 | |
|---|
| 336 | def buildnextreg(self, path, clist, include_names=True): |
|---|
| 337 | """Recursively build our regexp given a path, and a controller |
|---|
| 338 | list. |
|---|
| 339 | |
|---|
| 340 | Returns the regular expression string, and two booleans that |
|---|
| 341 | can be ignored as they're only used internally by buildnextreg. |
|---|
| 342 | |
|---|
| 343 | """ |
|---|
| 344 | if path: |
|---|
| 345 | part = path[0] |
|---|
| 346 | else: |
|---|
| 347 | part = '' |
|---|
| 348 | reg = '' |
|---|
| 349 | |
|---|
| 350 | # noreqs will remember whether the remainder has either a string |
|---|
| 351 | # match, or a non-defaulted regexp match on a key, allblank remembers |
|---|
| 352 | # if the rest could possible be completely empty |
|---|
| 353 | (rest, noreqs, allblank) = ('', True, True) |
|---|
| 354 | if len(path[1:]) > 0: |
|---|
| 355 | self.prior = part |
|---|
| 356 | (rest, noreqs, allblank) = self.buildnextreg(path[1:], clist, include_names) |
|---|
| 357 | |
|---|
| 358 | if isinstance(part, dict) and part['type'] in (':', '.'): |
|---|
| 359 | var = part['name'] |
|---|
| 360 | typ = part['type'] |
|---|
| 361 | partreg = '' |
|---|
| 362 | |
|---|
| 363 | # First we plug in the proper part matcher |
|---|
| 364 | if self.reqs.has_key(var): |
|---|
| 365 | if include_names: |
|---|
| 366 | partreg = '(?P<%s>%s)' % (var, self.reqs[var]) |
|---|
| 367 | else: |
|---|
| 368 | partreg = '(?:%s)' % self.reqs[var] |
|---|
| 369 | if typ == '.': |
|---|
| 370 | partreg = '(?:\.%s)??' % partreg |
|---|
| 371 | elif var == 'controller': |
|---|
| 372 | if include_names: |
|---|
| 373 | partreg = '(?P<%s>%s)' % (var, '|'.join(map(re.escape, clist))) |
|---|
| 374 | else: |
|---|
| 375 | partreg = '(?:%s)' % '|'.join(map(re.escape, clist)) |
|---|
| 376 | elif self.prior in ['/', '#']: |
|---|
| 377 | if include_names: |
|---|
| 378 | partreg = '(?P<' + var + '>[^' + self.prior + ']+?)' |
|---|
| 379 | else: |
|---|
| 380 | partreg = '(?:[^' + self.prior + ']+?)' |
|---|
| 381 | else: |
|---|
| 382 | if not rest: |
|---|
| 383 | if typ == '.': |
|---|
| 384 | exclude_chars = '/.' |
|---|
| 385 | else: |
|---|
| 386 | exclude_chars = '/' |
|---|
| 387 | if include_names: |
|---|
| 388 | partreg = '(?P<%s>[^%s]+?)' % (var, exclude_chars) |
|---|
| 389 | else: |
|---|
| 390 | partreg = '(?:[^%s]+?)' % exclude_chars |
|---|
| 391 | if typ == '.': |
|---|
| 392 | partreg = '(?:\.%s)??' % partreg |
|---|
| 393 | else: |
|---|
| 394 | end = ''.join(self.done_chars) |
|---|
| 395 | rem = rest |
|---|
| 396 | if rem[0] == '\\' and len(rem) > 1: |
|---|
| 397 | rem = rem[1] |
|---|
| 398 | elif rem.startswith('(\\') and len(rem) > 2: |
|---|
| 399 | rem = rem[2] |
|---|
| 400 | else: |
|---|
| 401 | rem = end |
|---|
| 402 | rem = frozenset(rem) | frozenset(['/']) |
|---|
| 403 | if include_names: |
|---|
| 404 | partreg = '(?P<%s>[^%s]+?)' % (var, ''.join(rem)) |
|---|
| 405 | else: |
|---|
| 406 | partreg = '(?:[^%s]+?)' % ''.join(rem) |
|---|
| 407 | |
|---|
| 408 | if self.reqs.has_key(var): |
|---|
| 409 | noreqs = False |
|---|
| 410 | if not self.defaults.has_key(var): |
|---|
| 411 | allblank = False |
|---|
| 412 | noreqs = False |
|---|
| 413 | |
|---|
| 414 | # Now we determine if its optional, or required. This changes |
|---|
| 415 | # depending on what is in the rest of the match. If noreqs is |
|---|
| 416 | # true, then its possible the entire thing is optional as there's |
|---|
| 417 | # no reqs or string matches. |
|---|
| 418 | if noreqs: |
|---|
| 419 | # The rest is optional, but now we have an optional with a |
|---|
| 420 | # regexp. Wrap to ensure that if we match anything, we match |
|---|
| 421 | # our regexp first. It's still possible we could be completely |
|---|
| 422 | # blank as we have a default |
|---|
| 423 | if self.reqs.has_key(var) and self.defaults.has_key(var): |
|---|
| 424 | reg = '(' + partreg + rest + ')?' |
|---|
| 425 | |
|---|
| 426 | # Or we have a regexp match with no default, so now being |
|---|
| 427 | # completely blank form here on out isn't possible |
|---|
| 428 | elif self.reqs.has_key(var): |
|---|
| 429 | allblank = False |
|---|
| 430 | reg = partreg + rest |
|---|
| 431 | |
|---|
| 432 | # If the character before this is a special char, it has to be |
|---|
| 433 | # followed by this |
|---|
| 434 | elif self.defaults.has_key(var) and \ |
|---|
| 435 | self.prior in (',', ';', '.'): |
|---|
| 436 | reg = partreg + rest |
|---|
| 437 | |
|---|
| 438 | # Or we have a default with no regexp, don't touch the allblank |
|---|
| 439 | elif self.defaults.has_key(var): |
|---|
| 440 | reg = partreg + '?' + rest |
|---|
| 441 | |
|---|
| 442 | # Or we have a key with no default, and no reqs. Not possible |
|---|
| 443 | # to be all blank from here |
|---|
| 444 | else: |
|---|
| 445 | allblank = False |
|---|
| 446 | reg = partreg + rest |
|---|
| 447 | # In this case, we have something dangling that might need to be |
|---|
| 448 | # matched |
|---|
| 449 | else: |
|---|
| 450 | # If they can all be blank, and we have a default here, we know |
|---|
| 451 | # its safe to make everything from here optional. Since |
|---|
| 452 | # something else in the chain does have req's though, we have |
|---|
| 453 | # to make the partreg here required to continue matching |
|---|
| 454 | if allblank and self.defaults.has_key(var): |
|---|
| 455 | reg = '(' + partreg + rest + ')?' |
|---|
| 456 | |
|---|
| 457 | # Same as before, but they can't all be blank, so we have to |
|---|
| 458 | # require it all to ensure our matches line up right |
|---|
| 459 | else: |
|---|
| 460 | reg = partreg + rest |
|---|
| 461 | elif isinstance(part, dict) and part['type'] == '*': |
|---|
| 462 | var = part['name'] |
|---|
| 463 | if noreqs: |
|---|
| 464 | if include_names: |
|---|
| 465 | reg = '(?P<%s>.*)' % var + rest |
|---|
| 466 | else: |
|---|
| 467 | reg = '(?:.*)' + rest |
|---|
| 468 | if not self.defaults.has_key(var): |
|---|
| 469 | allblank = False |
|---|
| 470 | noreqs = False |
|---|
| 471 | else: |
|---|
| 472 | if allblank and self.defaults.has_key(var): |
|---|
| 473 | if include_names: |
|---|
| 474 | reg = '(?P<%s>.*)' % var + rest |
|---|
| 475 | else: |
|---|
| 476 | reg = '(?:.*)' + rest |
|---|
| 477 | elif self.defaults.has_key(var): |
|---|
| 478 | if include_names: |
|---|
| 479 | reg = '(?P<%s>.*)' % var + rest |
|---|
| 480 | else: |
|---|
| 481 | reg = '(?:.*)' + rest |
|---|
| 482 | else: |
|---|
| 483 | if include_names: |
|---|
| 484 | reg = '(?P<%s>.*)' % var + rest |
|---|
| 485 | else: |
|---|
| 486 | reg = '(?:.*)' + rest |
|---|
| 487 | allblank = False |
|---|
| 488 | noreqs = False |
|---|
| 489 | elif part and part[-1] in self.done_chars: |
|---|
| 490 | if allblank: |
|---|
| 491 | reg = re.escape(part[:-1]) + '(' + re.escape(part[-1]) + rest |
|---|
| 492 | reg += ')?' |
|---|
| 493 | else: |
|---|
| 494 | allblank = False |
|---|
| 495 | reg = re.escape(part) + rest |
|---|
| 496 | |
|---|
| 497 | # We have a normal string here, this is a req, and it prevents us from |
|---|
| 498 | # being all blank |
|---|
| 499 | else: |
|---|
| 500 | noreqs = False |
|---|
| 501 | allblank = False |
|---|
| 502 | reg = re.escape(part) + rest |
|---|
| 503 | |
|---|
| 504 | return (reg, noreqs, allblank) |
|---|
| 505 | |
|---|
| 506 | def match(self, url, environ=None, sub_domains=False, |
|---|
| 507 | sub_domains_ignore=None, domain_match=''): |
|---|
| 508 | """Match a url to our regexp. |
|---|
| 509 | |
|---|
| 510 | While the regexp might match, this operation isn't |
|---|
| 511 | guaranteed as there's other factors that can cause a match to |
|---|
| 512 | fail even though the regexp succeeds (Default that was relied |
|---|
| 513 | on wasn't given, requirement regexp doesn't pass, etc.). |
|---|
| 514 | |
|---|
| 515 | Therefore the calling function shouldn't assume this will |
|---|
| 516 | return a valid dict, the other possible return is False if a |
|---|
| 517 | match doesn't work out. |
|---|
| 518 | |
|---|
| 519 | """ |
|---|
| 520 | # Static routes don't match, they generate only |
|---|
| 521 | if self.static: |
|---|
| 522 | return False |
|---|
| 523 | |
|---|
| 524 | match = self.regmatch.match(url) |
|---|
| 525 | |
|---|
| 526 | if not match: |
|---|
| 527 | return False |
|---|
| 528 | |
|---|
| 529 | sub_domain = None |
|---|
| 530 | |
|---|
| 531 | if sub_domains and environ and 'HTTP_HOST' in environ: |
|---|
| 532 | host = environ['HTTP_HOST'].split(':')[0] |
|---|
| 533 | sub_match = re.compile('^(.+?)\.%s$' % domain_match) |
|---|
| 534 | subdomain = re.sub(sub_match, r'\1', host) |
|---|
| 535 | if subdomain not in sub_domains_ignore and host != subdomain: |
|---|
| 536 | sub_domain = subdomain |
|---|
| 537 | |
|---|
| 538 | if self.conditions: |
|---|
| 539 | if 'method' in self.conditions and environ and \ |
|---|
| 540 | environ['REQUEST_METHOD'] not in self.conditions['method']: |
|---|
| 541 | return False |
|---|
| 542 | |
|---|
| 543 | # Check sub-domains? |
|---|
| 544 | use_sd = self.conditions.get('sub_domain') |
|---|
| 545 | if use_sd and not sub_domain: |
|---|
| 546 | return False |
|---|
| 547 | elif not use_sd and 'sub_domain' in self.conditions and sub_domain: |
|---|
| 548 | return False |
|---|
| 549 | if isinstance(use_sd, list) and sub_domain not in use_sd: |
|---|
| 550 | return False |
|---|
| 551 | |
|---|
| 552 | matchdict = match.groupdict() |
|---|
| 553 | result = {} |
|---|
| 554 | extras = self._default_keys - frozenset(matchdict.keys()) |
|---|
| 555 | for key, val in matchdict.iteritems(): |
|---|
| 556 | if key != 'path_info' and self.encoding: |
|---|
| 557 | # change back into python unicode objects from the URL |
|---|
| 558 | # representation |
|---|
| 559 | try: |
|---|
| 560 | val = val and val.decode(self.encoding, self.decode_errors) |
|---|
| 561 | except UnicodeDecodeError: |
|---|
| 562 | return False |
|---|
| 563 | |
|---|
| 564 | if not val and key in self.defaults and self.defaults[key]: |
|---|
| 565 | result[key] = self.defaults[key] |
|---|
| 566 | else: |
|---|
| 567 | result[key] = val |
|---|
| 568 | for key in extras: |
|---|
| 569 | result[key] = self.defaults[key] |
|---|
| 570 | |
|---|
| 571 | # Add the sub-domain if there is one |
|---|
| 572 | if sub_domains: |
|---|
| 573 | result['sub_domain'] = sub_domain |
|---|
| 574 | |
|---|
| 575 | # If there's a function, call it with environ and expire if it |
|---|
| 576 | # returns False |
|---|
| 577 | if self.conditions and 'function' in self.conditions and \ |
|---|
| 578 | not self.conditions['function'](environ, result): |
|---|
| 579 | return False |
|---|
| 580 | |
|---|
| 581 | return result |
|---|
| 582 | |
|---|
| 583 | def generate_non_minimized(self, kargs): |
|---|
| 584 | """Generate a non-minimal version of the URL""" |
|---|
| 585 | # Iterate through the keys that are defaults, and NOT in the route |
|---|
| 586 | # path. If its not in kargs, or doesn't match, or is None, this |
|---|
| 587 | # route won't work |
|---|
| 588 | for k in self.maxkeys - self.minkeys: |
|---|
| 589 | if k not in kargs: |
|---|
| 590 | return False |
|---|
| 591 | elif self.make_unicode(kargs[k]) != \ |
|---|
| 592 | self.make_unicode(self.defaults[k]): |
|---|
| 593 | return False |
|---|
| 594 | |
|---|
| 595 | # Ensure that all the args in the route path are present and not None |
|---|
| 596 | for arg in self.minkeys: |
|---|
| 597 | if arg not in kargs or kargs[arg] is None: |
|---|
| 598 | if arg in self.dotkeys: |
|---|
| 599 | kargs[arg] = '' |
|---|
| 600 | else: |
|---|
| 601 | return False |
|---|
| 602 | |
|---|
| 603 | # Encode all the argument that the regpath can use |
|---|
| 604 | for k in kargs: |
|---|
| 605 | if k in self.maxkeys: |
|---|
| 606 | if k in self.dotkeys: |
|---|
| 607 | if kargs[k]: |
|---|
| 608 | kargs[k] = url_quote('.' + kargs[k], self.encoding) |
|---|
| 609 | else: |
|---|
| 610 | kargs[k] = url_quote(kargs[k], self.encoding) |
|---|
| 611 | |
|---|
| 612 | return self.regpath % kargs |
|---|
| 613 | |
|---|
| 614 | def generate_minimized(self, kargs): |
|---|
| 615 | """Generate a minimized version of the URL""" |
|---|
| 616 | routelist = self.routebackwards |
|---|
| 617 | urllist = [] |
|---|
| 618 | gaps = False |
|---|
| 619 | for part in routelist: |
|---|
| 620 | if isinstance(part, dict) and part['type'] in (':', '.'): |
|---|
| 621 | arg = part['name'] |
|---|
| 622 | |
|---|
| 623 | # For efficiency, check these just once |
|---|
| 624 | has_arg = kargs.has_key(arg) |
|---|
| 625 | has_default = self.defaults.has_key(arg) |
|---|
| 626 | |
|---|
| 627 | # Determine if we can leave this part off |
|---|
| 628 | # First check if the default exists and wasn't provided in the |
|---|
| 629 | # call (also no gaps) |
|---|
| 630 | if has_default and not has_arg and not gaps: |
|---|
| 631 | continue |
|---|
| 632 | |
|---|
| 633 | # Now check to see if there's a default and it matches the |
|---|
| 634 | # incoming call arg |
|---|
| 635 | if (has_default and has_arg) and self.make_unicode(kargs[arg]) == \ |
|---|
| 636 | self.make_unicode(self.defaults[arg]) and not gaps: |
|---|
| 637 | continue |
|---|
| 638 | |
|---|
| 639 | # We need to pull the value to append, if the arg is None and |
|---|
| 640 | # we have a default, use that |
|---|
| 641 | if has_arg and kargs[arg] is None and has_default and not gaps: |
|---|
| 642 | continue |
|---|
| 643 | |
|---|
| 644 | # Otherwise if we do have an arg, use that |
|---|
| 645 | elif has_arg: |
|---|
| 646 | val = kargs[arg] |
|---|
| 647 | |
|---|
| 648 | elif has_default and self.defaults[arg] is not None: |
|---|
| 649 | val = self.defaults[arg] |
|---|
| 650 | # Optional format parameter? |
|---|
| 651 | elif part['type'] == '.': |
|---|
| 652 | continue |
|---|
| 653 | # No arg at all? This won't work |
|---|
| 654 | else: |
|---|
| 655 | return False |
|---|
| 656 | |
|---|
| 657 | urllist.append(url_quote(val, self.encoding)) |
|---|
| 658 | if part['type'] == '.': |
|---|
| 659 | urllist.append('.') |
|---|
| 660 | |
|---|
| 661 | if has_arg: |
|---|
| 662 | del kargs[arg] |
|---|
| 663 | gaps = True |
|---|
| 664 | elif isinstance(part, dict) and part['type'] == '*': |
|---|
| 665 | arg = part['name'] |
|---|
| 666 | kar = kargs.get(arg) |
|---|
| 667 | if kar is not None: |
|---|
| 668 | urllist.append(url_quote(kar, self.encoding)) |
|---|
| 669 | gaps = True |
|---|
| 670 | elif part and part[-1] in self.done_chars: |
|---|
| 671 | if not gaps and part in self.done_chars: |
|---|
| 672 | continue |
|---|
| 673 | elif not gaps: |
|---|
| 674 | urllist.append(part[:-1]) |
|---|
| 675 | gaps = True |
|---|
| 676 | else: |
|---|
| 677 | gaps = True |
|---|
| 678 | urllist.append(part) |
|---|
| 679 | else: |
|---|
| 680 | gaps = True |
|---|
| 681 | urllist.append(part) |
|---|
| 682 | urllist.reverse() |
|---|
| 683 | url = ''.join(urllist) |
|---|
| 684 | return url |
|---|
| 685 | |
|---|
| 686 | def generate(self, _ignore_req_list=False, _append_slash=False, **kargs): |
|---|
| 687 | """Generate a URL from ourself given a set of keyword arguments |
|---|
| 688 | |
|---|
| 689 | Toss an exception if this |
|---|
| 690 | set of keywords would cause a gap in the url. |
|---|
| 691 | |
|---|
| 692 | """ |
|---|
| 693 | # Verify that our args pass any regexp requirements |
|---|
| 694 | if not _ignore_req_list: |
|---|
| 695 | for key in self.reqs.keys(): |
|---|
| 696 | val = kargs.get(key) |
|---|
| 697 | if val and not self.req_regs[key].match(self.make_unicode(val)): |
|---|
| 698 | return False |
|---|
| 699 | |
|---|
| 700 | # Verify that if we have a method arg, its in the method accept list. |
|---|
| 701 | # Also, method will be changed to _method for route generation |
|---|
| 702 | meth = kargs.get('method') |
|---|
| 703 | if meth: |
|---|
| 704 | if self.conditions and 'method' in self.conditions \ |
|---|
| 705 | and meth.upper() not in self.conditions['method']: |
|---|
| 706 | return False |
|---|
| 707 | kargs.pop('method') |
|---|
| 708 | |
|---|
| 709 | if self.minimization: |
|---|
| 710 | url = self.generate_minimized(kargs) |
|---|
| 711 | else: |
|---|
| 712 | url = self.generate_non_minimized(kargs) |
|---|
| 713 | |
|---|
| 714 | if url is False: |
|---|
| 715 | return url |
|---|
| 716 | |
|---|
| 717 | if not url.startswith('/') and not self.static: |
|---|
| 718 | url = '/' + url |
|---|
| 719 | extras = frozenset(kargs.keys()) - self.maxkeys |
|---|
| 720 | if extras: |
|---|
| 721 | if _append_slash and not url.endswith('/'): |
|---|
| 722 | url += '/' |
|---|
| 723 | fragments = [] |
|---|
| 724 | # don't assume the 'extras' set preserves order: iterate |
|---|
| 725 | # through the ordered kargs instead |
|---|
| 726 | for key in kargs: |
|---|
| 727 | if key not in extras: |
|---|
| 728 | continue |
|---|
| 729 | if key == 'action' or key == 'controller': |
|---|
| 730 | continue |
|---|
| 731 | val = kargs[key] |
|---|
| 732 | if isinstance(val, (tuple, list)): |
|---|
| 733 | for value in val: |
|---|
| 734 | fragments.append((key, _str_encode(value, self.encoding))) |
|---|
| 735 | else: |
|---|
| 736 | fragments.append((key, _str_encode(val, self.encoding))) |
|---|
| 737 | if fragments: |
|---|
| 738 | url += '?' |
|---|
| 739 | url += urllib.urlencode(fragments) |
|---|
| 740 | elif _append_slash and not url.endswith('/'): |
|---|
| 741 | url += '/' |
|---|
| 742 | return url |
|---|