PageRenderTime 69ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/shake/routes.py

https://github.com/nbpalomino/Shake
Python | 1715 lines | 1679 code | 15 blank | 21 comment | 5 complexity | 8ccfd57bbf926480b7c9c439077ec6de MD5 | raw file
Possible License(s): BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. # -*- coding: utf-8 -*-
  2. """
  3. Shake.routes
  4. --------------------------
  5. When it comes to combining multiple controller or view functions (however
  6. you want to call them) you need a dispatcher. A simple way would be
  7. applying regular expression tests on the `PATH_INFO` and calling
  8. registered callback functions that return the value then.
  9. This module implements a much more powerful system than simple regular
  10. expression matching because it can also convert values in the URLs and
  11. build URLs.
  12. Here a simple example that creates an URL map for an application with
  13. two subdomains (www and kb) and some URL rules:
  14. m = Map([
  15. # Static URLs
  16. Rule('/', 'static/index'),
  17. Rule('/about', 'static/about'),
  18. Rule('/help', 'static/help'),
  19. # Knowledge Base
  20. Subdomain('kb', [
  21. Rule('/', 'kb/index'),
  22. Rule('/browse/', 'kb/browse'),
  23. Rule('/browse/<int:id>/', 'kb/browse'),
  24. Rule('/browse/<int:id>/<int:page>', 'kb/browse'),
  25. ]),
  26. ], default_subdomain='www')
  27. If the application doesn't use subdomains it's perfectly fine to not set
  28. the default subdomain and not use the `Subdomain` rule factory. The endpoint
  29. in the rules can be anything, for example import paths or unique
  30. identifiers. The WSGI application can use those endpoints to get the
  31. handler for that URL. It doesn't have to be a string at all but it's
  32. recommended.
  33. Now it's possible to create a URL adapter for one of the subdomains and
  34. build URLs:
  35. >>> c = m.bind('example.com')
  36. >>> c.build("kb/browse", dict(id=42))
  37. 'http://kb.example.com/browse/42/'
  38. >>> c.build("kb/browse", dict())
  39. 'http://kb.example.com/browse/'
  40. >>> c.build("kb/browse", dict(id=42, page=3))
  41. 'http://kb.example.com/browse/42/3'
  42. >>> c.build("static/about")
  43. '/about'
  44. >>> c.build("static/index", force_external=True)
  45. 'http://www.example.com/'
  46. >>> c = m.bind('example.com', subdomain='kb')
  47. >>> c.build("static/about")
  48. 'http://www.example.com/about'
  49. The first argument to bind is the server name *without* the subdomain.
  50. Per default it will assume that the script is mounted on the root, but
  51. often that's not the case so you can provide the real mount point as
  52. second argument:
  53. c = m.bind('example.com', '/applications/example')
  54. The third argument can be the subdomain, if not given the default
  55. subdomain is used. For more details about binding have a look at the
  56. documentation of the `MapAdapter`.
  57. And here is how you can match URLs:
  58. >>> c = m.bind('example.com')
  59. >>> c.match("/")
  60. ('static/index', {})
  61. >>> c.match("/about")
  62. ('static/about', {})
  63. >>> c = m.bind('example.com', '/', 'kb')
  64. >>> c.match("/")
  65. ('kb/index', {})
  66. >>> c.match("/browse/42/23")
  67. ('kb/browse', {'id': 42, 'page': 23})
  68. If matching fails you get a `NotFound` exception, if the rule thinks
  69. it's a good idea to redirect (for example because the URL was defined
  70. to have a slash at the end but the request was missing that slash) it
  71. will raise a `RequestRedirect` exception. Both are subclasses of the
  72. `HTTPException` so you can use those errors as responses in the
  73. application.
  74. If matching succeeded but the URL rule was incompatible to the given
  75. method (for example there were only rules for `GET` and `HEAD` and
  76. routing system tried to match a `POST` request) a `MethodNotAllowed`
  77. method is raised.
  78. ----------------
  79. Forked from the routing package of Werkzeug <http://werkzeug.pocoo.org/>
  80. Copyright Š 2011 by the Werkzeug Team. Used under the modified BSD license.
  81. Changes from the original:
  82. * Respect for the rules order as declared. The weights of the converters are no
  83. longer necessary.
  84. * `endpoint` is now the second argument of the `Rule` constructor.
  85. * Port number and/or server name doesn't matter for the binding.
  86. Why should we care? 127.0.0.1 == localhost.
  87. * The rules can be also declared as regexp too.
  88. * Added support for named rules.
  89. """
  90. import re
  91. import posixpath
  92. from pprint import pformat
  93. from urlparse import urljoin
  94. from werkzeug.urls import url_encode, url_decode, url_quote
  95. from werkzeug.utils import redirect, format_string, import_string
  96. from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed
  97. from werkzeug.datastructures import ImmutableDict, MultiDict
  98. __all__ = (
  99. 'Rule', 'RuleFactory', 'Subdomain', 'Submount', 'EndpointPrefix',
  100. 'RuleTemplate', 'Map', 'MapAdapter', 'BuildError', 'RequestRedirect',
  101. 'RequestSlash',
  102. )
  103. _rule_re = re.compile(r'''
  104. (?P<static>[^<]*) # static rule data
  105. <
  106. (?:
  107. (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name
  108. (?:\((?P<args>.*?)\))? # converter arguments
  109. \: # variable delimiter
  110. )?
  111. (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name
  112. >
  113. ''', re.VERBOSE)
  114. _simple_rule_re = re.compile(r'<([^>]+)>')
  115. _converter_args_re = re.compile(r'''
  116. ((?P<name>\w+)\s*=\s*)?
  117. (?P<value>
  118. True|False|
  119. \d+.\d+|
  120. \d+.|
  121. \d+|
  122. \w+|
  123. [urUR]?(?P<stringval>"[^"]*?"|'[^']*')
  124. )\s*,
  125. ''', re.VERBOSE|re.UNICODE)
  126. _regex_vars = re.compile(r'\(\?P\<(\w+)\>')
  127. _PYTHON_CONSTANTS = {
  128. 'None': None,
  129. 'True': True,
  130. 'False': False,
  131. }
  132. def _pythonize(value):
  133. if value in _PYTHON_CONSTANTS:
  134. return _PYTHON_CONSTANTS[value]
  135. for convert in int, float:
  136. try:
  137. return convert(value)
  138. except ValueError:
  139. pass
  140. if value[:1] == value[-1:] and value[0] in '"\'':
  141. value = value[1:-1]
  142. return unicode(value)
  143. def _get_environ(obj):
  144. env = getattr(obj, 'environ', obj)
  145. assert isinstance(env, dict), \
  146. '%r is not a WSGI environment (has to be a dict)' % type(obj).__name__
  147. return env
  148. def parse_converter_args(argstr):
  149. argstr += ','
  150. args = []
  151. kwargs = {}
  152. for item in _converter_args_re.finditer(argstr):
  153. value = item.group('stringval')
  154. if value is None:
  155. value = item.group('value')
  156. value = _pythonize(value)
  157. if not item.group('name'):
  158. args.append(value)
  159. else:
  160. name = item.group('name')
  161. kwargs[name] = value
  162. return tuple(args), kwargs
  163. def parse_rule(rule):
  164. """Parse a rule and return it as generator. Each iteration yields tuples
  165. in the form `(converter, arguments, variable)`. If the converter is
  166. `None` it's a static url part, otherwise it's a dynamic one.
  167. :internal:
  168. """
  169. pos = 0
  170. end = len(rule)
  171. do_match = _rule_re.match
  172. used_names = set()
  173. while pos < end:
  174. m = do_match(rule, pos)
  175. if m is None:
  176. break
  177. data = m.groupdict()
  178. if data['static']:
  179. yield None, None, data['static']
  180. variable = data['variable']
  181. converter = data['converter'] or 'default'
  182. if variable in used_names:
  183. raise ValueError('variable name %r used twice.' % variable)
  184. used_names.add(variable)
  185. yield converter, data['args'] or None, variable
  186. pos = m.end()
  187. if pos < end:
  188. remaining = rule[pos:]
  189. if '>' in remaining or '<' in remaining:
  190. raise ValueError('malformed url rule: %r' % rule)
  191. yield None, None, remaining
  192. def get_converter(map, name, args):
  193. """Create a new converter for the given arguments or raise
  194. exception if the converter does not exist.
  195. :internal:
  196. """
  197. if not name in map.converters:
  198. raise LookupError('the converter %r does not exist' % name)
  199. if args:
  200. args, kwargs = parse_converter_args(args)
  201. else:
  202. args = ()
  203. kwargs = {}
  204. return map.converters[name](map, *args, **kwargs)
  205. def get_regex_variables(rule):
  206. variables = _regex_vars.findall(rule)
  207. return set(variables)
  208. class RoutingException(Exception):
  209. """Special exceptions that require the application to redirect, notifying
  210. about missing urls, etc.
  211. :internal:
  212. """
  213. pass
  214. class RequestRedirect(HTTPException, RoutingException):
  215. """Raise if the map requests a redirect. This is for example the case if
  216. `strict_slashes` are activated and an url that requires a trailing slash.
  217. The attribute `new_url` contains the absolute destination url.
  218. """
  219. code = 301
  220. def __init__(self, new_url):
  221. RoutingException.__init__(self, new_url)
  222. self.new_url = new_url
  223. def get_response(self, environ):
  224. return redirect(self.new_url, self.code)
  225. class RequestSlash(RoutingException):
  226. """Internal exception."""
  227. pass
  228. class RequestAliasRedirect(RoutingException):
  229. """This rule is an alias and wants to redirect to the canonical URL."""
  230. def __init__(self, matched_values):
  231. self.matched_values = matched_values
  232. class BuildError(RoutingException, LookupError):
  233. """Raised if the build system cannot find a URL for an endpoint with the
  234. values provided.
  235. """
  236. def __init__(self, endpoint, values, method):
  237. LookupError.__init__(self, endpoint, values, method)
  238. self.endpoint = endpoint
  239. self.values = values
  240. self.method = method
  241. class ValidationError(ValueError):
  242. """Validation error. If a rule converter raises this exception the rule
  243. does not match the current URL and the next URL is tried.
  244. """
  245. pass
  246. class RuleFactory(object):
  247. """As soon as you have more complex URL setups it's a good idea to use rule
  248. factories to avoid repetitive tasks. Some of them are builtin, others can
  249. be added by subclassing `RuleFactory` and overriding `get_rules`.
  250. """
  251. def get_rules(self, map):
  252. """Subclasses of `RuleFactory` have to override this method and return
  253. an iterable of rules."""
  254. raise NotImplementedError()
  255. class Subdomain(RuleFactory):
  256. """All URLs provided by this factory have the subdomain set to a
  257. specific domain. For example if you want to use the subdomain for
  258. the current language this can be a good setup::
  259. url_map = Map([
  260. Rule('/', endpoint='#select_language'),
  261. Subdomain('<string(length=2):lang_code>', [
  262. Rule('/', endpoint='index'),
  263. Rule('/about', endpoint='about'),
  264. Rule('/help', endpoint='help'),
  265. ]),
  266. ])
  267. All the rules except for the `'#select_language'` endpoint will now
  268. listen on a two letter long subdomain that holds the language code
  269. for the current request.
  270. """
  271. def __init__(self, subdomain, rules):
  272. self.subdomain = subdomain
  273. self.rules = rules
  274. def get_rules(self, map):
  275. for rulefactory in self.rules:
  276. for rule in rulefactory.get_rules(map):
  277. rule = rule.empty()
  278. rule.subdomain = self.subdomain
  279. yield rule
  280. class Submount(RuleFactory):
  281. """Like `Subdomain` but prefixes the URL rule with a given string::
  282. url_map = Map([
  283. Rule('/', endpoint='index'),
  284. Submount('/blog', [
  285. Rule('/', endpoint='blog/index'),
  286. Rule('/entry/<entry_slug>', endpoint='blog/show'),
  287. ]),
  288. ])
  289. Now the rule `'blog/show'` matches `/blog/entry/<entry_slug>`.
  290. """
  291. def __init__(self, path, rules):
  292. if isinstance(rules, basestring):
  293. rules = import_string(rules)
  294. self.path = path.rstrip('/')
  295. self.rules = rules
  296. def get_rules(self, map):
  297. for rulefactory in self.rules:
  298. for rule in rulefactory.get_rules(map):
  299. rule = rule.empty()
  300. rule.rule = self.path + rule.rule
  301. yield rule
  302. class EndpointPrefix(RuleFactory):
  303. """Prefixes all endpoints (which must be strings for this factory) with
  304. another string. This can be useful for sub applications::
  305. url_map = Map([
  306. Rule('/', endpoint='index'),
  307. EndpointPrefix('blog', [
  308. Submount('/blog', [
  309. Rule('/', endpoint='index'),
  310. Rule('/entry/<entry_slug>', endpoint='show'),
  311. ]),
  312. ]),
  313. ])
  314. """
  315. def __init__(self, prefix, rules):
  316. self.prefix = prefix.rstrip('.') + '.'
  317. self.rules = rules
  318. def get_rules(self, map):
  319. for rulefactory in self.rules:
  320. for rule in rulefactory.get_rules(map):
  321. rule = rule.empty()
  322. rule.endpoint = self.prefix + rule.endpoint
  323. yield rule
  324. class RuleTemplate(object):
  325. """Returns copies of the rules wrapped and expands string templates in
  326. the endpoint, rule, defaults or subdomain sections.
  327. Here a small example for such a rule template::
  328. from werkzeug.routing import Map, Rule, RuleTemplate
  329. resource = RuleTemplate([
  330. Rule('/$name/', endpoint='$name.list'),
  331. Rule('/$name/<int:id>', endpoint='$name.show'),
  332. ])
  333. url_map = Map([resource(name='user'), resource(name='page')])
  334. When a rule template is called the keyword arguments are used to
  335. replace the placeholders in all the string parameters.
  336. """
  337. def __init__(self, rules):
  338. self.rules = list(rules)
  339. def __call__(self, *args, **kwargs):
  340. return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
  341. class RuleTemplateFactory(RuleFactory):
  342. """A factory that fills in template variables into rules. Used by
  343. `RuleTemplate` internally.
  344. :internal:
  345. """
  346. def __init__(self, rules, context):
  347. self.rules = rules
  348. self.context = context
  349. def get_rules(self, map):
  350. for rulefactory in self.rules:
  351. for rule in rulefactory.get_rules(map):
  352. new_defaults = subdomain = None
  353. if rule.defaults:
  354. new_defaults = {}
  355. for key, value in rule.defaults.iteritems():
  356. if isinstance(value, basestring):
  357. value = format_string(value, self.context)
  358. new_defaults[key] = value
  359. if rule.subdomain is not None:
  360. subdomain = format_string(rule.subdomain, self.context)
  361. new_endpoint = rule.endpoint
  362. if isinstance(new_endpoint, basestring):
  363. new_endpoint = format_string(new_endpoint, self.context)
  364. yield Rule(
  365. format_string(rule.rule, self.context),
  366. endpoint=new_endpoint,
  367. defaults=new_defaults,
  368. subdomain=subdomain,
  369. methods=rule.methods,
  370. name=rule.name,
  371. build_only=rule.build_only,
  372. strict_slashes=rule.strict_slashes
  373. )
  374. class Rule(RuleFactory):
  375. """A Rule represents one URL pattern. There are some options for `Rule`
  376. that change the way it behaves and are passed to the `Rule` constructor.
  377. Note that besides the rule-string and endpoint all arguments *must*
  378. be keyword arguments in order to not break the application on Shake
  379. upgrades.
  380. string
  381. : Rule strings basically are just normal URL paths with placeholders in
  382. the format `<converter(arguments):name>` where the converter and the
  383. arguments are optional. If no converter is defined the `default`
  384. converter is used which means `string` in the normal configuration.
  385. URL rules that end with a slash are branch URLs, others are leaves.
  386. If you have `strict_slashes` enabled (which is the default), all
  387. branch URLs that are matched without a trailing slash will trigger a
  388. redirect to the same URL with the missing slash appended.
  389. The converters are defined on the `Map`.
  390. endpoint
  391. : The endpoint for this rule. This can be anything. A reference to a
  392. function, a string, a number etc. The preferred way is using a string
  393. because the endpoint is used for URL generation.
  394. defaults
  395. : An optional dict with defaults for other rules with the same endpoint.
  396. This is a bit tricky but useful if you want to have unique URLs::
  397. url_map = Map([
  398. Rule('/all/', endpoint='all_entries', defaults={'page': 1}),
  399. Rule('/all/page/<int:page>', endpoint='all_entries'),
  400. ])
  401. If a user now visits `http://example.com/all/page/1` he will be
  402. redirected to `http://example.com/all/`. If `redirect_defaults` is
  403. disabled on the `Map` instance this will only affect the URL
  404. generation.
  405. You could also use it to pass other values to the controller
  406. (this can be useful if you want generic controllers).
  407. subdomain
  408. : The subdomain rule string for this rule. If not specified the rule
  409. only matches for the `default_subdomain` of the map. If the map is
  410. not bound to a subdomain this feature is disabled.
  411. Can be useful if you want to have user profiles on different subdomains
  412. and all subdomains are forwarded to your application::
  413. url_map = Map([
  414. Rule('/', endpoint='user/homepage', subdomain='<username>'),
  415. Rule('/stats', endpoint='user/stats', subdomain='<username>'),
  416. ])
  417. methods
  418. : A sequence of http methods this rule applies to. If not specified, all
  419. methods are allowed. For example this can be useful if you want
  420. different endpoints for `POST` and `GET`.
  421. If methods are defined and the path matches but the method matched against is not in this list or in the
  422. list of another rule for that path the error raised is of the type
  423. `MethodNotAllowed` rather than `NotFound`.
  424. If `GET` is present in the list of methods and `HEAD` is not,
  425. `HEAD` is added automatically. The reason for this is that existing
  426. code often did not work properly in servers not rewriting `HEAD` to
  427. `GET` automatically and it was not documented how `HEAD` should be
  428. treated.
  429. strict_slashes
  430. : Override the `Map` setting for `strict_slashes` only for this rule. If
  431. not specified the `Map` setting is used.
  432. build_only
  433. : Set this to True and the rule will never match but will create a URL
  434. that can be build. This is useful if you have resources on a subdomain
  435. or folder that are not handled by the WSGI application (like static data)
  436. redirect_to
  437. : If given this must be either a string or callable. In case of a
  438. callable it's called with the url adapter that triggered the match and
  439. the values of the URL as keyword arguments and has to return the target
  440. for the redirect, otherwise it has to be a string with placeholders in
  441. rule syntax::
  442. def foo_with_slug(adapter, id):
  443. # ask the database for the slug for the old id. this of
  444. # course has nothing to do with werkzeug.
  445. return 'foo/' + Foo.get_slug_for_id(id)
  446. url_map = Map([
  447. Rule('/foo/<slug>', endpoint='foo'),
  448. Rule('/some/old/url/<slug>', redirect_to='foo/<slug>'),
  449. Rule('/other/old/url/<int:id>', redirect_to=foo_with_slug),
  450. ])
  451. When the rule is matched the routing system will raise a
  452. `RequestRedirect` exception with the target for the redirect.
  453. Keep in mind that the URL will be joined against the URL root of the
  454. script so don't use a leading slash on the target URL unless you
  455. really mean root of that domain.
  456. alias
  457. : If enabled this rule serves as an alias for another rule with the same
  458. endpoint and arguments.
  459. host
  460. : If provided and the URL map has host matching enabled this can be
  461. used to provide a match rule for the whole host. This also means
  462. that the subdomain feature is disabled.
  463. """
  464. def __init__(self, string, endpoint=None, defaults=None, subdomain=None,
  465. methods=None, name=None, build_only=False, strict_slashes=None,
  466. redirect_to=None, alias=False, host=None):
  467. if not string.startswith('/'):
  468. raise ValueError('URLs must start with a leading slash')
  469. self.rule = string.rstrip('$')
  470. self.is_leaf = not string.endswith('/')
  471. self.map = None
  472. self.strict_slashes = strict_slashes
  473. self.subdomain = subdomain
  474. self.host = host
  475. self.defaults = defaults
  476. self.build_only = build_only
  477. self.alias = alias
  478. if methods is None:
  479. self.methods = None
  480. else:
  481. self.methods = set([x.upper() for x in methods])
  482. if 'HEAD' not in self.methods and 'GET' in self.methods:
  483. self.methods.add('HEAD')
  484. self.endpoint = endpoint
  485. self.name = name
  486. self.redirect_to = redirect_to
  487. if defaults:
  488. self.arguments = set(map(str, defaults))
  489. else:
  490. self.arguments = set()
  491. self._trace = self._converters = self._regex = None
  492. def empty(self):
  493. """Return an unbound copy of this rule. This can be useful if you
  494. want to reuse an already bound URL for another map.
  495. """
  496. defaults = None
  497. if self.defaults:
  498. defaults = dict(self.defaults)
  499. return Rule(
  500. self.rule,
  501. endpoint=self.endpoint,
  502. defaults=defaults,
  503. subdomain=self.subdomain,
  504. methods=self.methods,
  505. name=self.name,
  506. build_only=self.build_only,
  507. strict_slashes=self.strict_slashes,
  508. redirect_to=self.redirect_to,
  509. alias=self.alias,
  510. host=self.host)
  511. def get_rules(self, map):
  512. yield self
  513. def refresh(self):
  514. """Rebinds and refreshes the URL. Call this if you modified the
  515. rule in place.
  516. :internal:
  517. """
  518. self.bind(self.map, rebind=True)
  519. def bind(self, map, rebind=False):
  520. """Bind the url to a map and create a regular expression based on
  521. the information from the rule itself and the defaults from the map.
  522. :internal:
  523. """
  524. if self.map is not None and not rebind:
  525. raise RuntimeError('url rule %r already bound to map %r' %
  526. (self, self.map))
  527. self.map = map
  528. if self.strict_slashes is None:
  529. self.strict_slashes = map.strict_slashes
  530. if self.subdomain is None:
  531. self.subdomain = map.default_subdomain
  532. self.compile()
  533. def compile(self):
  534. """Compiles the regular expression and stores it.
  535. """
  536. assert self.map is not None, 'rule not bound'
  537. if self.map.host_matching:
  538. domain_rule = self.host or ''
  539. else:
  540. domain_rule = self.subdomain or ''
  541. self._trace = []
  542. self._converters = {}
  543. regex_parts = []
  544. def _build_raw_regex(rule):
  545. rule = r'\/' + rule.lstrip('/^')
  546. regex_parts.append(rule)
  547. convobj = _RawConverter(self.map)
  548. for variable in get_regex_variables(rule):
  549. self._converters[variable] = convobj
  550. def _build_regex(rule):
  551. if '(?P<' in rule:
  552. return _build_raw_regex(rule)
  553. for converter, arguments, variable in parse_rule(rule):
  554. if converter is None:
  555. regex_parts.append(re.escape(variable))
  556. self._trace.append((False, variable))
  557. else:
  558. convobj = get_converter(self.map, converter, arguments)
  559. regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex))
  560. self._converters[variable] = convobj
  561. self._trace.append((True, variable))
  562. self.arguments.add(str(variable))
  563. _build_regex(domain_rule)
  564. regex_parts.append('\\|')
  565. self._trace.append((False, '|'))
  566. _build_regex(self.is_leaf and self.rule or self.rule.rstrip('/'))
  567. if not self.is_leaf:
  568. self._trace.append((False, '/'))
  569. if self.build_only:
  570. return
  571. regex = r'^%s%s$' % (
  572. u''.join(regex_parts),
  573. (not self.is_leaf or not self.strict_slashes) and \
  574. '(?<!/)(?P<__suffix__>/?)' or ''
  575. )
  576. self._regex = re.compile(regex, re.UNICODE)
  577. def match(self, path):
  578. """Check if the rule matches a given path. Path is a string in the
  579. form `"subdomain|/path(method)"` and is assembled by the map. If
  580. the map is doing host matching the subdomain part will be the host
  581. instead.
  582. If the rule matches a dict with the converted values is returned,
  583. otherwise the return value is `None`.
  584. :internal:
  585. """
  586. if not self.build_only:
  587. m = self._regex.search(path)
  588. if m is not None:
  589. groups = m.groupdict()
  590. # we have a folder like part of the url without a trailing
  591. # slash and strict slashes enabled. raise an exception that
  592. # tells the map to redirect to the same url but with a
  593. # trailing slash
  594. if self.strict_slashes and not self.is_leaf and \
  595. not groups.pop('__suffix__'):
  596. raise RequestSlash()
  597. # if we are not in strict slashes mode we have to remove
  598. # a __suffix__
  599. elif not self.strict_slashes:
  600. del groups['__suffix__']
  601. result = {}
  602. for name, value in groups.iteritems():
  603. try:
  604. value = self._converters[name].to_python(value)
  605. except ValidationError:
  606. return
  607. result[str(name)] = value
  608. if self.defaults:
  609. result.update(self.defaults)
  610. if self.alias and self.map.redirect_defaults:
  611. raise RequestAliasRedirect(result)
  612. return result
  613. def build(self, values, append_unknown=True):
  614. """Assembles the relative url for that rule and the subdomain.
  615. If building doesn't work for some reasons `None` is returned.
  616. :internal:
  617. """
  618. tmp = []
  619. add = tmp.append
  620. processed = set(self.arguments)
  621. for is_dynamic, data in self._trace:
  622. if is_dynamic:
  623. try:
  624. add(self._converters[data].to_url(values[data]))
  625. except ValidationError:
  626. return
  627. processed.add(data)
  628. else:
  629. add(data)
  630. domain_part, url = (u''.join(tmp)).split('|', 1)
  631. if append_unknown:
  632. query_vars = MultiDict(values)
  633. for key in processed:
  634. if key in query_vars:
  635. del query_vars[key]
  636. if query_vars:
  637. url += '?' + url_encode(query_vars, self.map.charset,
  638. sort=self.map.sort_parameters,
  639. key=self.map.sort_key)
  640. return domain_part, url
  641. def provides_defaults_for(self, rule):
  642. """Check if this rule has defaults for a given rule.
  643. :internal:
  644. """
  645. return not self.build_only and self.defaults and \
  646. self.endpoint == rule.endpoint and self != rule and \
  647. self.arguments == rule.arguments
  648. def suitable_for(self, values, method=None):
  649. """Check if the dict of values has enough data for url generation.
  650. :internal:
  651. """
  652. # if a method was given explicitly and that method is not supported
  653. # by this rule, this rule is not suitable.
  654. if method is not None and self.methods is not None \
  655. and method not in self.methods:
  656. return False
  657. defaults = self.defaults or ()
  658. # all arguments required must be either in the defaults dict or
  659. # the value dictionary otherwise it's not suitable
  660. for key in self.arguments:
  661. if key not in defaults and key not in values:
  662. return False
  663. # in case defaults are given we ensure taht either the value was
  664. # skipped or the value is the same as the default value.
  665. if defaults:
  666. for key, value in defaults.iteritems():
  667. if key in values and value != values[key]:
  668. return False
  669. return True
  670. def __eq__(self, other):
  671. return self.__class__ is other.__class__ and \
  672. self._trace == other._trace
  673. def __ne__(self, other):
  674. return not self.__eq__(other)
  675. def __unicode__(self):
  676. return self.rule
  677. def __str__(self):
  678. charset = self.map is not None and self.map.charset or 'utf-8'
  679. return unicode(self).encode(charset)
  680. def __repr__(self):
  681. if self.map is None:
  682. return '<%s (unbound)>' % self.__class__.__name__
  683. charset = self.map is not None and self.map.charset or 'utf-8'
  684. tmp = []
  685. for is_dynamic, data in self._trace:
  686. if is_dynamic:
  687. tmp.append('<%s>' % data)
  688. else:
  689. tmp.append(data)
  690. return '<%s %r%s -> %s>' % (
  691. self.__class__.__name__,
  692. (u''.join(tmp).encode(charset)).lstrip('|'),
  693. self.methods is not None and ' (%s)' % \
  694. ', '.join(self.methods) or '',
  695. self.endpoint
  696. )
  697. class BaseConverter(object):
  698. """Base class for all converters.
  699. """
  700. regex = '[^/]+'
  701. def __init__(self, map):
  702. self.map = map
  703. def to_python(self, value):
  704. return value
  705. def to_url(self, value):
  706. return url_quote(value, self.map.charset)
  707. class UnicodeConverter(BaseConverter):
  708. """This converter is the default converter and accepts any string but
  709. only one path segment. Thus the string can not include a slash.
  710. This is the default validator.
  711. Example::
  712. Rule('/pages/<page>'),
  713. Rule('/<string(length=2):lang_code>')
  714. map
  715. : an instance of `Map`.
  716. minlength
  717. : The minimum length of the string. Must be greater or equal 1.
  718. maxlength
  719. : The maximum length of the string.
  720. length
  721. : The exact length of the string.
  722. """
  723. def __init__(self, map, minlength=1, maxlength=None, length=None):
  724. BaseConverter.__init__(self, map)
  725. if length is not None:
  726. length = '{%d}' % int(length)
  727. else:
  728. if maxlength is None:
  729. maxlength = ''
  730. else:
  731. maxlength = int(maxlength)
  732. length = '{%s,%s}' % (
  733. int(minlength),
  734. maxlength
  735. )
  736. self.regex = '[^/]' + length
  737. class AnyConverter(BaseConverter):
  738. """Matches one of the items provided. Items can either be Python
  739. identifiers or strings::
  740. Rule('/<any(about, help, imprint, class, "foo,bar"):page_name>')
  741. map
  742. : an instance of `Map`.
  743. items
  744. : This function accepts the possible items as positional arguments.
  745. """
  746. def __init__(self, map, *items):
  747. BaseConverter.__init__(self, map)
  748. self.regex = '(?:%s)' % '|'.join([re.escape(x) for x in items])
  749. class PathConverter(BaseConverter):
  750. """Like the default :class:`UnicodeConverter`, but it also matches
  751. slashes. This is useful for wikis and similar applications::
  752. Rule('/<path:wikipage>')
  753. Rule('/<path:wikipage>/edit')
  754. map
  755. : an instance of `Map`.
  756. """
  757. regex = '[^/].*?'
  758. class NumberConverter(BaseConverter):
  759. """Baseclass for `IntegerConverter` and `FloatConverter`.
  760. """
  761. def __init__(self, map, fixed_digits=0, min=None, max=None):
  762. BaseConverter.__init__(self, map)
  763. self.fixed_digits = fixed_digits
  764. self.min = min
  765. self.max = max
  766. def to_python(self, value):
  767. if (self.fixed_digits and len(value) != self.fixed_digits):
  768. raise ValidationError()
  769. value = self.num_convert(value)
  770. if (self.min is not None and value < self.min) or \
  771. (self.max is not None and value > self.max):
  772. raise ValidationError()
  773. return value
  774. def to_url(self, value):
  775. value = self.num_convert(value)
  776. if self.fixed_digits:
  777. value = ('%%0%sd' % self.fixed_digits) % value
  778. return str(value)
  779. class IntegerConverter(NumberConverter):
  780. """This converter only accepts integer values::
  781. Rule('/page/<int:page>')
  782. This converter does not support negative values.
  783. map
  784. : an instance of `Map`.
  785. fixed_digits
  786. : the number of fixed digits in the URL. If you set this to `4`,
  787. for example, the application will only match if the url looks
  788. like `/0001/`. The default is variable length.
  789. min
  790. : the minimum value.
  791. max
  792. : the maximum value.
  793. """
  794. regex = r'\d+'
  795. num_convert = int
  796. class FloatConverter(NumberConverter):
  797. """This converter only accepts floating point values::
  798. Rule('/probability/<float:probability>')
  799. This converter does not support negative values.
  800. map
  801. : an instance of `Map`.
  802. min
  803. : the minimum value.
  804. max
  805. : the maximum value.
  806. """
  807. regex = r'\d+\.\d+'
  808. num_convert = float
  809. def __init__(self, map, min=None, max=None):
  810. NumberConverter.__init__(self, map, 0, min, max)
  811. class _RawConverter(BaseConverter):
  812. """Converter for raw values
  813. """
  814. def __init__(self, map,):
  815. self.map = map
  816. #: the default converter mapping for the map.
  817. DEFAULT_CONVERTERS = {
  818. 'default': UnicodeConverter,
  819. 'string': UnicodeConverter,
  820. 'any': AnyConverter,
  821. 'path': PathConverter,
  822. 'int': IntegerConverter,
  823. 'float': FloatConverter,
  824. }
  825. class Map(object):
  826. """The map class stores all the URL rules and some configuration
  827. parameters. Some of the configuration values are only stored on the
  828. `Map` instance since those affect all rules, others are just defaults
  829. and can be overridden for each rule. Note that you have to specify all
  830. arguments besides the `rules` as keyword arguments!
  831. rules
  832. : sequence of url rules for this map.
  833. default_subdomain
  834. : The default subdomain for rules without a subdomain defined.
  835. charset
  836. : charset of the url. defaults to `"utf-8"`
  837. strict_slashes
  838. : Take care of trailing slashes.
  839. redirect_defaults
  840. : This will redirect to the default rule if it wasn't visited that way.
  841. This helps creating unique URLs.
  842. converters
  843. : A dict of converters that adds additional converters
  844. to the list of converters. If you redefine one
  845. converter this will override the original one.
  846. sort_parameters
  847. : If set to `True` the url parameters are sorted.
  848. See `url_encode` for more details.
  849. sort_key
  850. : The sort key function for `url_encode`.
  851. encoding_errors
  852. : The error method to use for decoding
  853. host_matching
  854. : if set to `True` it enables the host matching feature and disables
  855. the subdomain one. If enabled the `host` parameter to rules is used
  856. instead of the `subdomain` one.
  857. """
  858. default_converters = ImmutableDict(DEFAULT_CONVERTERS)
  859. def __init__(self, rules=None, default_subdomain='', charset='utf-8',
  860. strict_slashes=True, redirect_defaults=True,
  861. converters=None, sort_parameters=False, sort_key=None,
  862. encoding_errors='replace', host_matching=False):
  863. self._rules = []
  864. self._rules_by_endpoint = {}
  865. self._rules_by_name = {}
  866. self._remap = True
  867. self.default_subdomain = default_subdomain
  868. self.charset = charset
  869. self.encoding_errors = encoding_errors
  870. self.strict_slashes = strict_slashes
  871. self.redirect_defaults = redirect_defaults
  872. self.host_matching = host_matching
  873. self.converters = self.default_converters.copy()
  874. if converters:
  875. self.converters.update(converters)
  876. self.sort_parameters = sort_parameters
  877. self.sort_key = sort_key
  878. for rulefactory in rules or ():
  879. self.add(rulefactory)
  880. def is_endpoint_expecting(self, endpoint, *arguments):
  881. """Iterate over all rules and check if the endpoint expects
  882. the arguments provided. This is for example useful if you have
  883. some URLs that expect a language code and others that do not and
  884. you want to wrap the builder a bit so that the current language
  885. code is automatically added if not provided but endpoints expect
  886. it.
  887. endpoint
  888. : the endpoint to check.
  889. arguments
  890. : this function accepts one or more arguments as positional
  891. arguments. Each one of them is checked.
  892. """
  893. arguments = set(arguments)
  894. for rule in self._rules_by_endpoint[endpoint]:
  895. if arguments.issubset(rule.arguments):
  896. return True
  897. return False
  898. def iter_rules(self, endpoint=None):
  899. """Iterate over all rules or the rules of an endpoint.
  900. endpoint
  901. : if provided only the rules for that endpoint are returned.
  902. return
  903. : an iterator
  904. """
  905. if endpoint is not None:
  906. return iter(self._rules_by_endpoint[endpoint])
  907. return iter(self._rules)
  908. def add(self, rulefactory):
  909. """Add a new rule or factory to the map and bind it. Requires that the
  910. rule is not bound to another map.
  911. rulefactory
  912. : a `Rule` or `RuleFactory`
  913. """
  914. for rule in rulefactory.get_rules(self):
  915. rule.bind(self)
  916. self._rules.append(rule)
  917. self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
  918. if rule.name:
  919. self._rules_by_name.setdefault(rule.name, []).append(rule)
  920. self._remap = True
  921. def bind(self, server_name, script_name=None, subdomain=None,
  922. url_scheme='http', default_method='GET', path_info=None,
  923. query_args=None):
  924. """Return a new :class:`MapAdapter` with the details specified to the
  925. call. Note that `script_name` will default to `'/'` if not further
  926. specified or `None`. The `server_name` at least is a requirement
  927. because the HTTP RFC requires absolute URLs for redirects and so all
  928. redirect exceptions raised by Shake will contain the full canonical
  929. URL.
  930. If no path_info is passed to :meth:`match` it will use the default path
  931. info passed to bind. While this doesn't really make sense for
  932. manual bind calls, it's useful if you bind a map to a WSGI
  933. environment which already contains the path info.
  934. `subdomain` will default to the `default_subdomain` for this map if
  935. no defined. If there is no `default_subdomain` you cannot use the
  936. subdomain feature.
  937. """
  938. server_name = server_name.lower()
  939. if self.host_matching:
  940. if subdomain is not None:
  941. raise RuntimeError('host matching enabled and a '
  942. 'subdomain was provided')
  943. elif subdomain is None:
  944. subdomain = self.default_subdomain
  945. if script_name is None:
  946. script_name = '/'
  947. return MapAdapter(self, server_name, script_name, subdomain,
  948. url_scheme, path_info, default_method, query_args)
  949. def bind_to_environ(self, environ, server_name=None, subdomain=None):
  950. """Like the method `bind` but you can pass it an WSGI environment
  951. and it will fetch the information from that dictionary.
  952. Note that because of limitations in the protocol there is no way to
  953. get the current subdomain and real `server_name` from the environment.
  954. If you don't provide it, Shake will use `SERVER_NAME` and
  955. `SERVER_PORT` (or `HTTP_HOST` if provided) as used `server_name`
  956. with disabled subdomain feature.
  957. If `subdomain` is `None` but an environment and a server name is
  958. provided it will calculate the current subdomain automatically.
  959. Example: `server_name` is `'example.com'` and the `SERVER_NAME`
  960. in the wsgi `environ` is `'staging.dev.example.com'` the calculated
  961. subdomain will be `'staging.dev'`.
  962. If the object passed as environ has an environ attribute, the value of
  963. this attribute is used instead. This allows you to pass request
  964. objects. Additionally `PATH_INFO` added as a default of the
  965. `MapAdapter` so that you don't have to pass the path info to
  966. the match method.
  967. environ
  968. : a WSGI environment.
  969. server_name
  970. : an optional server name hint (see above).
  971. subdomain
  972. : optionally the current subdomain (see above).
  973. """
  974. environ = _get_environ(environ)
  975. if server_name is None:
  976. if 'HTTP_HOST' in environ:
  977. server_name = environ['HTTP_HOST']
  978. else:
  979. server_name = environ['SERVER_NAME']
  980. if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
  981. in (('https', '443'), ('http', '80')):
  982. server_name += ':' + environ['SERVER_PORT']
  983. elif subdomain is None and not self.host_matching:
  984. server_name = server_name.lower()
  985. if 'HTTP_HOST' in environ:
  986. wsgi_server_name = environ.get('HTTP_HOST')
  987. else:
  988. wsgi_server_name = environ.get('SERVER_NAME')
  989. if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
  990. in (('https', '443'), ('http', '80')):
  991. wsgi_server_name += ':' + environ['SERVER_PORT']
  992. wsgi_server_name = wsgi_server_name.lower()
  993. ## `cur_server_name` can be != real_server_name
  994. ## even with valid configs if the server was
  995. ## accesssed directly by IP address under some situations.
  996. ## Why should we care?
  997. cur_server_name = wsgi_server_name.split(':', 1)[0].split('.')
  998. real_server_name = server_name.split(':', 1)[0].split('.')
  999. offset = -len(real_server_name)
  1000. subdomain = '.'.join(filter(None, cur_server_name[:offset]))
  1001. return Map.bind(self, server_name, environ.get('SCRIPT_NAME'),
  1002. subdomain, environ['wsgi.url_scheme'], environ['REQUEST_METHOD'],
  1003. environ.get('PATH_INFO'),
  1004. query_args=environ.get('QUERY_STRING', ''))
  1005. def __repr__(self):
  1006. rules = self.iter_rules()
  1007. return '%s(%s)' % (self.__class__.__name__, pformat(list(rules)))
  1008. class MapAdapter(object):
  1009. """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does
  1010. the URL matching and building based on runtime information.
  1011. """
  1012. def __init__(self, map, server_name, script_name, subdomain,
  1013. url_scheme, path_info, default_method, query_args=None):
  1014. self.map = map
  1015. self.server_name = server_name
  1016. if not script_name.endswith('/'):
  1017. script_name += '/'
  1018. self.script_name = script_name
  1019. self.subdomain = subdomain
  1020. self.url_scheme = url_scheme
  1021. self.path_info = path_info or u''
  1022. self.default_method = default_method
  1023. self.query_args = query_args
  1024. def dispatch(self, view_func, path_info=None, method=None,
  1025. catch_http_exceptions=False):
  1026. """Does the complete dispatching process. `view_func` is called with
  1027. the endpoint and a dict with the values for the view. It should
  1028. look up the view function, call it, and return a response object
  1029. or WSGI application. http exceptions are not caught by default
  1030. so that applications can display nicer error messages by just
  1031. catching them by hand. If you want to stick with the default
  1032. error messages you can pass it `catch_http_exceptions=True` and
  1033. it will catch the http exceptions.
  1034. Here a small example for the dispatch usage::
  1035. from werkzeug.wrappers import Request, Response
  1036. from werkzeug.wsgi import responder
  1037. from werkzeug.routing import Map, Rule
  1038. def on_index(request):
  1039. return Response('Hello from the index')
  1040. url_map = Map([Rule('/', endpoint='index')])
  1041. views = {'index': on_index}
  1042. @responder
  1043. def application(environ, start_response):
  1044. request = Request(environ)
  1045. urls = url_map.bind_to_environ(environ)
  1046. return urls.dispatch(lambda e, v: views[e](request, **v),
  1047. catch_http_exceptions=True)
  1048. Keep in mind that this method might return exception objects, too, so
  1049. use :class:`Response.force_type` to get a response object.
  1050. view_func
  1051. : A function that is called with the endpoint as
  1052. first argument and the value dict as second. Has
  1053. to dispatch to the actual view function with this
  1054. information. (see above)
  1055. path_info
  1056. : The path info to use for matching. Overrides the
  1057. path info specified on binding.
  1058. method
  1059. : The HTTP method used for matching. Overrides the
  1060. method specified on binding.
  1061. catch_http_exceptions
  1062. : Set to `True` to catch any of the `HTTPException`s.
  1063. """
  1064. try:
  1065. try:
  1066. endpoint, args = self.match(path_info, method)
  1067. except RequestRedirect, e:
  1068. return e
  1069. return view_func(endpoint, args)
  1070. except HTTPException, e:
  1071. if catch_http_exceptions:
  1072. return e
  1073. raise
  1074. def match(self, path_info=None, method=None, return_rule=False,
  1075. query_args=None):
  1076. """The usage is simple: you just pass the match method the current
  1077. path info as well as the method (which defaults to `GET`). The
  1078. following things can then happen:
  1079. * you receive a `NotFound` exception that indicates that no URL is
  1080. matching. A `NotFound` exception is also a WSGI application you
  1081. can call to get a default page not found page (happens to be the
  1082. same object as `werkzeug.exceptions.NotFound`)
  1083. * you receive a `MethodNotAllowed` exception that indicates that there
  1084. is a match for this URL but not for the current request method.
  1085. This is useful for RESTful applications.
  1086. * you receive a `RequestRedirect` exception with a `new_url`
  1087. attribute. This exception is used to notify you about a request
  1088. Shake requests from your WSGI application. This is for example the
  1089. case if you request `/foo` although the correct URL is `/foo/`
  1090. You can use the `RequestRedirect` instance as response-like object
  1091. similar to all other subclasses of `HTTPException`.
  1092. * you get a tuple in the form `(endpoint, arguments)` if there is
  1093. a match (unless `return_rule` is True, in which case you get a tuple
  1094. in the form `(rule, arguments)`)
  1095. If the path info is not passed to the match method the default path
  1096. info of the map is used (defaults to the root URL if not defined
  1097. explicitly).
  1098. All of the exceptions raised are subclasses of `HTTPException` so they
  1099. can be used as WSGI responses. The will all render generic error or
  1100. redirect pages.
  1101. Here is a small example for matching:
  1102. >>> m = Map([
  1103. ... Rule('/', endpoint='index'),
  1104. ... Rule('/downloads/', endpoint='downloads/index'),
  1105. ... Rule('/downloads/<int:id>', endpoint='downloads/show')
  1106. ... ])
  1107. >>> urls = m.bind("example.com", "/")
  1108. >>> urls.match("/", "GET")
  1109. ('index', {})
  1110. >>> urls.match("/downloads/42")
  1111. ('downloads/show', {'id': 42})
  1112. And here is what happens on redirect and missing URLs:
  1113. >>> urls.match("/downloads")
  1114. Traceback (most recent call last):
  1115. ...
  1116. RequestRedirect: http://example.com/downloads/
  1117. >>> urls.match("/missing")
  1118. Traceback (most recent call last):
  1119. ...
  1120. NotFound: 404 Not Found
  1121. path_info
  1122. : The path info to use for matching. Overrides the path info
  1123. specified on binding.
  1124. method
  1125. : The HTTP method used for matching. Overrides the method
  1126. specified on binding.
  1127. return_rule
  1128. : Return the rule that matched instead of just the endpoint
  1129. (defaults to `False`).
  1130. query_args
  1131. : Optional query arguments that are used for automatic redirects as
  1132. string or dictionary. It's currently not possible to use the
  1133. query arguments for URL matching.
  1134. """
  1135. if path_info is Non

Large files files are truncated, but you can click here to view the full file