1"""
2
3uritemplate.variable
4====================
5
6This module contains the URIVariable class which powers the URITemplate class.
7
8What treasures await you:
9
10- URIVariable class
11
12You see a hammer in front of you.
13What do you do?
14>
15
16"""
17
18import sys
19
20try:
21    import collections.abc as collections_abc
22except ImportError:
23    import collections as collections_abc
24
25if sys.version_info.major == 2:
26    import urllib
27elif sys.version_info.major == 3:
28    import urllib.parse as urllib
29
30
31class URIVariable(object):
32
33    """This object validates everything inside the URITemplate object.
34
35    It validates template expansions and will truncate length as decided by
36    the template.
37
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.
41
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.
48
49    """
50
51    operators = ('+', '#', '.', '/', ';', '?', '&', '|', '!', '@')
52    reserved = ":/?#[]@!$&'()*+,;="
53
54    def __init__(self, var):
55        #: The original string that comes through with the variable
56        self.original = var
57        #: The operator for the variable
58        self.operator = ''
59        #: List of safe characters when quoting the string
60        self.safe = ''
61        #: List of variables in this variable
62        self.variables = []
63        #: List of variable names
64        self.variable_names = []
65        #: List of defaults passed in
66        self.defaults = {}
67        # Parse the variable itself.
68        self.parse()
69        self.post_parse()
70
71    def __repr__(self):
72        return 'URIVariable(%s)' % self
73
74    def __str__(self):
75        return self.original
76
77    def parse(self):
78        """Parse the variable.
79
80        This finds the:
81            - operator,
82            - set of safe characters,
83            - variables, and
84            - defaults.
85
86        """
87        var_list = self.original
88        if self.original[0] in URIVariable.operators:
89            self.operator = self.original[0]
90            var_list = self.original[1:]
91
92        if self.operator in URIVariable.operators[:2]:
93            self.safe = URIVariable.reserved
94
95        var_list = var_list.split(',')
96
97        for var in var_list:
98            default_val = None
99            name = var
100            if '=' in var:
101                name, default_val = tuple(var.split('=', 1))
102
103            explode = False
104            if name.endswith('*'):
105                explode = True
106                name = name[:-1]
107
108            prefix = None
109            if ':' in name:
110                name, prefix = tuple(name.split(':', 1))
111                prefix = int(prefix)
112
113            if default_val:
114                self.defaults[name] = default_val
115
116            self.variables.append(
117                (name, {'explode': explode, 'prefix': prefix})
118            )
119
120        self.variable_names = [varname for (varname, _) in self.variables]
121
122    def post_parse(self):
123        """Set ``start``, ``join_str`` and ``safe`` attributes.
124
125        After parsing the variable, we need to set up these attributes and it
126        only makes sense to do it in a more easily testable way.
127        """
128        self.safe = ''
129        self.start = self.join_str = self.operator
130        if self.operator == '+':
131            self.start = ''
132        if self.operator in ('+', '#', ''):
133            self.join_str = ','
134        if self.operator == '#':
135            self.start = '#'
136        if self.operator == '?':
137            self.start = '?'
138            self.join_str = '&'
139
140        if self.operator in ('+', '#'):
141            self.safe = URIVariable.reserved
142
143    def _query_expansion(self, name, value, explode, prefix):
144        """Expansion method for the '?' and '&' operators."""
145        if value is None:
146            return None
147
148        tuples, items = is_list_of_tuples(value)
149
150        safe = self.safe
151        if list_test(value) and not tuples:
152            if not value:
153                return None
154            if explode:
155                return self.join_str.join(
156                    '{}={}'.format(name, quote(v, safe)) for v in value
157                )
158            else:
159                value = ','.join(quote(v, safe) for v in value)
160                return '{}={}'.format(name, value)
161
162        if dict_test(value) or tuples:
163            if not value:
164                return None
165            items = items or sorted(value.items())
166            if explode:
167                return self.join_str.join(
168                    '{}={}'.format(
169                        quote(k, safe), quote(v, safe)
170                    ) for k, v in items
171                )
172            else:
173                value = ','.join(
174                    '{},{}'.format(
175                        quote(k, safe), quote(v, safe)
176                    ) for k, v in items
177                )
178                return '{}={}'.format(name, value)
179
180        if value:
181            value = value[:prefix] if prefix else value
182            return '{}={}'.format(name, quote(value, safe))
183        return name + '='
184
185    def _label_path_expansion(self, name, value, explode, prefix):
186        """Label and path expansion method.
187
188        Expands for operators: '/', '.'
189
190        """
191        join_str = self.join_str
192        safe = self.safe
193
194        if value is None or (len(value) == 0 and value != ''):
195            return None
196
197        tuples, items = is_list_of_tuples(value)
198
199        if list_test(value) and not tuples:
200            if not explode:
201                join_str = ','
202
203            fragments = [quote(v, safe) for v in value if v is not None]
204            return join_str.join(fragments) if fragments else None
205
206        if dict_test(value) or tuples:
207            items = items or sorted(value.items())
208            format_str = '%s=%s'
209            if not explode:
210                format_str = '%s,%s'
211                join_str = ','
212
213            expanded = join_str.join(
214                format_str % (
215                    quote(k, safe), quote(v, safe)
216                ) for k, v in items if v is not None
217            )
218            return expanded if expanded else None
219
220        value = value[:prefix] if prefix else value
221        return quote(value, safe)
222
223    def _semi_path_expansion(self, name, value, explode, prefix):
224        """Expansion method for ';' operator."""
225        join_str = self.join_str
226        safe = self.safe
227
228        if value is None:
229            return None
230
231        if self.operator == '?':
232            join_str = '&'
233
234        tuples, items = is_list_of_tuples(value)
235
236        if list_test(value) and not tuples:
237            if explode:
238                expanded = join_str.join(
239                    '{}={}'.format(
240                        name, quote(v, safe)
241                    ) for v in value if v is not None
242                )
243                return expanded if expanded else None
244            else:
245                value = ','.join(quote(v, safe) for v in value)
246                return '{}={}'.format(name, value)
247
248        if dict_test(value) or tuples:
249            items = items or sorted(value.items())
250
251            if explode:
252                return join_str.join(
253                    '{}={}'.format(
254                        quote(k, safe), quote(v, safe)
255                    ) for k, v in items if v is not None
256                )
257            else:
258                expanded = ','.join(
259                    '{},{}'.format(
260                        quote(k, safe), quote(v, safe)
261                    ) for k, v in items if v is not None
262                )
263                return '{}={}'.format(name, expanded)
264
265        value = value[:prefix] if prefix else value
266        if value:
267            return '{}={}'.format(name, quote(value, safe))
268
269        return name
270
271    def _string_expansion(self, name, value, explode, prefix):
272        if value is None:
273            return None
274
275        tuples, items = is_list_of_tuples(value)
276
277        if list_test(value) and not tuples:
278            return ','.join(quote(v, self.safe) for v in value)
279
280        if dict_test(value) or tuples:
281            items = items or sorted(value.items())
282            format_str = '%s=%s' if explode else '%s,%s'
283
284            return ','.join(
285                format_str % (
286                    quote(k, self.safe), quote(v, self.safe)
287                ) for k, v in items
288            )
289
290        value = value[:prefix] if prefix else value
291        return quote(value, self.safe)
292
293    def expand(self, var_dict=None):
294        """Expand the variable in question.
295
296        Using ``var_dict`` and the previously parsed defaults, expand this
297        variable and subvariables.
298
299        :param dict var_dict: dictionary of key-value pairs to be used during
300            expansion
301        :returns: dict(variable=value)
302
303        Examples::
304
305            # (1)
306            v = URIVariable('/var')
307            expansion = v.expand({'var': 'value'})
308            print(expansion)
309            # => {'/var': '/value'}
310
311            # (2)
312            v = URIVariable('?var,hello,x,y')
313            expansion = v.expand({'var': 'value', 'hello': 'Hello World!',
314                                  'x': '1024', 'y': '768'})
315            print(expansion)
316            # => {'?var,hello,x,y':
317            #     '?var=value&hello=Hello%20World%21&x=1024&y=768'}
318
319        """
320        return_values = []
321
322        for name, opts in self.variables:
323            value = var_dict.get(name, None)
324            if not value and value != '' and name in self.defaults:
325                value = self.defaults[name]
326
327            if value is None:
328                continue
329
330            expanded = None
331            if self.operator in ('/', '.'):
332                expansion = self._label_path_expansion
333            elif self.operator in ('?', '&'):
334                expansion = self._query_expansion
335            elif self.operator == ';':
336                expansion = self._semi_path_expansion
337            else:
338                expansion = self._string_expansion
339
340            expanded = expansion(name, value, opts['explode'], opts['prefix'])
341
342            if expanded is not None:
343                return_values.append(expanded)
344
345        value = ''
346        if return_values:
347            value = self.start + self.join_str.join(return_values)
348        return {self.original: value}
349
350
351def is_list_of_tuples(value):
352    if (not value or
353            not isinstance(value, (list, tuple)) or
354            not all(isinstance(t, tuple) and len(t) == 2 for t in value)):
355        return False, None
356
357    return True, value
358
359
360def list_test(value):
361    return isinstance(value, (list, tuple))
362
363
364def dict_test(value):
365    return isinstance(value, (dict, collections_abc.MutableMapping))
366
367
368try:
369    texttype = unicode
370except NameError:  # Python 3
371    texttype = str
372
373stringlikes = (texttype, bytes)
374
375
376def _encode(value, encoding='utf-8'):
377    if (isinstance(value, texttype) and
378            getattr(value, 'encode', None) is not None):
379        return value.encode(encoding)
380    return value
381
382
383def quote(value, safe):
384    if not isinstance(value, stringlikes):
385        value = str(value)
386    return urllib.quote(_encode(value), safe)
387