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