diff options
Diffstat (limited to 'uritemplate/variable.py')
-rw-r--r-- | uritemplate/variable.py | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/uritemplate/variable.py b/uritemplate/variable.py new file mode 100644 index 0000000..1842830 --- /dev/null +++ b/uritemplate/variable.py @@ -0,0 +1,384 @@ +""" + +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 +import sys + +if (2, 6) <= sys.version_info < (2, 8): + import urllib +elif (3, 3) <= sys.version_info < (4, 0): + 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( + '%s=%s' % (name, quote(v, safe)) for v in value + ) + else: + value = ','.join(quote(v, safe) for v in value) + return '%s=%s' % (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( + '%s=%s' % ( + quote(k, safe), quote(v, safe) + ) for k, v in items + ) + else: + value = ','.join( + '%s,%s' % ( + quote(k, safe), quote(v, safe) + ) for k, v in items + ) + return '%s=%s' % (name, value) + + if value: + value = value[:prefix] if prefix else value + return '%s=%s' % (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 = ',' + + expanded = join_str.join( + quote(v, safe) for v in value if value is not None + ) + return expanded if expanded 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( + '%s=%s' % ( + 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 '%s=%s' % (name, value) + + if dict_test(value) or tuples: + items = items or sorted(value.items()) + + if explode: + return join_str.join( + '%s=%s' % ( + quote(k, safe), quote(v, safe) + ) for k, v in items if v is not None + ) + else: + expanded = ','.join( + '%s,%s' % ( + quote(k, safe), quote(v, safe) + ) for k, v in items if v is not None + ) + return '%s=%s' % (name, expanded) + + value = value[:prefix] if prefix else value + if value: + return '%s=%s' % (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.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) |