1# Argument Clinic 2# Copyright 2012-2013 by Larry Hastings. 3# Licensed to the PSF under a contributor agreement. 4# 5 6import clinic 7from clinic import DSLParser 8import collections 9import inspect 10from test import support 11import sys 12import unittest 13from unittest import TestCase 14 15 16class FakeConverter: 17 def __init__(self, name, args): 18 self.name = name 19 self.args = args 20 21 22class FakeConverterFactory: 23 def __init__(self, name): 24 self.name = name 25 26 def __call__(self, name, default, **kwargs): 27 return FakeConverter(self.name, kwargs) 28 29 30class FakeConvertersDict: 31 def __init__(self): 32 self.used_converters = {} 33 34 def get(self, name, default): 35 return self.used_converters.setdefault(name, FakeConverterFactory(name)) 36 37clinic.Clinic.presets_text = '' 38c = clinic.Clinic(language='C') 39 40class FakeClinic: 41 def __init__(self): 42 self.converters = FakeConvertersDict() 43 self.legacy_converters = FakeConvertersDict() 44 self.language = clinic.CLanguage(None) 45 self.filename = None 46 self.block_parser = clinic.BlockParser('', self.language) 47 self.modules = collections.OrderedDict() 48 self.classes = collections.OrderedDict() 49 clinic.clinic = self 50 self.name = "FakeClinic" 51 self.line_prefix = self.line_suffix = '' 52 self.destinations = {} 53 self.add_destination("block", "buffer") 54 self.add_destination("file", "buffer") 55 self.add_destination("suppress", "suppress") 56 d = self.destinations.get 57 self.field_destinations = collections.OrderedDict(( 58 ('docstring_prototype', d('suppress')), 59 ('docstring_definition', d('block')), 60 ('methoddef_define', d('block')), 61 ('impl_prototype', d('block')), 62 ('parser_prototype', d('suppress')), 63 ('parser_definition', d('block')), 64 ('impl_definition', d('block')), 65 )) 66 67 def get_destination(self, name): 68 d = self.destinations.get(name) 69 if not d: 70 sys.exit("Destination does not exist: " + repr(name)) 71 return d 72 73 def add_destination(self, name, type, *args): 74 if name in self.destinations: 75 sys.exit("Destination already exists: " + repr(name)) 76 self.destinations[name] = clinic.Destination(name, type, self, *args) 77 78 def is_directive(self, name): 79 return name == "module" 80 81 def directive(self, name, args): 82 self.called_directives[name] = args 83 84 _module_and_class = clinic.Clinic._module_and_class 85 86class ClinicWholeFileTest(TestCase): 87 def test_eol(self): 88 # regression test: 89 # clinic's block parser didn't recognize 90 # the "end line" for the block if it 91 # didn't end in "\n" (as in, the last) 92 # byte of the file was '/'. 93 # so it would spit out an end line for you. 94 # and since you really already had one, 95 # the last line of the block got corrupted. 96 c = clinic.Clinic(clinic.CLanguage(None)) 97 raw = "/*[clinic]\nfoo\n[clinic]*/" 98 cooked = c.parse(raw).splitlines() 99 end_line = cooked[2].rstrip() 100 # this test is redundant, it's just here explicitly to catch 101 # the regression test so we don't forget what it looked like 102 self.assertNotEqual(end_line, "[clinic]*/[clinic]*/") 103 self.assertEqual(end_line, "[clinic]*/") 104 105 106 107class ClinicGroupPermuterTest(TestCase): 108 def _test(self, l, m, r, output): 109 computed = clinic.permute_optional_groups(l, m, r) 110 self.assertEqual(output, computed) 111 112 def test_range(self): 113 self._test([['start']], ['stop'], [['step']], 114 ( 115 ('stop',), 116 ('start', 'stop',), 117 ('start', 'stop', 'step',), 118 )) 119 120 def test_add_window(self): 121 self._test([['x', 'y']], ['ch'], [['attr']], 122 ( 123 ('ch',), 124 ('ch', 'attr'), 125 ('x', 'y', 'ch',), 126 ('x', 'y', 'ch', 'attr'), 127 )) 128 129 def test_ludicrous(self): 130 self._test([['a1', 'a2', 'a3'], ['b1', 'b2']], ['c1'], [['d1', 'd2'], ['e1', 'e2', 'e3']], 131 ( 132 ('c1',), 133 ('b1', 'b2', 'c1'), 134 ('b1', 'b2', 'c1', 'd1', 'd2'), 135 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1'), 136 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1', 'd1', 'd2'), 137 ('a1', 'a2', 'a3', 'b1', 'b2', 'c1', 'd1', 'd2', 'e1', 'e2', 'e3'), 138 )) 139 140 def test_right_only(self): 141 self._test([], [], [['a'],['b'],['c']], 142 ( 143 (), 144 ('a',), 145 ('a', 'b'), 146 ('a', 'b', 'c') 147 )) 148 149 def test_have_left_options_but_required_is_empty(self): 150 def fn(): 151 clinic.permute_optional_groups(['a'], [], []) 152 self.assertRaises(AssertionError, fn) 153 154 155class ClinicLinearFormatTest(TestCase): 156 def _test(self, input, output, **kwargs): 157 computed = clinic.linear_format(input, **kwargs) 158 self.assertEqual(output, computed) 159 160 def test_empty_strings(self): 161 self._test('', '') 162 163 def test_solo_newline(self): 164 self._test('\n', '\n') 165 166 def test_no_substitution(self): 167 self._test(""" 168 abc 169 """, """ 170 abc 171 """) 172 173 def test_empty_substitution(self): 174 self._test(""" 175 abc 176 {name} 177 def 178 """, """ 179 abc 180 def 181 """, name='') 182 183 def test_single_line_substitution(self): 184 self._test(""" 185 abc 186 {name} 187 def 188 """, """ 189 abc 190 GARGLE 191 def 192 """, name='GARGLE') 193 194 def test_multiline_substitution(self): 195 self._test(""" 196 abc 197 {name} 198 def 199 """, """ 200 abc 201 bingle 202 bungle 203 204 def 205 """, name='bingle\nbungle\n') 206 207class InertParser: 208 def __init__(self, clinic): 209 pass 210 211 def parse(self, block): 212 pass 213 214class CopyParser: 215 def __init__(self, clinic): 216 pass 217 218 def parse(self, block): 219 block.output = block.input 220 221 222class ClinicBlockParserTest(TestCase): 223 def _test(self, input, output): 224 language = clinic.CLanguage(None) 225 226 blocks = list(clinic.BlockParser(input, language)) 227 writer = clinic.BlockPrinter(language) 228 for block in blocks: 229 writer.print_block(block) 230 output = writer.f.getvalue() 231 assert output == input, "output != input!\n\noutput " + repr(output) + "\n\n input " + repr(input) 232 233 def round_trip(self, input): 234 return self._test(input, input) 235 236 def test_round_trip_1(self): 237 self.round_trip(""" 238 verbatim text here 239 lah dee dah 240""") 241 def test_round_trip_2(self): 242 self.round_trip(""" 243 verbatim text here 244 lah dee dah 245/*[inert] 246abc 247[inert]*/ 248def 249/*[inert checksum: 7b18d017f89f61cf17d47f92749ea6930a3f1deb]*/ 250xyz 251""") 252 253 def _test_clinic(self, input, output): 254 language = clinic.CLanguage(None) 255 c = clinic.Clinic(language) 256 c.parsers['inert'] = InertParser(c) 257 c.parsers['copy'] = CopyParser(c) 258 computed = c.parse(input) 259 self.assertEqual(output, computed) 260 261 def test_clinic_1(self): 262 self._test_clinic(""" 263 verbatim text here 264 lah dee dah 265/*[copy input] 266def 267[copy start generated code]*/ 268abc 269/*[copy end generated code: output=03cfd743661f0797 input=7b18d017f89f61cf]*/ 270xyz 271""", """ 272 verbatim text here 273 lah dee dah 274/*[copy input] 275def 276[copy start generated code]*/ 277def 278/*[copy end generated code: output=7b18d017f89f61cf input=7b18d017f89f61cf]*/ 279xyz 280""") 281 282 283class ClinicParserTest(TestCase): 284 def test_trivial(self): 285 parser = DSLParser(FakeClinic()) 286 block = clinic.Block("module os\nos.access") 287 parser.parse(block) 288 module, function = block.signatures 289 self.assertEqual("access", function.name) 290 self.assertEqual("os", module.name) 291 292 def test_ignore_line(self): 293 block = self.parse("#\nmodule os\nos.access") 294 module, function = block.signatures 295 self.assertEqual("access", function.name) 296 self.assertEqual("os", module.name) 297 298 def test_param(self): 299 function = self.parse_function("module os\nos.access\n path: int") 300 self.assertEqual("access", function.name) 301 self.assertEqual(2, len(function.parameters)) 302 p = function.parameters['path'] 303 self.assertEqual('path', p.name) 304 self.assertIsInstance(p.converter, clinic.int_converter) 305 306 def test_param_default(self): 307 function = self.parse_function("module os\nos.access\n follow_symlinks: bool = True") 308 p = function.parameters['follow_symlinks'] 309 self.assertEqual(True, p.default) 310 311 def test_param_with_continuations(self): 312 function = self.parse_function("module os\nos.access\n follow_symlinks: \\\n bool \\\n =\\\n True") 313 p = function.parameters['follow_symlinks'] 314 self.assertEqual(True, p.default) 315 316 def test_param_default_expression(self): 317 function = self.parse_function("module os\nos.access\n follow_symlinks: int(c_default='MAXSIZE') = sys.maxsize") 318 p = function.parameters['follow_symlinks'] 319 self.assertEqual(sys.maxsize, p.default) 320 self.assertEqual("MAXSIZE", p.converter.c_default) 321 322 s = self.parse_function_should_fail("module os\nos.access\n follow_symlinks: int = sys.maxsize") 323 self.assertEqual(s, "Error on line 0:\nWhen you specify a named constant ('sys.maxsize') as your default value,\nyou MUST specify a valid c_default.\n") 324 325 def test_param_no_docstring(self): 326 function = self.parse_function(""" 327module os 328os.access 329 follow_symlinks: bool = True 330 something_else: str = ''""") 331 p = function.parameters['follow_symlinks'] 332 self.assertEqual(3, len(function.parameters)) 333 self.assertIsInstance(function.parameters['something_else'].converter, clinic.str_converter) 334 335 def test_param_default_parameters_out_of_order(self): 336 s = self.parse_function_should_fail(""" 337module os 338os.access 339 follow_symlinks: bool = True 340 something_else: str""") 341 self.assertEqual(s, """Error on line 0: 342Can't have a parameter without a default ('something_else') 343after a parameter with a default! 344""") 345 346 def disabled_test_converter_arguments(self): 347 function = self.parse_function("module os\nos.access\n path: path_t(allow_fd=1)") 348 p = function.parameters['path'] 349 self.assertEqual(1, p.converter.args['allow_fd']) 350 351 def test_function_docstring(self): 352 function = self.parse_function(""" 353module os 354os.stat as os_stat_fn 355 356 path: str 357 Path to be examined 358 359Perform a stat system call on the given path.""") 360 self.assertEqual(""" 361stat($module, /, path) 362-- 363 364Perform a stat system call on the given path. 365 366 path 367 Path to be examined 368""".strip(), function.docstring) 369 370 def test_explicit_parameters_in_docstring(self): 371 function = self.parse_function(""" 372module foo 373foo.bar 374 x: int 375 Documentation for x. 376 y: int 377 378This is the documentation for foo. 379 380Okay, we're done here. 381""") 382 self.assertEqual(""" 383bar($module, /, x, y) 384-- 385 386This is the documentation for foo. 387 388 x 389 Documentation for x. 390 391Okay, we're done here. 392""".strip(), function.docstring) 393 394 def test_parser_regression_special_character_in_parameter_column_of_docstring_first_line(self): 395 function = self.parse_function(""" 396module os 397os.stat 398 path: str 399This/used to break Clinic! 400""") 401 self.assertEqual("stat($module, /, path)\n--\n\nThis/used to break Clinic!", function.docstring) 402 403 def test_c_name(self): 404 function = self.parse_function("module os\nos.stat as os_stat_fn") 405 self.assertEqual("os_stat_fn", function.c_basename) 406 407 def test_return_converter(self): 408 function = self.parse_function("module os\nos.stat -> int") 409 self.assertIsInstance(function.return_converter, clinic.int_return_converter) 410 411 def test_star(self): 412 function = self.parse_function("module os\nos.access\n *\n follow_symlinks: bool = True") 413 p = function.parameters['follow_symlinks'] 414 self.assertEqual(inspect.Parameter.KEYWORD_ONLY, p.kind) 415 self.assertEqual(0, p.group) 416 417 def test_group(self): 418 function = self.parse_function("module window\nwindow.border\n [\n ls : int\n ]\n /\n") 419 p = function.parameters['ls'] 420 self.assertEqual(1, p.group) 421 422 def test_left_group(self): 423 function = self.parse_function(""" 424module curses 425curses.addch 426 [ 427 y: int 428 Y-coordinate. 429 x: int 430 X-coordinate. 431 ] 432 ch: char 433 Character to add. 434 [ 435 attr: long 436 Attributes for the character. 437 ] 438 / 439""") 440 for name, group in ( 441 ('y', -1), ('x', -1), 442 ('ch', 0), 443 ('attr', 1), 444 ): 445 p = function.parameters[name] 446 self.assertEqual(p.group, group) 447 self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY) 448 self.assertEqual(function.docstring.strip(), """ 449addch([y, x,] ch, [attr]) 450 451 452 y 453 Y-coordinate. 454 x 455 X-coordinate. 456 ch 457 Character to add. 458 attr 459 Attributes for the character. 460 """.strip()) 461 462 def test_nested_groups(self): 463 function = self.parse_function(""" 464module curses 465curses.imaginary 466 [ 467 [ 468 y1: int 469 Y-coordinate. 470 y2: int 471 Y-coordinate. 472 ] 473 x1: int 474 X-coordinate. 475 x2: int 476 X-coordinate. 477 ] 478 ch: char 479 Character to add. 480 [ 481 attr1: long 482 Attributes for the character. 483 attr2: long 484 Attributes for the character. 485 attr3: long 486 Attributes for the character. 487 [ 488 attr4: long 489 Attributes for the character. 490 attr5: long 491 Attributes for the character. 492 attr6: long 493 Attributes for the character. 494 ] 495 ] 496 / 497""") 498 for name, group in ( 499 ('y1', -2), ('y2', -2), 500 ('x1', -1), ('x2', -1), 501 ('ch', 0), 502 ('attr1', 1), ('attr2', 1), ('attr3', 1), 503 ('attr4', 2), ('attr5', 2), ('attr6', 2), 504 ): 505 p = function.parameters[name] 506 self.assertEqual(p.group, group) 507 self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY) 508 509 self.assertEqual(function.docstring.strip(), """ 510imaginary([[y1, y2,] x1, x2,] ch, [attr1, attr2, attr3, [attr4, attr5, 511 attr6]]) 512 513 514 y1 515 Y-coordinate. 516 y2 517 Y-coordinate. 518 x1 519 X-coordinate. 520 x2 521 X-coordinate. 522 ch 523 Character to add. 524 attr1 525 Attributes for the character. 526 attr2 527 Attributes for the character. 528 attr3 529 Attributes for the character. 530 attr4 531 Attributes for the character. 532 attr5 533 Attributes for the character. 534 attr6 535 Attributes for the character. 536 """.strip()) 537 538 def parse_function_should_fail(self, s): 539 with support.captured_stdout() as stdout: 540 with self.assertRaises(SystemExit): 541 self.parse_function(s) 542 return stdout.getvalue() 543 544 def test_disallowed_grouping__two_top_groups_on_left(self): 545 s = self.parse_function_should_fail(""" 546module foo 547foo.two_top_groups_on_left 548 [ 549 group1 : int 550 ] 551 [ 552 group2 : int 553 ] 554 param: int 555 """) 556 self.assertEqual(s, 557 ('Error on line 0:\n' 558 'Function two_top_groups_on_left has an unsupported group configuration. (Unexpected state 2.b)\n')) 559 560 def test_disallowed_grouping__two_top_groups_on_right(self): 561 self.parse_function_should_fail(""" 562module foo 563foo.two_top_groups_on_right 564 param: int 565 [ 566 group1 : int 567 ] 568 [ 569 group2 : int 570 ] 571 """) 572 573 def test_disallowed_grouping__parameter_after_group_on_right(self): 574 self.parse_function_should_fail(""" 575module foo 576foo.parameter_after_group_on_right 577 param: int 578 [ 579 [ 580 group1 : int 581 ] 582 group2 : int 583 ] 584 """) 585 586 def test_disallowed_grouping__group_after_parameter_on_left(self): 587 self.parse_function_should_fail(""" 588module foo 589foo.group_after_parameter_on_left 590 [ 591 group2 : int 592 [ 593 group1 : int 594 ] 595 ] 596 param: int 597 """) 598 599 def test_disallowed_grouping__empty_group_on_left(self): 600 self.parse_function_should_fail(""" 601module foo 602foo.empty_group 603 [ 604 [ 605 ] 606 group2 : int 607 ] 608 param: int 609 """) 610 611 def test_disallowed_grouping__empty_group_on_right(self): 612 self.parse_function_should_fail(""" 613module foo 614foo.empty_group 615 param: int 616 [ 617 [ 618 ] 619 group2 : int 620 ] 621 """) 622 623 def test_no_parameters(self): 624 function = self.parse_function(""" 625module foo 626foo.bar 627 628Docstring 629 630""") 631 self.assertEqual("bar($module, /)\n--\n\nDocstring", function.docstring) 632 self.assertEqual(1, len(function.parameters)) # self! 633 634 def test_init_with_no_parameters(self): 635 function = self.parse_function(""" 636module foo 637class foo.Bar "unused" "notneeded" 638foo.Bar.__init__ 639 640Docstring 641 642""", signatures_in_block=3, function_index=2) 643 # self is not in the signature 644 self.assertEqual("Bar()\n--\n\nDocstring", function.docstring) 645 # but it *is* a parameter 646 self.assertEqual(1, len(function.parameters)) 647 648 def test_illegal_module_line(self): 649 self.parse_function_should_fail(""" 650module foo 651foo.bar => int 652 / 653""") 654 655 def test_illegal_c_basename(self): 656 self.parse_function_should_fail(""" 657module foo 658foo.bar as 935 659 / 660""") 661 662 def test_single_star(self): 663 self.parse_function_should_fail(""" 664module foo 665foo.bar 666 * 667 * 668""") 669 670 def test_parameters_required_after_star_without_initial_parameters_or_docstring(self): 671 self.parse_function_should_fail(""" 672module foo 673foo.bar 674 * 675""") 676 677 def test_parameters_required_after_star_without_initial_parameters_with_docstring(self): 678 self.parse_function_should_fail(""" 679module foo 680foo.bar 681 * 682Docstring here. 683""") 684 685 def test_parameters_required_after_star_with_initial_parameters_without_docstring(self): 686 self.parse_function_should_fail(""" 687module foo 688foo.bar 689 this: int 690 * 691""") 692 693 def test_parameters_required_after_star_with_initial_parameters_and_docstring(self): 694 self.parse_function_should_fail(""" 695module foo 696foo.bar 697 this: int 698 * 699Docstring. 700""") 701 702 def test_single_slash(self): 703 self.parse_function_should_fail(""" 704module foo 705foo.bar 706 / 707 / 708""") 709 710 def test_mix_star_and_slash(self): 711 self.parse_function_should_fail(""" 712module foo 713foo.bar 714 x: int 715 y: int 716 * 717 z: int 718 / 719""") 720 721 def test_parameters_not_permitted_after_slash_for_now(self): 722 self.parse_function_should_fail(""" 723module foo 724foo.bar 725 / 726 x: int 727""") 728 729 def test_function_not_at_column_0(self): 730 function = self.parse_function(""" 731 module foo 732 foo.bar 733 x: int 734 Nested docstring here, goeth. 735 * 736 y: str 737 Not at column 0! 738""") 739 self.assertEqual(""" 740bar($module, /, x, *, y) 741-- 742 743Not at column 0! 744 745 x 746 Nested docstring here, goeth. 747""".strip(), function.docstring) 748 749 def test_directive(self): 750 c = FakeClinic() 751 parser = DSLParser(c) 752 parser.flag = False 753 parser.directives['setflag'] = lambda : setattr(parser, 'flag', True) 754 block = clinic.Block("setflag") 755 parser.parse(block) 756 self.assertTrue(parser.flag) 757 758 def test_legacy_converters(self): 759 block = self.parse('module os\nos.access\n path: "s"') 760 module, function = block.signatures 761 self.assertIsInstance((function.parameters['path']).converter, clinic.str_converter) 762 763 def parse(self, text): 764 c = FakeClinic() 765 parser = DSLParser(c) 766 block = clinic.Block(text) 767 parser.parse(block) 768 return block 769 770 def parse_function(self, text, signatures_in_block=2, function_index=1): 771 block = self.parse(text) 772 s = block.signatures 773 self.assertEqual(len(s), signatures_in_block) 774 assert isinstance(s[0], clinic.Module) 775 assert isinstance(s[function_index], clinic.Function) 776 return s[function_index] 777 778 def test_scaffolding(self): 779 # test repr on special values 780 self.assertEqual(repr(clinic.unspecified), '<Unspecified>') 781 self.assertEqual(repr(clinic.NULL), '<Null>') 782 783 # test that fail fails 784 with support.captured_stdout() as stdout: 785 with self.assertRaises(SystemExit): 786 clinic.fail('The igloos are melting!', filename='clown.txt', line_number=69) 787 self.assertEqual(stdout.getvalue(), 'Error in file "clown.txt" on line 69:\nThe igloos are melting!\n') 788 789 790if __name__ == "__main__": 791 unittest.main() 792