387 lines
11 KiB
Python
387 lines
11 KiB
Python
|
"""
|
||
|
|
||
|
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 sys
|
||
|
|
||
|
try:
|
||
|
import collections.abc as collections_abc
|
||
|
except ImportError:
|
||
|
import collections as collections_abc
|
||
|
|
||
|
if sys.version_info.major == 2:
|
||
|
import urllib
|
||
|
elif sys.version_info.major == 3:
|
||
|
import urllib.parse as urllib
|
||
|
|
||
|
|
||
|
class URIVariable(object):
|
||
|
|
||
|
"""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):
|
||
|
#: The original string that comes through with the variable
|
||
|
self.original = var
|
||
|
#: The operator for the variable
|
||
|
self.operator = ''
|
||
|
#: List of safe characters when quoting the string
|
||
|
self.safe = ''
|
||
|
#: List of variables in this variable
|
||
|
self.variables = []
|
||
|
#: List of variable names
|
||
|
self.variable_names = []
|
||
|
#: List of defaults passed in
|
||
|
self.defaults = {}
|
||
|
# Parse the variable itself.
|
||
|
self.parse()
|
||
|
self.post_parse()
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'URIVariable(%s)' % self
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.original
|
||
|
|
||
|
def parse(self):
|
||
|
"""Parse the variable.
|
||
|
|
||
|
This finds the:
|
||
|
- operator,
|
||
|
- set of safe characters,
|
||
|
- variables, and
|
||
|
- defaults.
|
||
|
|
||
|
"""
|
||
|
var_list = self.original
|
||
|
if self.original[0] in URIVariable.operators:
|
||
|
self.operator = self.original[0]
|
||
|
var_list = self.original[1:]
|
||
|
|
||
|
if self.operator in URIVariable.operators[:2]:
|
||
|
self.safe = URIVariable.reserved
|
||
|
|
||
|
var_list = var_list.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 = None
|
||
|
if ':' in name:
|
||
|
name, prefix = tuple(name.split(':', 1))
|
||
|
prefix = int(prefix)
|
||
|
|
||
|
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):
|
||
|
"""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, value, explode, prefix):
|
||
|
"""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
|
||
|
if explode:
|
||
|
return self.join_str.join(
|
||
|
'{}={}'.format(name, quote(v, safe)) for v in value
|
||
|
)
|
||
|
else:
|
||
|
value = ','.join(quote(v, safe) for v in value)
|
||
|
return '{}={}'.format(name, value)
|
||
|
|
||
|
if dict_test(value) or tuples:
|
||
|
if not value:
|
||
|
return None
|
||
|
items = items or sorted(value.items())
|
||
|
if explode:
|
||
|
return self.join_str.join(
|
||
|
'{}={}'.format(
|
||
|
quote(k, safe), quote(v, safe)
|
||
|
) for k, v in items
|
||
|
)
|
||
|
else:
|
||
|
value = ','.join(
|
||
|
'{},{}'.format(
|
||
|
quote(k, safe), quote(v, safe)
|
||
|
) for k, v in items
|
||
|
)
|
||
|
return '{}={}'.format(name, value)
|
||
|
|
||
|
if value:
|
||
|
value = value[:prefix] if prefix else value
|
||
|
return '{}={}'.format(name, quote(value, safe))
|
||
|
return name + '='
|
||
|
|
||
|
def _label_path_expansion(self, name, value, explode, prefix):
|
||
|
"""Label and path expansion method.
|
||
|
|
||
|
Expands for operators: '/', '.'
|
||
|
|
||
|
"""
|
||
|
join_str = self.join_str
|
||
|
safe = self.safe
|
||
|
|
||
|
if value is None or (len(value) == 0 and value != ''):
|
||
|
return None
|
||
|
|
||
|
tuples, items = is_list_of_tuples(value)
|
||
|
|
||
|
if list_test(value) and not tuples:
|
||
|
if not explode:
|
||
|
join_str = ','
|
||
|
|
||
|
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:
|
||
|
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 = value[:prefix] if prefix else value
|
||
|
return quote(value, safe)
|
||
|
|
||
|
def _semi_path_expansion(self, name, value, explode, prefix):
|
||
|
"""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:
|
||
|
if explode:
|
||
|
expanded = join_str.join(
|
||
|
'{}={}'.format(
|
||
|
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 '{}={}'.format(name, value)
|
||
|
|
||
|
if dict_test(value) or tuples:
|
||
|
items = items or sorted(value.items())
|
||
|
|
||
|
if explode:
|
||
|
return join_str.join(
|
||
|
'{}={}'.format(
|
||
|
quote(k, safe), quote(v, safe)
|
||
|
) for k, v in items if v is not None
|
||
|
)
|
||
|
else:
|
||
|
expanded = ','.join(
|
||
|
'{},{}'.format(
|
||
|
quote(k, safe), quote(v, safe)
|
||
|
) for k, v in items if v is not None
|
||
|
)
|
||
|
return '{}={}'.format(name, expanded)
|
||
|
|
||
|
value = value[:prefix] if prefix else value
|
||
|
if value:
|
||
|
return '{}={}'.format(name, quote(value, safe))
|
||
|
|
||
|
return name
|
||
|
|
||
|
def _string_expansion(self, name, value, explode, prefix):
|
||
|
if value is None:
|
||
|
return None
|
||
|
|
||
|
tuples, items = is_list_of_tuples(value)
|
||
|
|
||
|
if list_test(value) and not tuples:
|
||
|
return ','.join(quote(v, self.safe) for v in value)
|
||
|
|
||
|
if dict_test(value) or tuples:
|
||
|
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 = value[:prefix] if prefix else value
|
||
|
return quote(value, self.safe)
|
||
|
|
||
|
def expand(self, var_dict=None):
|
||
|
"""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 = []
|
||
|
|
||
|
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):
|
||
|
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):
|
||
|
return isinstance(value, (list, tuple))
|
||
|
|
||
|
|
||
|
def dict_test(value):
|
||
|
return isinstance(value, (dict, collections_abc.MutableMapping))
|
||
|
|
||
|
|
||
|
try:
|
||
|
texttype = unicode
|
||
|
except NameError: # Python 3
|
||
|
texttype = str
|
||
|
|
||
|
stringlikes = (texttype, bytes)
|
||
|
|
||
|
|
||
|
def _encode(value, encoding='utf-8'):
|
||
|
if (isinstance(value, texttype) and
|
||
|
getattr(value, 'encode', None) is not None):
|
||
|
return value.encode(encoding)
|
||
|
return value
|
||
|
|
||
|
|
||
|
def quote(value, safe):
|
||
|
if not isinstance(value, stringlikes):
|
||
|
value = str(value)
|
||
|
return urllib.quote(_encode(value), safe)
|