Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/uritemplate/variable.py: 7%
193 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
3uritemplate.variable
4====================
6This module contains the URIVariable class which powers the URITemplate class.
8What treasures await you:
10- URIVariable class
12You see a hammer in front of you.
13What do you do?
14>
16"""
17import collections.abc
18import typing as t
19import urllib.parse
21ScalarVariableValue = t.Union[int, float, complex, str]
22VariableValue = t.Union[
23 t.Sequence[ScalarVariableValue],
24 t.Mapping[str, ScalarVariableValue],
25 t.Tuple[str, ScalarVariableValue],
26 ScalarVariableValue,
27]
28VariableValueDict = t.Dict[str, VariableValue]
31class URIVariable:
33 """This object validates everything inside the URITemplate object.
35 It validates template expansions and will truncate length as decided by
36 the template.
38 Please note that just like the :class:`URITemplate <URITemplate>`, this
39 object's ``__str__`` and ``__repr__`` methods do not return the same
40 information. Calling ``str(var)`` will return the original variable.
42 This object does the majority of the heavy lifting. The ``URITemplate``
43 object finds the variables in the URI and then creates ``URIVariable``
44 objects. Expansions of the URI are handled by each ``URIVariable``
45 object. ``URIVariable.expand()`` returns a dictionary of the original
46 variable and the expanded value. Check that method's documentation for
47 more information.
49 """
51 operators = ("+", "#", ".", "/", ";", "?", "&", "|", "!", "@")
52 reserved = ":/?#[]@!$&'()*+,;="
54 def __init__(self, var: str):
55 #: The original string that comes through with the variable
56 self.original: str = var
57 #: The operator for the variable
58 self.operator: str = ""
59 #: List of safe characters when quoting the string
60 self.safe: str = ""
61 #: List of variables in this variable
62 self.variables: t.List[
63 t.Tuple[str, t.MutableMapping[str, t.Any]]
64 ] = []
65 #: List of variable names
66 self.variable_names: t.List[str] = []
67 #: List of defaults passed in
68 self.defaults: t.MutableMapping[str, ScalarVariableValue] = {}
69 # Parse the variable itself.
70 self.parse()
71 self.post_parse()
73 def __repr__(self) -> str:
74 return "URIVariable(%s)" % self
76 def __str__(self) -> str:
77 return self.original
79 def parse(self) -> None:
80 """Parse the variable.
82 This finds the:
83 - operator,
84 - set of safe characters,
85 - variables, and
86 - defaults.
88 """
89 var_list_str = self.original
90 if self.original[0] in URIVariable.operators:
91 self.operator = self.original[0]
92 var_list_str = self.original[1:]
94 if self.operator in URIVariable.operators[:2]:
95 self.safe = URIVariable.reserved
97 var_list = var_list_str.split(",")
99 for var in var_list:
100 default_val = None
101 name = var
102 if "=" in var:
103 name, default_val = tuple(var.split("=", 1))
105 explode = False
106 if name.endswith("*"):
107 explode = True
108 name = name[:-1]
110 prefix: t.Optional[int] = None
111 if ":" in name:
112 name, prefix_str = tuple(name.split(":", 1))
113 prefix = int(prefix_str)
115 if default_val:
116 self.defaults[name] = default_val
118 self.variables.append(
119 (name, {"explode": explode, "prefix": prefix})
120 )
122 self.variable_names = [varname for (varname, _) in self.variables]
124 def post_parse(self) -> None:
125 """Set ``start``, ``join_str`` and ``safe`` attributes.
127 After parsing the variable, we need to set up these attributes and it
128 only makes sense to do it in a more easily testable way.
129 """
130 self.safe = ""
131 self.start = self.join_str = self.operator
132 if self.operator == "+":
133 self.start = ""
134 if self.operator in ("+", "#", ""):
135 self.join_str = ","
136 if self.operator == "#":
137 self.start = "#"
138 if self.operator == "?":
139 self.start = "?"
140 self.join_str = "&"
142 if self.operator in ("+", "#"):
143 self.safe = URIVariable.reserved
145 def _query_expansion(
146 self,
147 name: str,
148 value: VariableValue,
149 explode: bool,
150 prefix: t.Optional[int],
151 ) -> t.Optional[str]:
152 """Expansion method for the '?' and '&' operators."""
153 if value is None:
154 return None
156 tuples, items = is_list_of_tuples(value)
158 safe = self.safe
159 if list_test(value) and not tuples:
160 if not value:
161 return None
162 value = t.cast(t.Sequence[ScalarVariableValue], value)
163 if explode:
164 return self.join_str.join(
165 f"{name}={quote(v, safe)}" for v in value
166 )
167 else:
168 value = ",".join(quote(v, safe) for v in value)
169 return f"{name}={value}"
171 if dict_test(value) or tuples:
172 if not value:
173 return None
174 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
175 items = items or sorted(value.items())
176 if explode:
177 return self.join_str.join(
178 f"{quote(k, safe)}={quote(v, safe)}" for k, v in items
179 )
180 else:
181 value = ",".join(
182 f"{quote(k, safe)},{quote(v, safe)}" for k, v in items
183 )
184 return f"{name}={value}"
186 if value:
187 value = t.cast(t.Text, value)
188 value = value[:prefix] if prefix else value
189 return f"{name}={quote(value, safe)}"
190 return name + "="
192 def _label_path_expansion(
193 self,
194 name: str,
195 value: VariableValue,
196 explode: bool,
197 prefix: t.Optional[int],
198 ) -> t.Optional[str]:
199 """Label and path expansion method.
201 Expands for operators: '/', '.'
203 """
204 join_str = self.join_str
205 safe = self.safe
207 if value is None or (
208 not isinstance(value, (str, int, float, complex))
209 and len(value) == 0
210 ):
211 return None
213 tuples, items = is_list_of_tuples(value)
215 if list_test(value) and not tuples:
216 if not explode:
217 join_str = ","
219 value = t.cast(t.Sequence[ScalarVariableValue], value)
220 fragments = [quote(v, safe) for v in value if v is not None]
221 return join_str.join(fragments) if fragments else None
223 if dict_test(value) or tuples:
224 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
225 items = items or sorted(value.items())
226 format_str = "%s=%s"
227 if not explode:
228 format_str = "%s,%s"
229 join_str = ","
231 expanded = join_str.join(
232 format_str % (quote(k, safe), quote(v, safe))
233 for k, v in items
234 if v is not None
235 )
236 return expanded if expanded else None
238 value = t.cast(t.Text, value)
239 value = value[:prefix] if prefix else value
240 return quote(value, safe)
242 def _semi_path_expansion(
243 self,
244 name: str,
245 value: VariableValue,
246 explode: bool,
247 prefix: t.Optional[int],
248 ) -> t.Optional[str]:
249 """Expansion method for ';' operator."""
250 join_str = self.join_str
251 safe = self.safe
253 if value is None:
254 return None
256 if self.operator == "?":
257 join_str = "&"
259 tuples, items = is_list_of_tuples(value)
261 if list_test(value) and not tuples:
262 value = t.cast(t.Sequence[ScalarVariableValue], value)
263 if explode:
264 expanded = join_str.join(
265 f"{name}={quote(v, safe)}" for v in value if v is not None
266 )
267 return expanded if expanded else None
268 else:
269 value = ",".join(quote(v, safe) for v in value)
270 return f"{name}={value}"
272 if dict_test(value) or tuples:
273 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
274 items = items or sorted(value.items())
276 if explode:
277 return join_str.join(
278 f"{quote(k, safe)}={quote(v, safe)}"
279 for k, v in items
280 if v is not None
281 )
282 else:
283 expanded = ",".join(
284 f"{quote(k, safe)},{quote(v, safe)}"
285 for k, v in items
286 if v is not None
287 )
288 return f"{name}={expanded}"
290 value = t.cast(t.Text, value)
291 value = value[:prefix] if prefix else value
292 if value:
293 return f"{name}={quote(value, safe)}"
295 return name
297 def _string_expansion(
298 self,
299 name: str,
300 value: VariableValue,
301 explode: bool,
302 prefix: t.Optional[int],
303 ) -> t.Optional[str]:
304 if value is None:
305 return None
307 tuples, items = is_list_of_tuples(value)
309 if list_test(value) and not tuples:
310 value = t.cast(t.Sequence[ScalarVariableValue], value)
311 return ",".join(quote(v, self.safe) for v in value)
313 if dict_test(value) or tuples:
314 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
315 items = items or sorted(value.items())
316 format_str = "%s=%s" if explode else "%s,%s"
318 return ",".join(
319 format_str % (quote(k, self.safe), quote(v, self.safe))
320 for k, v in items
321 )
323 value = t.cast(t.Text, value)
324 value = value[:prefix] if prefix else value
325 return quote(value, self.safe)
327 def expand(
328 self, var_dict: t.Optional[VariableValueDict] = None
329 ) -> t.Mapping[str, str]:
330 """Expand the variable in question.
332 Using ``var_dict`` and the previously parsed defaults, expand this
333 variable and subvariables.
335 :param dict var_dict: dictionary of key-value pairs to be used during
336 expansion
337 :returns: dict(variable=value)
339 Examples::
341 # (1)
342 v = URIVariable('/var')
343 expansion = v.expand({'var': 'value'})
344 print(expansion)
345 # => {'/var': '/value'}
347 # (2)
348 v = URIVariable('?var,hello,x,y')
349 expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
350 'x': '1024', 'y': '768'})
351 print(expansion)
352 # => {'?var,hello,x,y':
353 # '?var=value&hello=Hello%20World%21&x=1024&y=768'}
355 """
356 return_values = []
357 if var_dict is None:
358 return {self.original: self.original}
360 for name, opts in self.variables:
361 value = var_dict.get(name, None)
362 if not value and value != "" and name in self.defaults:
363 value = self.defaults[name]
365 if value is None:
366 continue
368 expanded = None
369 if self.operator in ("/", "."):
370 expansion = self._label_path_expansion
371 elif self.operator in ("?", "&"):
372 expansion = self._query_expansion
373 elif self.operator == ";":
374 expansion = self._semi_path_expansion
375 else:
376 expansion = self._string_expansion
378 expanded = expansion(name, value, opts["explode"], opts["prefix"])
380 if expanded is not None:
381 return_values.append(expanded)
383 value = ""
384 if return_values:
385 value = self.start + self.join_str.join(return_values)
386 return {self.original: value}
389def is_list_of_tuples(
390 value: t.Any,
391) -> t.Tuple[bool, t.Optional[t.Sequence[t.Tuple[str, ScalarVariableValue]]]]:
392 if (
393 not value
394 or not isinstance(value, (list, tuple))
395 or not all(isinstance(t, tuple) and len(t) == 2 for t in value)
396 ):
397 return False, None
399 return True, value
402def list_test(value: t.Any) -> bool:
403 return isinstance(value, (list, tuple))
406def dict_test(value: t.Any) -> bool:
407 return isinstance(value, (dict, collections.abc.MutableMapping))
410def _encode(value: t.AnyStr, encoding: str = "utf-8") -> bytes:
411 if isinstance(value, str):
412 return value.encode(encoding)
413 return value
416def quote(value: t.Any, safe: str) -> str:
417 if not isinstance(value, (str, bytes)):
418 value = str(value)
419 return urllib.parse.quote(_encode(value), safe)