1from unittest import TestCase, main
2from uritemplate import URITemplate, expand, partial, variables
3from uritemplate import variable
4
5
6def merge_dicts(*args):
7    d = {}
8    for arg in args:
9        d.update(arg)
10    return d
11
12
13class RFCTemplateExamples(type):
14    var = {'var': 'value'}
15    hello = {'hello': 'Hello World!'}
16    path = {'path': '/foo/bar'}
17    x = {'x': '1024'}
18    y = {'y': '768'}
19    empty = {'empty': ''}
20    merged_x_y = merge_dicts(x, y)
21    list_ex = {'list': ['red', 'green', 'blue']}
22    keys = {'keys': [('semi', ';'), ('dot', '.'), ('comma', ',')]}
23
24    # # Level 1
25    # Simple string expansion
26    level1_examples = {
27        '{var}': {
28            'expansion': var,
29            'expected': 'value',
30        },
31        '{hello}': {
32            'expansion': hello,
33            'expected': 'Hello%20World%21',
34        },
35    }
36
37    # # Level 2
38    # Reserved string expansion
39    level2_reserved_examples = {
40        '{+var}': {
41            'expansion': var,
42            'expected': 'value',
43        },
44        '{+hello}': {
45            'expansion': hello,
46            'expected': 'Hello%20World!',
47        },
48        '{+path}/here': {
49            'expansion': path,
50            'expected': '/foo/bar/here',
51        },
52        'here?ref={+path}': {
53            'expansion': path,
54            'expected': 'here?ref=/foo/bar',
55        },
56    }
57
58    # Fragment expansion, crosshatch-prefixed
59    level2_fragment_examples = {
60        'X{#var}': {
61            'expansion': var,
62            'expected': 'X#value',
63        },
64        'X{#hello}': {
65            'expansion': hello,
66            'expected': 'X#Hello%20World!'
67        },
68    }
69
70    # # Level 3
71    # String expansion with multiple variables
72    level3_multiple_variable_examples = {
73        'map?{x,y}': {
74            'expansion': merged_x_y,
75            'expected': 'map?1024,768',
76        },
77        '{x,hello,y}': {
78            'expansion': merge_dicts(x, y, hello),
79            'expected': '1024,Hello%20World%21,768',
80        },
81    }
82
83    # Reserved expansion with multiple variables
84    level3_reserved_examples = {
85        '{+x,hello,y}': {
86            'expansion': merge_dicts(x, y, hello),
87            'expected': '1024,Hello%20World!,768',
88        },
89        '{+path,x}/here': {
90            'expansion': merge_dicts(path, x),
91            'expected': '/foo/bar,1024/here',
92        },
93    }
94
95    # Fragment expansion with multiple variables
96    level3_fragment_examples = {
97        '{#x,hello,y}': {
98            'expansion': merge_dicts(x, y, hello),
99            'expected': '#1024,Hello%20World!,768',
100        },
101        '{#path,x}/here': {
102            'expansion': merge_dicts(path, x),
103            'expected': '#/foo/bar,1024/here'
104        },
105    }
106
107    # Label expansion, dot-prefixed
108    level3_label_examples = {
109        'X{.var}': {
110            'expansion': var,
111            'expected': 'X.value',
112        },
113        'X{.x,y}': {
114            'expansion': merged_x_y,
115            'expected': 'X.1024.768',
116        }
117    }
118
119    # Path segments, slash-prefixed
120    level3_path_segment_examples = {
121        '{/var}': {
122            'expansion': var,
123            'expected': '/value',
124        },
125        '{/var,x}/here': {
126            'expansion': merge_dicts(var, x),
127            'expected': '/value/1024/here',
128        },
129    }
130
131    # Path-style parameters, semicolon-prefixed
132    level3_path_semi_examples = {
133        '{;x,y}': {
134            'expansion': merged_x_y,
135            'expected': ';x=1024;y=768',
136        },
137        '{;x,y,empty}': {
138            'expansion': merge_dicts(x, y, empty),
139            'expected': ';x=1024;y=768;empty',
140        },
141    }
142
143    # Form-style query, ampersand-separated
144    level3_form_amp_examples = {
145        '{?x,y}': {
146            'expansion': merged_x_y,
147            'expected': '?x=1024&y=768',
148        },
149        '{?x,y,empty}': {
150            'expansion': merge_dicts(x, y, empty),
151            'expected': '?x=1024&y=768&empty=',
152        },
153    }
154
155    # Form-style query continuation
156    level3_form_cont_examples = {
157        '?fixed=yes{&x}': {
158            'expansion': x,
159            'expected': '?fixed=yes&x=1024',
160        },
161        '{&x,y,empty}': {
162            'expansion': merge_dicts(x, y, empty),
163            'expected': '&x=1024&y=768&empty=',
164        }
165    }
166
167    # # Level 4
168    # String expansion with value modifiers
169    level4_value_modifier_examples = {
170        '{var:3}': {
171            'expansion': var,
172            'expected': 'val',
173        },
174        '{var:30}': {
175            'expansion': var,
176            'expected': 'value',
177        },
178        '{list}': {
179            'expansion': list_ex,
180            'expected': 'red,green,blue',
181        },
182        '{list*}': {
183            'expansion': list_ex,
184            'expected': 'red,green,blue',
185        },
186        '{keys}': {
187            'expansion': keys,
188            'expected': 'semi,%3B,dot,.,comma,%2C',
189        },
190        '{keys*}': {
191            'expansion': keys,
192            'expected': 'semi=%3B,dot=.,comma=%2C',
193        },
194    }
195
196    # Reserved expansion with value modifiers
197    level4_reserved_examples = {
198        '{+path:6}/here': {
199            'expansion': path,
200            'expected': '/foo/b/here',
201        },
202        '{+list}': {
203            'expansion': list_ex,
204            'expected': 'red,green,blue',
205        },
206        '{+list*}': {
207            'expansion': list_ex,
208            'expected': 'red,green,blue',
209        },
210        '{+keys}': {
211            'expansion': keys,
212            'expected': 'semi,;,dot,.,comma,,',
213        },
214        '{+keys*}': {
215            'expansion': keys,
216            'expected': 'semi=;,dot=.,comma=,',
217        },
218    }
219
220    # Fragment expansion with value modifiers
221    level4_fragment_examples = {
222        '{#path:6}/here': {
223            'expansion': path,
224            'expected': '#/foo/b/here',
225        },
226        '{#list}': {
227            'expansion': list_ex,
228            'expected': '#red,green,blue',
229        },
230        '{#list*}': {
231            'expansion': list_ex,
232            'expected': '#red,green,blue',
233        },
234        '{#keys}': {
235            'expansion': keys,
236            'expected': '#semi,;,dot,.,comma,,'
237        },
238        '{#keys*}': {
239            'expansion': keys,
240            'expected': '#semi=;,dot=.,comma=,'
241        },
242    }
243
244    # Label expansion, dot-prefixed
245    level4_label_examples = {
246        'X{.var:3}': {
247            'expansion': var,
248            'expected': 'X.val',
249        },
250        'X{.list}': {
251            'expansion': list_ex,
252            'expected': 'X.red,green,blue',
253        },
254        'X{.list*}': {
255            'expansion': list_ex,
256            'expected': 'X.red.green.blue',
257        },
258        'X{.keys}': {
259            'expansion': keys,
260            'expected': 'X.semi,%3B,dot,.,comma,%2C',
261        },
262        'X{.keys*}': {
263            'expansion': keys,
264            'expected': 'X.semi=%3B.dot=..comma=%2C',
265        },
266    }
267
268    # Path segments, slash-prefixed
269    level4_path_slash_examples = {
270        '{/var:1,var}': {
271            'expansion': var,
272            'expected': '/v/value',
273        },
274        '{/list}': {
275            'expansion': list_ex,
276            'expected': '/red,green,blue',
277        },
278        '{/list*}': {
279            'expansion': list_ex,
280            'expected': '/red/green/blue',
281        },
282        '{/list*,path:4}': {
283            'expansion': merge_dicts(list_ex, path),
284            'expected': '/red/green/blue/%2Ffoo',
285        },
286        '{/keys}': {
287            'expansion': keys,
288            'expected': '/semi,%3B,dot,.,comma,%2C',
289        },
290        '{/keys*}': {
291            'expansion': keys,
292            'expected': '/semi=%3B/dot=./comma=%2C',
293        },
294    }
295
296    # Path-style parameters, semicolon-prefixed
297    level4_path_semi_examples = {
298        '{;hello:5}': {
299            'expansion': hello,
300            'expected': ';hello=Hello',
301        },
302        '{;list}': {
303            'expansion': list_ex,
304            'expected': ';list=red,green,blue',
305        },
306        '{;list*}': {
307            'expansion': list_ex,
308            'expected': ';list=red;list=green;list=blue',
309        },
310        '{;keys}': {
311            'expansion': keys,
312            'expected': ';keys=semi,%3B,dot,.,comma,%2C',
313        },
314        '{;keys*}': {
315            'expansion': keys,
316            'expected': ';semi=%3B;dot=.;comma=%2C',
317        },
318    }
319
320    # Form-style query, ampersand-separated
321    level4_form_amp_examples = {
322        '{?var:3}': {
323            'expansion': var,
324            'expected': '?var=val',
325        },
326        '{?list}': {
327            'expansion': list_ex,
328            'expected': '?list=red,green,blue',
329        },
330        '{?list*}': {
331            'expansion': list_ex,
332            'expected': '?list=red&list=green&list=blue',
333        },
334        '{?keys}': {
335            'expansion': keys,
336            'expected': '?keys=semi,%3B,dot,.,comma,%2C',
337        },
338        '{?keys*}': {
339            'expansion': keys,
340            'expected': '?semi=%3B&dot=.&comma=%2C',
341        },
342    }
343
344    # Form-style query continuation
345    level4_form_query_examples = {
346        '{&var:3}': {
347            'expansion': var,
348            'expected': '&var=val',
349        },
350        '{&list}': {
351            'expansion': list_ex,
352            'expected': '&list=red,green,blue',
353        },
354        '{&list*}': {
355            'expansion': list_ex,
356            'expected': '&list=red&list=green&list=blue',
357        },
358        '{&keys}': {
359            'expansion': keys,
360            'expected': '&keys=semi,%3B,dot,.,comma,%2C',
361        },
362        '{&keys*}': {
363            'expansion': keys,
364            'expected': '&semi=%3B&dot=.&comma=%2C',
365        },
366    }
367
368    def __new__(cls, name, bases, attrs):
369        def make_test(d):
370            def _test_(self):
371                for k, v in d.items():
372                    t = URITemplate(k)
373                    self.assertEqual(t.expand(v['expansion']), v['expected'])
374            return _test_
375
376        examples = [
377            (
378                n, getattr(RFCTemplateExamples, n)
379            ) for n in dir(RFCTemplateExamples) if n.startswith('level')
380        ]
381
382        for name, value in examples:
383            testname = 'test_%s' % name
384            attrs[testname] = make_test(value)
385
386        return type.__new__(cls, name, bases, attrs)
387
388
389class TestURITemplate(RFCTemplateExamples('RFCMeta', (TestCase,), {})):
390    def test_no_variables_in_uri(self):
391        """
392        This test ensures that if there are no variables present, the
393        template evaluates to itself.
394        """
395        uri = 'https://api.github.com/users'
396        t = URITemplate(uri)
397        self.assertEqual(t.expand(), uri)
398        self.assertEqual(t.expand(users='foo'), uri)
399
400    def test_all_variables_parsed(self):
401        """
402        This test ensures that all variables are parsed.
403        """
404        uris = [
405            'https://api.github.com',
406            'https://api.github.com/users{/user}',
407            'https://api.github.com/repos{/user}{/repo}',
408            'https://api.github.com/repos{/user}{/repo}/issues{/issue}'
409        ]
410
411        for i, uri in enumerate(uris):
412            t = URITemplate(uri)
413            self.assertEqual(len(t.variables), i)
414
415    def test_expand(self):
416        """
417        This test ensures that expansion works as expected.
418        """
419        # Single
420        t = URITemplate('https://api.github.com/users{/user}')
421        expanded = 'https://api.github.com/users/sigmavirus24'
422        self.assertEqual(t.expand(user='sigmavirus24'), expanded)
423        v = t.variables[0]
424        self.assertEqual(v.expand({'user': None}), {'/user': ''})
425
426        # Multiple
427        t = URITemplate('https://api.github.com/users{/user}{/repo}')
428        expanded = 'https://api.github.com/users/sigmavirus24/github3.py'
429        self.assertEqual(
430            t.expand({'repo': 'github3.py'}, user='sigmavirus24'),
431            expanded
432        )
433
434    def test_str_repr(self):
435        uri = 'https://api.github.com{/endpoint}'
436        t = URITemplate(uri)
437        self.assertEqual(str(t), uri)
438        self.assertEqual(str(t.variables[0]), '/endpoint')
439        self.assertEqual(repr(t), 'URITemplate("%s")' % uri)
440        self.assertEqual(repr(t.variables[0]), 'URIVariable(/endpoint)')
441
442    def test_hash(self):
443        uri = 'https://api.github.com{/endpoint}'
444        self.assertEqual(hash(URITemplate(uri)), hash(uri))
445
446    def test_default_value(self):
447        uri = 'https://api.github.com/user{/user=sigmavirus24}'
448        t = URITemplate(uri)
449        self.assertEqual(t.expand(),
450                         'https://api.github.com/user/sigmavirus24')
451        self.assertEqual(t.expand(user='lukasa'),
452                         'https://api.github.com/user/lukasa')
453
454    def test_query_expansion(self):
455        t = URITemplate('{foo}')
456        self.assertEqual(
457            t.variables[0]._query_expansion('foo', None, False, False), None
458        )
459
460    def test_label_path_expansion(self):
461        t = URITemplate('{foo}')
462        self.assertEqual(
463            t.variables[0]._label_path_expansion('foo', None, False, False),
464            None
465        )
466
467    def test_semi_path_expansion(self):
468        t = URITemplate('{foo}')
469        v = t.variables[0]
470        self.assertEqual(
471            v._semi_path_expansion('foo', None, False, False),
472            None
473        )
474        t.variables[0].operator = '?'
475        self.assertEqual(
476            v._semi_path_expansion('foo', ['bar', 'bogus'], True, False),
477            'foo=bar&foo=bogus'
478        )
479
480    def test_string_expansion(self):
481        t = URITemplate('{foo}')
482        self.assertEqual(
483            t.variables[0]._string_expansion('foo', None, False, False),
484            None
485        )
486
487    def test_hashability(self):
488        t = URITemplate('{foo}')
489        u = URITemplate('{foo}')
490        d = {t: 1}
491        d[u] += 1
492        self.assertEqual(d, {t: 2})
493
494    def test_no_mutate(self):
495        args = {}
496        t = URITemplate('')
497        t.expand(args, key=1)
498        self.assertEqual(args, {})
499
500
501class TestURIVariable(TestCase):
502    def setUp(self):
503        self.v = variable.URIVariable('{foo}')
504
505    def test_post_parse(self):
506        v = self.v
507        self.assertEqual(v.join_str, ',')
508        self.assertEqual(v.operator, '')
509        self.assertEqual(v.safe, '')
510        self.assertEqual(v.start, '')
511
512    def test_post_parse_plus(self):
513        v = self.v
514        v.operator = '+'
515        v.post_parse()
516        self.assertEqual(v.join_str, ',')
517        self.assertEqual(v.safe, variable.URIVariable.reserved)
518        self.assertEqual(v.start, '')
519
520    def test_post_parse_octothorpe(self):
521        v = self.v
522        v.operator = '#'
523        v.post_parse()
524        self.assertEqual(v.join_str, ',')
525        self.assertEqual(v.safe, variable.URIVariable.reserved)
526        self.assertEqual(v.start, '#')
527
528    def test_post_parse_question(self):
529        v = self.v
530        v.operator = '?'
531        v.post_parse()
532        self.assertEqual(v.join_str, '&')
533        self.assertEqual(v.safe, '')
534        self.assertEqual(v.start, '?')
535
536    def test_post_parse_ampersand(self):
537        v = self.v
538        v.operator = '&'
539        v.post_parse()
540        self.assertEqual(v.join_str, '&')
541        self.assertEqual(v.safe, '')
542        self.assertEqual(v.start, '&')
543
544
545class TestVariableModule(TestCase):
546    def test_is_list_of_tuples(self):
547        l = [(1, 2), (3, 4)]
548        self.assertEqual(variable.is_list_of_tuples(l), (True, l))
549
550        l = [1, 2, 3, 4]
551        self.assertEqual(variable.is_list_of_tuples(l), (False, None))
552
553    def test_list_test(self):
554        l = [1, 2, 3, 4]
555        self.assertEqual(variable.list_test(l), True)
556
557        l = str([1, 2, 3, 4])
558        self.assertEqual(variable.list_test(l), False)
559
560    def test_list_of_tuples_test(self):
561        l = [(1, 2), (3, 4)]
562        self.assertEqual(variable.dict_test(l), False)
563
564        d = dict(l)
565        self.assertEqual(variable.dict_test(d), True)
566
567
568class TestAPI(TestCase):
569    uri = 'https://api.github.com{/endpoint}'
570
571    def test_expand(self):
572        self.assertEqual(expand(self.uri, {'endpoint': 'users'}),
573                         'https://api.github.com/users')
574
575    def test_partial(self):
576        self.assertEqual(partial(self.uri), URITemplate(self.uri))
577        uri = self.uri + '/sigmavirus24{/other}'
578        self.assertEqual(
579            partial(uri, endpoint='users'),
580            URITemplate('https://api.github.com/users/sigmavirus24{/other}')
581            )
582
583    def test_variables(self):
584        self.assertEqual(variables(self.uri),
585                         URITemplate(self.uri).variable_names)
586
587
588if __name__ == '__main__':
589    main()
590