urls.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. from __future__ import unicode_literals
  2. import re
  3. try:
  4. from urllib.parse import urlencode, unquote_plus, quote_plus
  5. except ImportError:
  6. from urllib import urlencode, unquote_plus, quote_plus
  7. from .common import pickle_dict, unpickle_dict
  8. import sys
  9. if sys.version_info[0] >= 3:
  10. unicode = str
  11. long = int
  12. basestring = (str, bytes)
  13. # TODO: Use regular Exceptions
  14. class AmbiguousUrlException(Exception):
  15. pass
  16. class NotFoundException(Exception):
  17. pass
  18. class UrlRule(object):
  19. '''This object stores the various properties related to a routing URL rule.
  20. It also provides a few methods to create URLs from the rule or to match a
  21. given URL against a rule.
  22. :param url_rule: The relative url pattern for the rule. It may include
  23. <var_name> to denote where dynamic variables should be
  24. matched.
  25. :param view_func: The function that should be bound to this rule. This
  26. should be an actual function object.
  27. .. warning:: The function signature should match any
  28. variable names in the provided url_rule.
  29. :param name: The name of the url rule. This is used in the reverse process
  30. of creating urls for a given rule.
  31. :param options: A dict containing any default values for the url rule.
  32. '''
  33. def __init__(self, url_rule, view_func, name, options=None, cache=True, update=False, content_type="", view_mode=0, goto_top=True):
  34. self._name = name
  35. self._url_rule = url_rule
  36. self._view_func = view_func
  37. self._goto_top = goto_top
  38. self._cache = cache
  39. self._update = update
  40. self._content_type = content_type
  41. self._view_mode = view_mode
  42. self._options = {}
  43. if options:
  44. self._options.update(options)
  45. self._keywords = re.findall(r'\<(.+?)\>', url_rule)
  46. # change <> to {} for use with str.format()
  47. self._url_format = self._url_rule.replace('<', '{').replace('>', '}')
  48. # Make a regex pattern for matching incoming URLs
  49. rule = self._url_rule
  50. if rule != '/':
  51. # Except for a path of '/', the trailing slash is optional.
  52. rule = self._url_rule.rstrip('/') + '/?'
  53. p = rule.replace('<', '(?P<').replace('>', '>[^/]+?)')
  54. try:
  55. self._regex = re.compile('^' + p + '$')
  56. except re.error as e:
  57. raise ValueError('There was a problem creating this URL rule. '
  58. 'Ensure you do not have any unpaired angle '
  59. 'brackets: "<" or ">"')
  60. def __eq__(self, other):
  61. return (
  62. (self._name, self._url_rule, self._view_func, self._options) ==
  63. (other._name, other._url_rule, other._view_func, other._options)
  64. )
  65. def __ne__(self, other):
  66. return not self.__eq__(other)
  67. def match(self, path):
  68. '''Attempts to match a url to the given path. If successful, a tuple is
  69. returned. The first item is the matchd function and the second item is
  70. a dictionary containing items to be passed to the function parsed from
  71. the provided path.
  72. If the provided path does not match this url rule then a
  73. NotFoundException is raised.
  74. '''
  75. m = self._regex.search(path)
  76. if not m:
  77. raise NotFoundException
  78. # urlunencode the values
  79. items = dict((key, unquote_plus(val))
  80. for key, val in m.groupdict().items())
  81. # unpickle any items if present
  82. items = unpickle_dict(items)
  83. # We need to update our dictionary with default values provided in
  84. # options if the keys don't already exist.
  85. return self._view_func, items
  86. def _make_path(self, items):
  87. '''Returns a relative path for the given dictionary of items.
  88. Uses this url rule's url pattern and replaces instances of <var_name>
  89. with the appropriate value from the items dict.
  90. '''
  91. for key, val in items.items():
  92. if not isinstance(val, basestring):
  93. raise Exception('Value "%s" for key "%s" must be an instance of basestring but is %s' % (val, key, str(type(val))))
  94. items[key] = quote_plus(val)
  95. try:
  96. path = self._url_format.format(**items)
  97. except AttributeError:
  98. # Old version of python
  99. path = self._url_format
  100. for key, val in items.items():
  101. path = path.replace('{%s}' % key, val)
  102. return path
  103. def _make_qs(self, items):
  104. '''Returns a query string for the given dictionary of items. All keys
  105. and values in the provided items will be urlencoded. If necessary, any
  106. python objects will be pickled before being urlencoded.
  107. '''
  108. return urlencode(pickle_dict(items))
  109. def make_path_qs(self, items):
  110. '''Returns a relative path complete with query string for the given
  111. dictionary of items.
  112. Any items with keys matching this rule's url pattern will be inserted
  113. into the path. Any remaining items will be appended as query string
  114. parameters.
  115. All items will be urlencoded. Any items which are not instances of
  116. basestring, or int/long will be pickled before being urlencoded.
  117. .. warning:: The pickling of items only works for key/value pairs which
  118. will be in the query string. This behavior should only be
  119. used for the simplest of python objects. It causes the
  120. URL to get very lengthy (and unreadable) and XBMC has a
  121. hard limit on URL length. See the caching section if you
  122. need to persist a large amount of data between requests.
  123. '''
  124. # Convert any ints and longs to strings
  125. for key, val in items.items():
  126. if isinstance(val, (int, long)):
  127. items[key] = str(val)
  128. # First use our defaults passed when registering the rule
  129. url_items = {}
  130. # Now update with any items explicitly passed to url_for
  131. url_items.update((key, val) for key, val in items.items()
  132. if key in self._keywords)
  133. # Create the path
  134. path = self._make_path(url_items)
  135. # Extra arguments get tacked on to the query string
  136. qs_items = dict((key, val) for key, val in items.items()
  137. if key not in self._keywords)
  138. qs = self._make_qs(qs_items)
  139. if qs:
  140. return '?'.join([path, qs])
  141. return path
  142. @property
  143. def regex(self):
  144. '''The regex for matching paths against this url rule.'''
  145. return self._regex
  146. @property
  147. def view_func(self):
  148. '''The bound function'''
  149. return self._view_func
  150. @property
  151. def url_format(self):
  152. '''The url pattern'''
  153. return self._url_format
  154. @property
  155. def name(self):
  156. '''The name of this url rule.'''
  157. return self._name
  158. @property
  159. def keywords(self):
  160. '''The list of path keywords for this url rule.'''
  161. return self._keywords
  162. @property
  163. def options(self):
  164. return self._options
  165. @property
  166. def cache(self):
  167. return self._cache
  168. @property
  169. def update(self):
  170. return self._update
  171. @property
  172. def content_type(self):
  173. return self._content_type
  174. @property
  175. def view_mode(self):
  176. return self._view_mode
  177. @property
  178. def goto_top(self):
  179. return self._goto_top