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