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