variable.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. """
  2. uritemplate.variable
  3. ====================
  4. This module contains the URIVariable class which powers the URITemplate class.
  5. What treasures await you:
  6. - URIVariable class
  7. You see a hammer in front of you.
  8. What do you do?
  9. >
  10. """
  11. import collections.abc
  12. import typing as t
  13. import urllib.parse
  14. ScalarVariableValue = t.Union[int, float, complex, str]
  15. VariableValue = t.Union[
  16. t.Sequence[ScalarVariableValue],
  17. t.Mapping[str, ScalarVariableValue],
  18. t.Tuple[str, ScalarVariableValue],
  19. ScalarVariableValue,
  20. ]
  21. VariableValueDict = t.Dict[str, VariableValue]
  22. class URIVariable:
  23. """This object validates everything inside the URITemplate object.
  24. It validates template expansions and will truncate length as decided by
  25. the template.
  26. Please note that just like the :class:`URITemplate <URITemplate>`, this
  27. object's ``__str__`` and ``__repr__`` methods do not return the same
  28. information. Calling ``str(var)`` will return the original variable.
  29. This object does the majority of the heavy lifting. The ``URITemplate``
  30. object finds the variables in the URI and then creates ``URIVariable``
  31. objects. Expansions of the URI are handled by each ``URIVariable``
  32. object. ``URIVariable.expand()`` returns a dictionary of the original
  33. variable and the expanded value. Check that method's documentation for
  34. more information.
  35. """
  36. operators = ("+", "#", ".", "/", ";", "?", "&", "|", "!", "@")
  37. reserved = ":/?#[]@!$&'()*+,;="
  38. def __init__(self, var: str):
  39. #: The original string that comes through with the variable
  40. self.original: str = var
  41. #: The operator for the variable
  42. self.operator: str = ""
  43. #: List of safe characters when quoting the string
  44. self.safe: str = ""
  45. #: List of variables in this variable
  46. self.variables: t.List[
  47. t.Tuple[str, t.MutableMapping[str, t.Any]]
  48. ] = []
  49. #: List of variable names
  50. self.variable_names: t.List[str] = []
  51. #: List of defaults passed in
  52. self.defaults: t.MutableMapping[str, ScalarVariableValue] = {}
  53. # Parse the variable itself.
  54. self.parse()
  55. self.post_parse()
  56. def __repr__(self) -> str:
  57. return "URIVariable(%s)" % self
  58. def __str__(self) -> str:
  59. return self.original
  60. def parse(self) -> None:
  61. """Parse the variable.
  62. This finds the:
  63. - operator,
  64. - set of safe characters,
  65. - variables, and
  66. - defaults.
  67. """
  68. var_list_str = self.original
  69. if self.original[0] in URIVariable.operators:
  70. self.operator = self.original[0]
  71. var_list_str = self.original[1:]
  72. if self.operator in URIVariable.operators[:2]:
  73. self.safe = URIVariable.reserved
  74. var_list = var_list_str.split(",")
  75. for var in var_list:
  76. default_val = None
  77. name = var
  78. if "=" in var:
  79. name, default_val = tuple(var.split("=", 1))
  80. explode = False
  81. if name.endswith("*"):
  82. explode = True
  83. name = name[:-1]
  84. prefix: t.Optional[int] = None
  85. if ":" in name:
  86. name, prefix_str = tuple(name.split(":", 1))
  87. prefix = int(prefix_str)
  88. if default_val:
  89. self.defaults[name] = default_val
  90. self.variables.append(
  91. (name, {"explode": explode, "prefix": prefix})
  92. )
  93. self.variable_names = [varname for (varname, _) in self.variables]
  94. def post_parse(self) -> None:
  95. """Set ``start``, ``join_str`` and ``safe`` attributes.
  96. After parsing the variable, we need to set up these attributes and it
  97. only makes sense to do it in a more easily testable way.
  98. """
  99. self.safe = ""
  100. self.start = self.join_str = self.operator
  101. if self.operator == "+":
  102. self.start = ""
  103. if self.operator in ("+", "#", ""):
  104. self.join_str = ","
  105. if self.operator == "#":
  106. self.start = "#"
  107. if self.operator == "?":
  108. self.start = "?"
  109. self.join_str = "&"
  110. if self.operator in ("+", "#"):
  111. self.safe = URIVariable.reserved
  112. def _query_expansion(
  113. self,
  114. name: str,
  115. value: VariableValue,
  116. explode: bool,
  117. prefix: t.Optional[int],
  118. ) -> t.Optional[str]:
  119. """Expansion method for the '?' and '&' operators."""
  120. if value is None:
  121. return None
  122. tuples, items = is_list_of_tuples(value)
  123. safe = self.safe
  124. if list_test(value) and not tuples:
  125. if not value:
  126. return None
  127. value = t.cast(t.Sequence[ScalarVariableValue], value)
  128. if explode:
  129. return self.join_str.join(
  130. f"{name}={quote(v, safe)}" for v in value
  131. )
  132. else:
  133. value = ",".join(quote(v, safe) for v in value)
  134. return f"{name}={value}"
  135. if dict_test(value) or tuples:
  136. if not value:
  137. return None
  138. value = t.cast(t.Mapping[str, ScalarVariableValue], value)
  139. items = items or sorted(value.items())
  140. if explode:
  141. return self.join_str.join(
  142. f"{quote(k, safe)}={quote(v, safe)}" for k, v in items
  143. )
  144. else:
  145. value = ",".join(
  146. f"{quote(k, safe)},{quote(v, safe)}" for k, v in items
  147. )
  148. return f"{name}={value}"
  149. if value:
  150. value = t.cast(t.Text, value)
  151. value = value[:prefix] if prefix else value
  152. return f"{name}={quote(value, safe)}"
  153. return name + "="
  154. def _label_path_expansion(
  155. self,
  156. name: str,
  157. value: VariableValue,
  158. explode: bool,
  159. prefix: t.Optional[int],
  160. ) -> t.Optional[str]:
  161. """Label and path expansion method.
  162. Expands for operators: '/', '.'
  163. """
  164. join_str = self.join_str
  165. safe = self.safe
  166. if value is None or (
  167. not isinstance(value, (str, int, float, complex))
  168. and len(value) == 0
  169. ):
  170. return None
  171. tuples, items = is_list_of_tuples(value)
  172. if list_test(value) and not tuples:
  173. if not explode:
  174. join_str = ","
  175. value = t.cast(t.Sequence[ScalarVariableValue], value)
  176. fragments = [quote(v, safe) for v in value if v is not None]
  177. return join_str.join(fragments) if fragments else None
  178. if dict_test(value) or tuples:
  179. value = t.cast(t.Mapping[str, ScalarVariableValue], value)
  180. items = items or sorted(value.items())
  181. format_str = "%s=%s"
  182. if not explode:
  183. format_str = "%s,%s"
  184. join_str = ","
  185. expanded = join_str.join(
  186. format_str % (quote(k, safe), quote(v, safe))
  187. for k, v in items
  188. if v is not None
  189. )
  190. return expanded if expanded else None
  191. value = t.cast(t.Text, value)
  192. value = value[:prefix] if prefix else value
  193. return quote(value, safe)
  194. def _semi_path_expansion(
  195. self,
  196. name: str,
  197. value: VariableValue,
  198. explode: bool,
  199. prefix: t.Optional[int],
  200. ) -> t.Optional[str]:
  201. """Expansion method for ';' operator."""
  202. join_str = self.join_str
  203. safe = self.safe
  204. if value is None:
  205. return None
  206. if self.operator == "?":
  207. join_str = "&"
  208. tuples, items = is_list_of_tuples(value)
  209. if list_test(value) and not tuples:
  210. value = t.cast(t.Sequence[ScalarVariableValue], value)
  211. if explode:
  212. expanded = join_str.join(
  213. f"{name}={quote(v, safe)}" for v in value if v is not None
  214. )
  215. return expanded if expanded else None
  216. else:
  217. value = ",".join(quote(v, safe) for v in value)
  218. return f"{name}={value}"
  219. if dict_test(value) or tuples:
  220. value = t.cast(t.Mapping[str, ScalarVariableValue], value)
  221. items = items or sorted(value.items())
  222. if explode:
  223. return join_str.join(
  224. f"{quote(k, safe)}={quote(v, safe)}"
  225. for k, v in items
  226. if v is not None
  227. )
  228. else:
  229. expanded = ",".join(
  230. f"{quote(k, safe)},{quote(v, safe)}"
  231. for k, v in items
  232. if v is not None
  233. )
  234. return f"{name}={expanded}"
  235. value = t.cast(t.Text, value)
  236. value = value[:prefix] if prefix else value
  237. if value:
  238. return f"{name}={quote(value, safe)}"
  239. return name
  240. def _string_expansion(
  241. self,
  242. name: str,
  243. value: VariableValue,
  244. explode: bool,
  245. prefix: t.Optional[int],
  246. ) -> t.Optional[str]:
  247. if value is None:
  248. return None
  249. tuples, items = is_list_of_tuples(value)
  250. if list_test(value) and not tuples:
  251. value = t.cast(t.Sequence[ScalarVariableValue], value)
  252. return ",".join(quote(v, self.safe) for v in value)
  253. if dict_test(value) or tuples:
  254. value = t.cast(t.Mapping[str, ScalarVariableValue], value)
  255. items = items or sorted(value.items())
  256. format_str = "%s=%s" if explode else "%s,%s"
  257. return ",".join(
  258. format_str % (quote(k, self.safe), quote(v, self.safe))
  259. for k, v in items
  260. )
  261. value = t.cast(t.Text, value)
  262. value = value[:prefix] if prefix else value
  263. return quote(value, self.safe)
  264. def expand(
  265. self, var_dict: t.Optional[VariableValueDict] = None
  266. ) -> t.Mapping[str, str]:
  267. """Expand the variable in question.
  268. Using ``var_dict`` and the previously parsed defaults, expand this
  269. variable and subvariables.
  270. :param dict var_dict: dictionary of key-value pairs to be used during
  271. expansion
  272. :returns: dict(variable=value)
  273. Examples::
  274. # (1)
  275. v = URIVariable('/var')
  276. expansion = v.expand({'var': 'value'})
  277. print(expansion)
  278. # => {'/var': '/value'}
  279. # (2)
  280. v = URIVariable('?var,hello,x,y')
  281. expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
  282. 'x': '1024', 'y': '768'})
  283. print(expansion)
  284. # => {'?var,hello,x,y':
  285. # '?var=value&hello=Hello%20World%21&x=1024&y=768'}
  286. """
  287. return_values = []
  288. if var_dict is None:
  289. return {self.original: self.original}
  290. for name, opts in self.variables:
  291. value = var_dict.get(name, None)
  292. if not value and value != "" and name in self.defaults:
  293. value = self.defaults[name]
  294. if value is None:
  295. continue
  296. expanded = None
  297. if self.operator in ("/", "."):
  298. expansion = self._label_path_expansion
  299. elif self.operator in ("?", "&"):
  300. expansion = self._query_expansion
  301. elif self.operator == ";":
  302. expansion = self._semi_path_expansion
  303. else:
  304. expansion = self._string_expansion
  305. expanded = expansion(name, value, opts["explode"], opts["prefix"])
  306. if expanded is not None:
  307. return_values.append(expanded)
  308. value = ""
  309. if return_values:
  310. value = self.start + self.join_str.join(return_values)
  311. return {self.original: value}
  312. def is_list_of_tuples(
  313. value: t.Any,
  314. ) -> t.Tuple[bool, t.Optional[t.Sequence[t.Tuple[str, ScalarVariableValue]]]]:
  315. if (
  316. not value
  317. or not isinstance(value, (list, tuple))
  318. or not all(isinstance(t, tuple) and len(t) == 2 for t in value)
  319. ):
  320. return False, None
  321. return True, value
  322. def list_test(value: t.Any) -> bool:
  323. return isinstance(value, (list, tuple))
  324. def dict_test(value: t.Any) -> bool:
  325. return isinstance(value, (dict, collections.abc.MutableMapping))
  326. def _encode(value: t.AnyStr, encoding: str = "utf-8") -> bytes:
  327. if isinstance(value, str):
  328. return value.encode(encoding)
  329. return value
  330. def quote(value: t.Any, safe: str) -> str:
  331. if not isinstance(value, (str, bytes)):
  332. value = str(value)
  333. return urllib.parse.quote(_encode(value), safe)