| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- """
- uritemplate.variable
- ====================
- This module contains the URIVariable class which powers the URITemplate class.
- What treasures await you:
- - URIVariable class
- You see a hammer in front of you.
- What do you do?
- >
- """
- import collections.abc
- import typing as t
- import urllib.parse
- ScalarVariableValue = t.Union[int, float, complex, str]
- VariableValue = t.Union[
- t.Sequence[ScalarVariableValue],
- t.Mapping[str, ScalarVariableValue],
- t.Tuple[str, ScalarVariableValue],
- ScalarVariableValue,
- ]
- VariableValueDict = t.Dict[str, VariableValue]
- class URIVariable:
- """This object validates everything inside the URITemplate object.
- It validates template expansions and will truncate length as decided by
- the template.
- Please note that just like the :class:`URITemplate <URITemplate>`, this
- object's ``__str__`` and ``__repr__`` methods do not return the same
- information. Calling ``str(var)`` will return the original variable.
- This object does the majority of the heavy lifting. The ``URITemplate``
- object finds the variables in the URI and then creates ``URIVariable``
- objects. Expansions of the URI are handled by each ``URIVariable``
- object. ``URIVariable.expand()`` returns a dictionary of the original
- variable and the expanded value. Check that method's documentation for
- more information.
- """
- operators = ("+", "#", ".", "/", ";", "?", "&", "|", "!", "@")
- reserved = ":/?#[]@!$&'()*+,;="
- def __init__(self, var: str):
- #: The original string that comes through with the variable
- self.original: str = var
- #: The operator for the variable
- self.operator: str = ""
- #: List of safe characters when quoting the string
- self.safe: str = ""
- #: List of variables in this variable
- self.variables: t.List[
- t.Tuple[str, t.MutableMapping[str, t.Any]]
- ] = []
- #: List of variable names
- self.variable_names: t.List[str] = []
- #: List of defaults passed in
- self.defaults: t.MutableMapping[str, ScalarVariableValue] = {}
- # Parse the variable itself.
- self.parse()
- self.post_parse()
- def __repr__(self) -> str:
- return "URIVariable(%s)" % self
- def __str__(self) -> str:
- return self.original
- def parse(self) -> None:
- """Parse the variable.
- This finds the:
- - operator,
- - set of safe characters,
- - variables, and
- - defaults.
- """
- var_list_str = self.original
- if self.original[0] in URIVariable.operators:
- self.operator = self.original[0]
- var_list_str = self.original[1:]
- if self.operator in URIVariable.operators[:2]:
- self.safe = URIVariable.reserved
- var_list = var_list_str.split(",")
- for var in var_list:
- default_val = None
- name = var
- if "=" in var:
- name, default_val = tuple(var.split("=", 1))
- explode = False
- if name.endswith("*"):
- explode = True
- name = name[:-1]
- prefix: t.Optional[int] = None
- if ":" in name:
- name, prefix_str = tuple(name.split(":", 1))
- prefix = int(prefix_str)
- if default_val:
- self.defaults[name] = default_val
- self.variables.append(
- (name, {"explode": explode, "prefix": prefix})
- )
- self.variable_names = [varname for (varname, _) in self.variables]
- def post_parse(self) -> None:
- """Set ``start``, ``join_str`` and ``safe`` attributes.
- After parsing the variable, we need to set up these attributes and it
- only makes sense to do it in a more easily testable way.
- """
- self.safe = ""
- self.start = self.join_str = self.operator
- if self.operator == "+":
- self.start = ""
- if self.operator in ("+", "#", ""):
- self.join_str = ","
- if self.operator == "#":
- self.start = "#"
- if self.operator == "?":
- self.start = "?"
- self.join_str = "&"
- if self.operator in ("+", "#"):
- self.safe = URIVariable.reserved
- def _query_expansion(
- self,
- name: str,
- value: VariableValue,
- explode: bool,
- prefix: t.Optional[int],
- ) -> t.Optional[str]:
- """Expansion method for the '?' and '&' operators."""
- if value is None:
- return None
- tuples, items = is_list_of_tuples(value)
- safe = self.safe
- if list_test(value) and not tuples:
- if not value:
- return None
- value = t.cast(t.Sequence[ScalarVariableValue], value)
- if explode:
- return self.join_str.join(
- f"{name}={quote(v, safe)}" for v in value
- )
- else:
- value = ",".join(quote(v, safe) for v in value)
- return f"{name}={value}"
- if dict_test(value) or tuples:
- if not value:
- return None
- value = t.cast(t.Mapping[str, ScalarVariableValue], value)
- items = items or sorted(value.items())
- if explode:
- return self.join_str.join(
- f"{quote(k, safe)}={quote(v, safe)}" for k, v in items
- )
- else:
- value = ",".join(
- f"{quote(k, safe)},{quote(v, safe)}" for k, v in items
- )
- return f"{name}={value}"
- if value:
- value = t.cast(t.Text, value)
- value = value[:prefix] if prefix else value
- return f"{name}={quote(value, safe)}"
- return name + "="
- def _label_path_expansion(
- self,
- name: str,
- value: VariableValue,
- explode: bool,
- prefix: t.Optional[int],
- ) -> t.Optional[str]:
- """Label and path expansion method.
- Expands for operators: '/', '.'
- """
- join_str = self.join_str
- safe = self.safe
- if value is None or (
- not isinstance(value, (str, int, float, complex))
- and len(value) == 0
- ):
- return None
- tuples, items = is_list_of_tuples(value)
- if list_test(value) and not tuples:
- if not explode:
- join_str = ","
- value = t.cast(t.Sequence[ScalarVariableValue], value)
- fragments = [quote(v, safe) for v in value if v is not None]
- return join_str.join(fragments) if fragments else None
- if dict_test(value) or tuples:
- value = t.cast(t.Mapping[str, ScalarVariableValue], value)
- items = items or sorted(value.items())
- format_str = "%s=%s"
- if not explode:
- format_str = "%s,%s"
- join_str = ","
- expanded = join_str.join(
- format_str % (quote(k, safe), quote(v, safe))
- for k, v in items
- if v is not None
- )
- return expanded if expanded else None
- value = t.cast(t.Text, value)
- value = value[:prefix] if prefix else value
- return quote(value, safe)
- def _semi_path_expansion(
- self,
- name: str,
- value: VariableValue,
- explode: bool,
- prefix: t.Optional[int],
- ) -> t.Optional[str]:
- """Expansion method for ';' operator."""
- join_str = self.join_str
- safe = self.safe
- if value is None:
- return None
- if self.operator == "?":
- join_str = "&"
- tuples, items = is_list_of_tuples(value)
- if list_test(value) and not tuples:
- value = t.cast(t.Sequence[ScalarVariableValue], value)
- if explode:
- expanded = join_str.join(
- f"{name}={quote(v, safe)}" for v in value if v is not None
- )
- return expanded if expanded else None
- else:
- value = ",".join(quote(v, safe) for v in value)
- return f"{name}={value}"
- if dict_test(value) or tuples:
- value = t.cast(t.Mapping[str, ScalarVariableValue], value)
- items = items or sorted(value.items())
- if explode:
- return join_str.join(
- f"{quote(k, safe)}={quote(v, safe)}"
- for k, v in items
- if v is not None
- )
- else:
- expanded = ",".join(
- f"{quote(k, safe)},{quote(v, safe)}"
- for k, v in items
- if v is not None
- )
- return f"{name}={expanded}"
- value = t.cast(t.Text, value)
- value = value[:prefix] if prefix else value
- if value:
- return f"{name}={quote(value, safe)}"
- return name
- def _string_expansion(
- self,
- name: str,
- value: VariableValue,
- explode: bool,
- prefix: t.Optional[int],
- ) -> t.Optional[str]:
- if value is None:
- return None
- tuples, items = is_list_of_tuples(value)
- if list_test(value) and not tuples:
- value = t.cast(t.Sequence[ScalarVariableValue], value)
- return ",".join(quote(v, self.safe) for v in value)
- if dict_test(value) or tuples:
- value = t.cast(t.Mapping[str, ScalarVariableValue], value)
- items = items or sorted(value.items())
- format_str = "%s=%s" if explode else "%s,%s"
- return ",".join(
- format_str % (quote(k, self.safe), quote(v, self.safe))
- for k, v in items
- )
- value = t.cast(t.Text, value)
- value = value[:prefix] if prefix else value
- return quote(value, self.safe)
- def expand(
- self, var_dict: t.Optional[VariableValueDict] = None
- ) -> t.Mapping[str, str]:
- """Expand the variable in question.
- Using ``var_dict`` and the previously parsed defaults, expand this
- variable and subvariables.
- :param dict var_dict: dictionary of key-value pairs to be used during
- expansion
- :returns: dict(variable=value)
- Examples::
- # (1)
- v = URIVariable('/var')
- expansion = v.expand({'var': 'value'})
- print(expansion)
- # => {'/var': '/value'}
- # (2)
- v = URIVariable('?var,hello,x,y')
- expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
- 'x': '1024', 'y': '768'})
- print(expansion)
- # => {'?var,hello,x,y':
- # '?var=value&hello=Hello%20World%21&x=1024&y=768'}
- """
- return_values = []
- if var_dict is None:
- return {self.original: self.original}
- for name, opts in self.variables:
- value = var_dict.get(name, None)
- if not value and value != "" and name in self.defaults:
- value = self.defaults[name]
- if value is None:
- continue
- expanded = None
- if self.operator in ("/", "."):
- expansion = self._label_path_expansion
- elif self.operator in ("?", "&"):
- expansion = self._query_expansion
- elif self.operator == ";":
- expansion = self._semi_path_expansion
- else:
- expansion = self._string_expansion
- expanded = expansion(name, value, opts["explode"], opts["prefix"])
- if expanded is not None:
- return_values.append(expanded)
- value = ""
- if return_values:
- value = self.start + self.join_str.join(return_values)
- return {self.original: value}
- def is_list_of_tuples(
- value: t.Any,
- ) -> t.Tuple[bool, t.Optional[t.Sequence[t.Tuple[str, ScalarVariableValue]]]]:
- if (
- not value
- or not isinstance(value, (list, tuple))
- or not all(isinstance(t, tuple) and len(t) == 2 for t in value)
- ):
- return False, None
- return True, value
- def list_test(value: t.Any) -> bool:
- return isinstance(value, (list, tuple))
- def dict_test(value: t.Any) -> bool:
- return isinstance(value, (dict, collections.abc.MutableMapping))
- def _encode(value: t.AnyStr, encoding: str = "utf-8") -> bytes:
- if isinstance(value, str):
- return value.encode(encoding)
- return value
- def quote(value: t.Any, safe: str) -> str:
- if not isinstance(value, (str, bytes)):
- value = str(value)
- return urllib.parse.quote(_encode(value), safe)
|