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