Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/uritemplate/variable.py: 74%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""
18import collections.abc
19import enum
20import string
21import typing as t
22import urllib.parse
24ScalarVariableValue = t.Union[int, float, complex, str, None]
25VariableValue = t.Union[
26 t.Sequence[ScalarVariableValue],
27 t.List[ScalarVariableValue],
28 t.Mapping[str, ScalarVariableValue],
29 t.Tuple[str, ScalarVariableValue],
30 ScalarVariableValue,
31]
32VariableValueMapping = t.Mapping[str, VariableValue]
35_UNRESERVED_CHARACTERS: t.Final[str] = (
36 f"{string.ascii_letters}{string.digits}~-_."
37)
38_GEN_DELIMS: t.Final[str] = ":/?#[]@"
39_SUB_DELIMS: t.Final[str] = "!$&'()*+,;="
40_RESERVED_CHARACTERS: t.Final[str] = f"{_GEN_DELIMS}{_SUB_DELIMS}"
43class Operator(enum.Enum):
44 # Section 2.2. Expressions
45 # expression = "{" [ operator ] variable-list "}"
46 # operator = op-level2 / op-level3 / op-reserve
47 # op-level2 = "+" / "#"
48 # op-level3 = "." / "/" / ";" / "?" / "&"
49 # op-reserve = "=" / "," / "!" / "@" / "|"
50 default = "" # 3.2.2. Simple String Expansiona: {var}
51 # Operator Level 2 (op-level2)
52 reserved = "+" # 3.2.3. Reserved Expansion: {+var}
53 fragment = "#" # 3.2.4. Fragment Expansion: {#var}
54 # Operator Level 3 (op-level3)
55 # 3.2.5. Label Expansion with Dot-Prefix: {.var}
56 label_with_dot_prefix = "."
57 path_segment = "/" # 3.2.6. Path Segment Expansion: {/var}
58 path_style_parameter = (
59 ";" # 3.2.7. Path-Style Parameter Expansion: {;var}
60 )
61 form_style_query = "?" # 3.2.8. Form-Style Query Expansion: {?var}
62 # 3.2.9. Form-Style Query Continuation: {&var}
63 form_style_query_continuation = "&"
64 # Reserved Operators (op-reserve)
65 reserved_eq = "="
66 reserved_comma = ","
67 reserved_bang = "!"
68 reserved_at = "@"
69 reserved_pipe = "|"
71 def reserved_characters(self) -> str:
72 # TODO: Re-enable after un-commenting 3.9
73 # match self:
74 # case Operator.reserved:
75 # return _RESERVED_CHARACTERS + "%"
76 # # case Operator.default | Operator.reserved | Operator.fragment:
77 # case Operator.fragment:
78 # return _RESERVED_CHARACTERS
79 # case _:
80 # return ""
81 if self == Operator.reserved:
82 return _RESERVED_CHARACTERS + "%"
83 if self == Operator.fragment:
84 return _RESERVED_CHARACTERS
85 return ""
87 def expansion_separator(self) -> str:
88 """Identify the separator used during expansion.
90 Per `Section 3.2.1. Variable Expansion`_:
92 ====== =========== =========
93 Type Separator
94 ====== =========== =========
95 ``","`` (default)
96 ``+`` ``","``
97 ``#`` ``","``
98 ``.`` ``"."``
99 ``/`` ``"/"``
100 ``;`` ``";"``
101 ``?`` ``"&"``
102 ``&`` ``"&"``
103 ====== =========== =========
105 .. _`Section 3.2.1. Variable Expansion`:
106 https://www.rfc-editor.org/rfc/rfc6570#section-3.2.1
107 """
108 if self == Operator.label_with_dot_prefix:
109 return "."
110 if self == Operator.path_segment:
111 return "/"
112 if self == Operator.path_style_parameter:
113 return ";"
114 if (
115 self == Operator.form_style_query
116 or self == Operator.form_style_query_continuation
117 ):
118 return "&"
119 # if self == Operator.reserved or self == Operator.fragment:
120 # return ","
121 return ","
122 # match self:
123 # case Operator.label_with_dot_prefix:
124 # return "."
125 # case Operator.path_segment:
126 # return "/"
127 # case Operator.path_style_parameter:
128 # return ";"
129 # case (
130 # Operator.form_style_query |
131 # Operator.form_style_query_continuation
132 # ):
133 # return "&"
134 # case Operator.reserved | Operator.fragment:
135 # return ","
136 # case _:
137 # return ","
139 def variable_prefix(self) -> str:
140 if self == Operator.reserved:
141 return ""
142 return t.cast(str, self.value)
143 # match self:
144 # case Operator.reserved:
145 # return ""
146 # case _:
147 # return t.cast(str, self.value)
149 def _always_quote(self, value: str) -> str:
150 return quote(value, "")
152 def _only_quote_unquoted_characters(self, value: str) -> str:
153 if urllib.parse.unquote(value) == value:
154 return quote(value, _RESERVED_CHARACTERS)
155 return value
157 def quote(self, value: t.Any) -> str:
158 if not isinstance(value, (str, bytes)):
159 value = str(value)
160 if isinstance(value, bytes):
161 value = value.decode()
163 if self == Operator.reserved or self == Operator.fragment:
164 return self._only_quote_unquoted_characters(value)
165 return self._always_quote(value)
167 @staticmethod
168 def from_string(s: str) -> "Operator":
169 return _operators.get(s, Operator.default)
172_operators: t.Final[t.Dict[str, Operator]] = {
173 "+": Operator.reserved,
174 "#": Operator.fragment,
175 ".": Operator.label_with_dot_prefix,
176 "/": Operator.path_segment,
177 ";": Operator.path_style_parameter,
178 "?": Operator.form_style_query,
179 "&": Operator.form_style_query_continuation,
180 "!": Operator.reserved_bang,
181 "|": Operator.reserved_pipe,
182 "@": Operator.reserved_at,
183 "=": Operator.reserved_eq,
184 ",": Operator.reserved_comma,
185}
188class URIVariable:
189 """This object validates everything inside the URITemplate object.
191 It validates template expansions and will truncate length as decided by
192 the template.
194 Please note that just like the :class:`URITemplate <URITemplate>`, this
195 object's ``__str__`` and ``__repr__`` methods do not return the same
196 information. Calling ``str(var)`` will return the original variable.
198 This object does the majority of the heavy lifting. The ``URITemplate``
199 object finds the variables in the URI and then creates ``URIVariable``
200 objects. Expansions of the URI are handled by each ``URIVariable``
201 object. ``URIVariable.expand()`` returns a dictionary of the original
202 variable and the expanded value. Check that method's documentation for
203 more information.
205 """
207 def __init__(self, var: str):
208 #: The original string that comes through with the variable
209 self.original: str = var
210 #: The operator for the variable
211 self.operator: Operator = Operator.default
212 #: List of variables in this variable
213 self.variables: t.List[t.Tuple[str, t.MutableMapping[str, t.Any]]] = (
214 []
215 )
216 #: List of variable names
217 self.variable_names: t.List[str] = []
218 #: List of defaults passed in
219 self.defaults: t.MutableMapping[str, ScalarVariableValue] = {}
220 # Parse the variable itself.
221 self.parse()
223 def __repr__(self) -> str:
224 return "URIVariable(%s)" % self
226 def __str__(self) -> str:
227 return self.original
229 def parse(self) -> None:
230 """Parse the variable.
232 This finds the:
233 - operator,
234 - set of safe characters,
235 - variables, and
236 - defaults.
238 """
239 var_list_str = self.original
240 if (operator_str := self.original[0]) in _operators:
241 self.operator = Operator.from_string(operator_str)
242 var_list_str = self.original[1:]
244 var_list = var_list_str.split(",")
246 for var in var_list:
247 default_val = None
248 name = var
249 # NOTE(sigmavirus24): This is from an earlier draft but is not in
250 # the specification
251 if "=" in var:
252 name, default_val = tuple(var.split("=", 1))
254 explode = name.endswith("*")
255 name = name.rstrip("*")
257 prefix: t.Optional[int] = None
258 if ":" in name:
259 name, prefix_str = tuple(name.split(":", 1))
260 prefix = int(prefix_str, 10)
262 if default_val:
263 self.defaults[name] = default_val
265 self.variables.append(
266 (name, {"explode": explode, "prefix": prefix})
267 )
269 self.variable_names = [varname for (varname, _) in self.variables]
271 def _query_expansion(
272 self,
273 name: str,
274 value: VariableValue,
275 explode: bool,
276 prefix: t.Optional[int],
277 ) -> t.Optional[str]:
278 """Expansion method for the '?' and '&' operators."""
279 if value is None:
280 return None
282 tuples, items = is_list_of_tuples(value)
284 safe = self.operator.reserved_characters()
285 _quote = self.operator.quote
286 if list_test(value) and not tuples:
287 if not value:
288 return None
289 value = t.cast(t.Sequence[ScalarVariableValue], value)
290 if explode:
291 return self.operator.expansion_separator().join(
292 f"{name}={_quote(v)}" for v in value
293 )
294 else:
295 value = ",".join(_quote(v) for v in value)
296 return f"{name}={value}"
298 if dict_test(value) or tuples:
299 if not value:
300 return None
301 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
302 items = items or sorted(value.items())
303 if explode:
304 return self.operator.expansion_separator().join(
305 f"{quote(k, safe)}={_quote(v)}" for k, v in items
306 )
307 else:
308 value = ",".join(
309 f"{quote(k, safe)},{_quote(v)}" for k, v in items
310 )
311 return f"{name}={value}"
313 if value:
314 value = t.cast(t.Text, value)
315 value = value[:prefix] if prefix else value
316 return f"{name}={_quote(value)}"
317 return name + "="
319 def _label_path_expansion(
320 self,
321 name: str,
322 value: VariableValue,
323 explode: bool,
324 prefix: t.Optional[int],
325 ) -> t.Optional[str]:
326 """Label and path expansion method.
328 Expands for operators: '/', '.'
330 """
331 join_str = self.operator.expansion_separator()
332 safe = self.operator.reserved_characters()
334 if value is None or (
335 not isinstance(value, (str, int, float, complex))
336 and len(value) == 0
337 ):
338 return None
340 tuples, items = is_list_of_tuples(value)
342 if list_test(value) and not tuples:
343 if not explode:
344 join_str = ","
346 value = t.cast(t.Sequence[ScalarVariableValue], value)
347 fragments = [
348 self.operator.quote(v) for v in value if v is not None
349 ]
350 return join_str.join(fragments) if fragments else None
352 if dict_test(value) or tuples:
353 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
354 items = items or sorted(value.items())
355 format_str = "%s=%s"
356 if not explode:
357 format_str = "%s,%s"
358 join_str = ","
360 expanded = join_str.join(
361 format_str % (quote(k, safe), self.operator.quote(v))
362 for k, v in items
363 if v is not None
364 )
365 return expanded if expanded else None
367 value = t.cast(t.Text, value)
368 value = value[:prefix] if prefix else value
369 return self.operator.quote(value)
371 def _semi_path_expansion(
372 self,
373 name: str,
374 value: VariableValue,
375 explode: bool,
376 prefix: t.Optional[int],
377 ) -> t.Optional[str]:
378 """Expansion method for ';' operator."""
379 join_str = self.operator.expansion_separator()
380 safe = self.operator.reserved_characters()
382 if value is None:
383 return None
385 tuples, items = is_list_of_tuples(value)
387 if list_test(value) and not tuples:
388 value = t.cast(t.Sequence[ScalarVariableValue], value)
389 if explode:
390 expanded = join_str.join(
391 f"{name}={quote(v, safe)}" for v in value if v is not None
392 )
393 return expanded if expanded else None
394 else:
395 value = ",".join(quote(v, safe) for v in value)
396 return f"{name}={value}"
398 if dict_test(value) or tuples:
399 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
400 items = items or sorted(value.items())
402 if explode:
403 return join_str.join(
404 f"{quote(k, safe)}={self.operator.quote(v)}"
405 for k, v in items
406 if v is not None
407 )
408 else:
409 expanded = ",".join(
410 f"{quote(k, safe)},{self.operator.quote(v)}"
411 for k, v in items
412 if v is not None
413 )
414 return f"{name}={expanded}"
416 value = t.cast(t.Text, value)
417 value = value[:prefix] if prefix else value
418 if value:
419 return f"{name}={self.operator.quote(value)}"
421 return name
423 def _string_expansion(
424 self,
425 name: str,
426 value: VariableValue,
427 explode: bool,
428 prefix: t.Optional[int],
429 ) -> t.Optional[str]:
430 if value is None:
431 return None
433 tuples, items = is_list_of_tuples(value)
435 if list_test(value) and not tuples:
436 value = t.cast(t.Sequence[ScalarVariableValue], value)
437 return ",".join(self.operator.quote(v) for v in value)
439 if dict_test(value) or tuples:
440 value = t.cast(t.Mapping[str, ScalarVariableValue], value)
441 items = items or sorted(value.items())
442 format_str = "%s=%s" if explode else "%s,%s"
444 return ",".join(
445 format_str % (self.operator.quote(k), self.operator.quote(v))
446 for k, v in items
447 )
449 value = t.cast(t.Text, value)
450 value = value[:prefix] if prefix else value
451 return self.operator.quote(value)
453 def expand(
454 self, var_dict: t.Optional[VariableValueMapping] = None
455 ) -> t.Mapping[str, str]:
456 """Expand the variable in question.
458 Using ``var_dict`` and the previously parsed defaults, expand this
459 variable and subvariables.
461 :param dict var_dict: dictionary of key-value pairs to be used during
462 expansion
463 :returns: dict(variable=value)
465 Examples::
467 # (1)
468 v = URIVariable('/var')
469 expansion = v.expand({'var': 'value'})
470 print(expansion)
471 # => {'/var': '/value'}
473 # (2)
474 v = URIVariable('?var,hello,x,y')
475 expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
476 'x': '1024', 'y': '768'})
477 print(expansion)
478 # => {'?var,hello,x,y':
479 # '?var=value&hello=Hello%20World%21&x=1024&y=768'}
481 """
482 return_values = []
483 if var_dict is None:
484 return {self.original: self.original}
486 for name, opts in self.variables:
487 value = var_dict.get(name, None)
488 if not value and value != "" and name in self.defaults:
489 value = self.defaults[name]
491 if value is None:
492 continue
494 expanded = None
495 if (
496 self.operator == Operator.path_segment
497 or self.operator == Operator.label_with_dot_prefix
498 ):
499 expansion = self._label_path_expansion
500 elif (
501 self.operator == Operator.form_style_query
502 or self.operator == Operator.form_style_query_continuation
503 ):
504 expansion = self._query_expansion
505 elif self.operator == Operator.path_style_parameter:
506 expansion = self._semi_path_expansion
507 else:
508 expansion = self._string_expansion
509 # match self.operator:
510 # case Operator.path_segment | Operator.label_with_dot_prefix:
511 # expansion = self._label_path_expansion
512 # case (Operator.form_style_query |
513 # Operator.form_style_query_continuation):
514 # expansion = self._query_expansion
515 # case Operator.path_style_parameter:
516 # expansion = self._semi_path_expansion
517 # case _:
518 # expansion = self._string_expansion
520 expanded = expansion(name, value, opts["explode"], opts["prefix"])
522 if expanded is not None:
523 return_values.append(expanded)
525 value = ""
526 if return_values:
527 value = (
528 self.operator.variable_prefix()
529 + self.operator.expansion_separator().join(return_values)
530 )
531 return {self.original: value}
534def is_list_of_tuples(
535 value: t.Any,
536) -> t.Tuple[bool, t.Optional[t.Sequence[t.Tuple[str, ScalarVariableValue]]]]:
537 if (
538 not value
539 or not isinstance(value, (list, tuple))
540 or not all(isinstance(t, tuple) and len(t) == 2 for t in value)
541 ):
542 return False, None
544 return True, value
547def list_test(value: t.Any) -> bool:
548 return isinstance(value, (list, tuple))
551def dict_test(value: t.Any) -> bool:
552 return isinstance(value, (dict, collections.abc.MutableMapping))
555def _encode(value: t.AnyStr, encoding: str = "utf-8") -> bytes:
556 if isinstance(value, str):
557 return value.encode(encoding)
558 return value
561def quote(value: t.Any, safe: str) -> str:
562 if not isinstance(value, (str, bytes)):
563 value = str(value)
564 return urllib.parse.quote(_encode(value), safe)