1"Test format, coverage 99%." 2 3from idlelib import format as ft 4import unittest 5from unittest import mock 6from test.support import requires 7from tkinter import Tk, Text 8from idlelib.editor import EditorWindow 9from idlelib.idle_test.mock_idle import Editor as MockEditor 10 11 12class Is_Get_Test(unittest.TestCase): 13 """Test the is_ and get_ functions""" 14 test_comment = '# This is a comment' 15 test_nocomment = 'This is not a comment' 16 trailingws_comment = '# This is a comment ' 17 leadingws_comment = ' # This is a comment' 18 leadingws_nocomment = ' This is not a comment' 19 20 def test_is_all_white(self): 21 self.assertTrue(ft.is_all_white('')) 22 self.assertTrue(ft.is_all_white('\t\n\r\f\v')) 23 self.assertFalse(ft.is_all_white(self.test_comment)) 24 25 def test_get_indent(self): 26 Equal = self.assertEqual 27 Equal(ft.get_indent(self.test_comment), '') 28 Equal(ft.get_indent(self.trailingws_comment), '') 29 Equal(ft.get_indent(self.leadingws_comment), ' ') 30 Equal(ft.get_indent(self.leadingws_nocomment), ' ') 31 32 def test_get_comment_header(self): 33 Equal = self.assertEqual 34 # Test comment strings 35 Equal(ft.get_comment_header(self.test_comment), '#') 36 Equal(ft.get_comment_header(self.trailingws_comment), '#') 37 Equal(ft.get_comment_header(self.leadingws_comment), ' #') 38 # Test non-comment strings 39 Equal(ft.get_comment_header(self.leadingws_nocomment), ' ') 40 Equal(ft.get_comment_header(self.test_nocomment), '') 41 42 43class FindTest(unittest.TestCase): 44 """Test the find_paragraph function in paragraph module. 45 46 Using the runcase() function, find_paragraph() is called with 'mark' set at 47 multiple indexes before and inside the test paragraph. 48 49 It appears that code with the same indentation as a quoted string is grouped 50 as part of the same paragraph, which is probably incorrect behavior. 51 """ 52 53 @classmethod 54 def setUpClass(cls): 55 from idlelib.idle_test.mock_tk import Text 56 cls.text = Text() 57 58 def runcase(self, inserttext, stopline, expected): 59 # Check that find_paragraph returns the expected paragraph when 60 # the mark index is set to beginning, middle, end of each line 61 # up to but not including the stop line 62 text = self.text 63 text.insert('1.0', inserttext) 64 for line in range(1, stopline): 65 linelength = int(text.index("%d.end" % line).split('.')[1]) 66 for col in (0, linelength//2, linelength): 67 tempindex = "%d.%d" % (line, col) 68 self.assertEqual(ft.find_paragraph(text, tempindex), expected) 69 text.delete('1.0', 'end') 70 71 def test_find_comment(self): 72 comment = ( 73 "# Comment block with no blank lines before\n" 74 "# Comment line\n" 75 "\n") 76 self.runcase(comment, 3, ('1.0', '3.0', '#', comment[0:58])) 77 78 comment = ( 79 "\n" 80 "# Comment block with whitespace line before and after\n" 81 "# Comment line\n" 82 "\n") 83 self.runcase(comment, 4, ('2.0', '4.0', '#', comment[1:70])) 84 85 comment = ( 86 "\n" 87 " # Indented comment block with whitespace before and after\n" 88 " # Comment line\n" 89 "\n") 90 self.runcase(comment, 4, ('2.0', '4.0', ' #', comment[1:82])) 91 92 comment = ( 93 "\n" 94 "# Single line comment\n" 95 "\n") 96 self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:23])) 97 98 comment = ( 99 "\n" 100 " # Single line comment with leading whitespace\n" 101 "\n") 102 self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:51])) 103 104 comment = ( 105 "\n" 106 "# Comment immediately followed by code\n" 107 "x = 42\n" 108 "\n") 109 self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:40])) 110 111 comment = ( 112 "\n" 113 " # Indented comment immediately followed by code\n" 114 "x = 42\n" 115 "\n") 116 self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:53])) 117 118 comment = ( 119 "\n" 120 "# Comment immediately followed by indented code\n" 121 " x = 42\n" 122 "\n") 123 self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:49])) 124 125 def test_find_paragraph(self): 126 teststring = ( 127 '"""String with no blank lines before\n' 128 'String line\n' 129 '"""\n' 130 '\n') 131 self.runcase(teststring, 4, ('1.0', '4.0', '', teststring[0:53])) 132 133 teststring = ( 134 "\n" 135 '"""String with whitespace line before and after\n' 136 'String line.\n' 137 '"""\n' 138 '\n') 139 self.runcase(teststring, 5, ('2.0', '5.0', '', teststring[1:66])) 140 141 teststring = ( 142 '\n' 143 ' """Indented string with whitespace before and after\n' 144 ' Comment string.\n' 145 ' """\n' 146 '\n') 147 self.runcase(teststring, 5, ('2.0', '5.0', ' ', teststring[1:85])) 148 149 teststring = ( 150 '\n' 151 '"""Single line string."""\n' 152 '\n') 153 self.runcase(teststring, 3, ('2.0', '3.0', '', teststring[1:27])) 154 155 teststring = ( 156 '\n' 157 ' """Single line string with leading whitespace."""\n' 158 '\n') 159 self.runcase(teststring, 3, ('2.0', '3.0', ' ', teststring[1:55])) 160 161 162class ReformatFunctionTest(unittest.TestCase): 163 """Test the reformat_paragraph function without the editor window.""" 164 165 def test_reformat_paragraph(self): 166 Equal = self.assertEqual 167 reform = ft.reformat_paragraph 168 hw = "O hello world" 169 Equal(reform(' ', 1), ' ') 170 Equal(reform("Hello world", 20), "Hello world") 171 172 # Test without leading newline 173 Equal(reform(hw, 1), "O\nhello\nworld") 174 Equal(reform(hw, 6), "O\nhello\nworld") 175 Equal(reform(hw, 7), "O hello\nworld") 176 Equal(reform(hw, 12), "O hello\nworld") 177 Equal(reform(hw, 13), "O hello world") 178 179 # Test with leading newline 180 hw = "\nO hello world" 181 Equal(reform(hw, 1), "\nO\nhello\nworld") 182 Equal(reform(hw, 6), "\nO\nhello\nworld") 183 Equal(reform(hw, 7), "\nO hello\nworld") 184 Equal(reform(hw, 12), "\nO hello\nworld") 185 Equal(reform(hw, 13), "\nO hello world") 186 187 188class ReformatCommentTest(unittest.TestCase): 189 """Test the reformat_comment function without the editor window.""" 190 191 def test_reformat_comment(self): 192 Equal = self.assertEqual 193 194 # reformat_comment formats to a minimum of 20 characters 195 test_string = ( 196 " \"\"\"this is a test of a reformat for a triple quoted string" 197 " will it reformat to less than 70 characters for me?\"\"\"") 198 result = ft.reformat_comment(test_string, 70, " ") 199 expected = ( 200 " \"\"\"this is a test of a reformat for a triple quoted string will it\n" 201 " reformat to less than 70 characters for me?\"\"\"") 202 Equal(result, expected) 203 204 test_comment = ( 205 "# this is a test of a reformat for a triple quoted string will " 206 "it reformat to less than 70 characters for me?") 207 result = ft.reformat_comment(test_comment, 70, "#") 208 expected = ( 209 "# this is a test of a reformat for a triple quoted string will it\n" 210 "# reformat to less than 70 characters for me?") 211 Equal(result, expected) 212 213 214class FormatClassTest(unittest.TestCase): 215 def test_init_close(self): 216 instance = ft.FormatParagraph('editor') 217 self.assertEqual(instance.editwin, 'editor') 218 instance.close() 219 self.assertEqual(instance.editwin, None) 220 221 222# For testing format_paragraph_event, Initialize FormatParagraph with 223# a mock Editor with .text and .get_selection_indices. The text must 224# be a Text wrapper that adds two methods 225 226# A real EditorWindow creates unneeded, time-consuming baggage and 227# sometimes emits shutdown warnings like this: 228# "warning: callback failed in WindowList <class '_tkinter.TclError'> 229# : invalid command name ".55131368.windows". 230# Calling EditorWindow._close in tearDownClass prevents this but causes 231# other problems (windows left open). 232 233class TextWrapper: 234 def __init__(self, master): 235 self.text = Text(master=master) 236 def __getattr__(self, name): 237 return getattr(self.text, name) 238 def undo_block_start(self): pass 239 def undo_block_stop(self): pass 240 241class Editor: 242 def __init__(self, root): 243 self.text = TextWrapper(root) 244 get_selection_indices = EditorWindow. get_selection_indices 245 246class FormatEventTest(unittest.TestCase): 247 """Test the formatting of text inside a Text widget. 248 249 This is done with FormatParagraph.format.paragraph_event, 250 which calls functions in the module as appropriate. 251 """ 252 test_string = ( 253 " '''this is a test of a reformat for a triple " 254 "quoted string will it reformat to less than 70 " 255 "characters for me?'''\n") 256 multiline_test_string = ( 257 " '''The first line is under the max width.\n" 258 " The second line's length is way over the max width. It goes " 259 "on and on until it is over 100 characters long.\n" 260 " Same thing with the third line. It is also way over the max " 261 "width, but FormatParagraph will fix it.\n" 262 " '''\n") 263 multiline_test_comment = ( 264 "# The first line is under the max width.\n" 265 "# The second line's length is way over the max width. It goes on " 266 "and on until it is over 100 characters long.\n" 267 "# Same thing with the third line. It is also way over the max " 268 "width, but FormatParagraph will fix it.\n" 269 "# The fourth line is short like the first line.") 270 271 @classmethod 272 def setUpClass(cls): 273 requires('gui') 274 cls.root = Tk() 275 cls.root.withdraw() 276 editor = Editor(root=cls.root) 277 cls.text = editor.text.text # Test code does not need the wrapper. 278 cls.formatter = ft.FormatParagraph(editor).format_paragraph_event 279 # Sets the insert mark just after the re-wrapped and inserted text. 280 281 @classmethod 282 def tearDownClass(cls): 283 del cls.text, cls.formatter 284 cls.root.update_idletasks() 285 cls.root.destroy() 286 del cls.root 287 288 def test_short_line(self): 289 self.text.insert('1.0', "Short line\n") 290 self.formatter("Dummy") 291 self.assertEqual(self.text.get('1.0', 'insert'), "Short line\n" ) 292 self.text.delete('1.0', 'end') 293 294 def test_long_line(self): 295 text = self.text 296 297 # Set cursor ('insert' mark) to '1.0', within text. 298 text.insert('1.0', self.test_string) 299 text.mark_set('insert', '1.0') 300 self.formatter('ParameterDoesNothing', limit=70) 301 result = text.get('1.0', 'insert') 302 # find function includes \n 303 expected = ( 304" '''this is a test of a reformat for a triple quoted string will it\n" 305" reformat to less than 70 characters for me?'''\n") # yes 306 self.assertEqual(result, expected) 307 text.delete('1.0', 'end') 308 309 # Select from 1.11 to line end. 310 text.insert('1.0', self.test_string) 311 text.tag_add('sel', '1.11', '1.end') 312 self.formatter('ParameterDoesNothing', limit=70) 313 result = text.get('1.0', 'insert') 314 # selection excludes \n 315 expected = ( 316" '''this is a test of a reformat for a triple quoted string will it reformat\n" 317" to less than 70 characters for me?'''") # no 318 self.assertEqual(result, expected) 319 text.delete('1.0', 'end') 320 321 def test_multiple_lines(self): 322 text = self.text 323 # Select 2 long lines. 324 text.insert('1.0', self.multiline_test_string) 325 text.tag_add('sel', '2.0', '4.0') 326 self.formatter('ParameterDoesNothing', limit=70) 327 result = text.get('2.0', 'insert') 328 expected = ( 329" The second line's length is way over the max width. It goes on and\n" 330" on until it is over 100 characters long. Same thing with the third\n" 331" line. It is also way over the max width, but FormatParagraph will\n" 332" fix it.\n") 333 self.assertEqual(result, expected) 334 text.delete('1.0', 'end') 335 336 def test_comment_block(self): 337 text = self.text 338 339 # Set cursor ('insert') to '1.0', within block. 340 text.insert('1.0', self.multiline_test_comment) 341 self.formatter('ParameterDoesNothing', limit=70) 342 result = text.get('1.0', 'insert') 343 expected = ( 344"# The first line is under the max width. The second line's length is\n" 345"# way over the max width. It goes on and on until it is over 100\n" 346"# characters long. Same thing with the third line. It is also way over\n" 347"# the max width, but FormatParagraph will fix it. The fourth line is\n" 348"# short like the first line.\n") 349 self.assertEqual(result, expected) 350 text.delete('1.0', 'end') 351 352 # Select line 2, verify line 1 unaffected. 353 text.insert('1.0', self.multiline_test_comment) 354 text.tag_add('sel', '2.0', '3.0') 355 self.formatter('ParameterDoesNothing', limit=70) 356 result = text.get('1.0', 'insert') 357 expected = ( 358"# The first line is under the max width.\n" 359"# The second line's length is way over the max width. It goes on and\n" 360"# on until it is over 100 characters long.\n") 361 self.assertEqual(result, expected) 362 text.delete('1.0', 'end') 363 364# The following block worked with EditorWindow but fails with the mock. 365# Lines 2 and 3 get pasted together even though the previous block left 366# the previous line alone. More investigation is needed. 367## # Select lines 3 and 4 368## text.insert('1.0', self.multiline_test_comment) 369## text.tag_add('sel', '3.0', '5.0') 370## self.formatter('ParameterDoesNothing') 371## result = text.get('3.0', 'insert') 372## expected = ( 373##"# Same thing with the third line. It is also way over the max width,\n" 374##"# but FormatParagraph will fix it. The fourth line is short like the\n" 375##"# first line.\n") 376## self.assertEqual(result, expected) 377## text.delete('1.0', 'end') 378 379 380class DummyEditwin: 381 def __init__(self, root, text): 382 self.root = root 383 self.text = text 384 self.indentwidth = 4 385 self.tabwidth = 4 386 self.usetabs = False 387 self.context_use_ps1 = True 388 389 _make_blanks = EditorWindow._make_blanks 390 get_selection_indices = EditorWindow.get_selection_indices 391 392 393class FormatRegionTest(unittest.TestCase): 394 395 @classmethod 396 def setUpClass(cls): 397 requires('gui') 398 cls.root = Tk() 399 cls.root.withdraw() 400 cls.text = Text(cls.root) 401 cls.text.undo_block_start = mock.Mock() 402 cls.text.undo_block_stop = mock.Mock() 403 cls.editor = DummyEditwin(cls.root, cls.text) 404 cls.formatter = ft.FormatRegion(cls.editor) 405 406 @classmethod 407 def tearDownClass(cls): 408 del cls.text, cls.formatter, cls.editor 409 cls.root.update_idletasks() 410 cls.root.destroy() 411 del cls.root 412 413 def setUp(self): 414 self.text.insert('1.0', self.code_sample) 415 416 def tearDown(self): 417 self.text.delete('1.0', 'end') 418 419 code_sample = """\ 420# WS line needed for test. 421class C1: 422 # Class comment. 423 def __init__(self, a, b): 424 self.a = a 425 self.b = b 426 427 def compare(self): 428 if a > b: 429 return a 430 elif a < b: 431 return b 432 else: 433 return None 434""" 435 436 def test_get_region(self): 437 get = self.formatter.get_region 438 text = self.text 439 eq = self.assertEqual 440 441 # Add selection. 442 text.tag_add('sel', '7.0', '10.0') 443 expected_lines = ['', 444 ' def compare(self):', 445 ' if a > b:', 446 ''] 447 eq(get(), ('7.0', '10.0', '\n'.join(expected_lines), expected_lines)) 448 449 # Remove selection. 450 text.tag_remove('sel', '1.0', 'end') 451 eq(get(), ('15.0', '16.0', '\n', ['', ''])) 452 453 def test_set_region(self): 454 set_ = self.formatter.set_region 455 text = self.text 456 eq = self.assertEqual 457 458 save_bell = text.bell 459 text.bell = mock.Mock() 460 line6 = self.code_sample.splitlines()[5] 461 line10 = self.code_sample.splitlines()[9] 462 463 text.tag_add('sel', '6.0', '11.0') 464 head, tail, chars, lines = self.formatter.get_region() 465 466 # No changes. 467 set_(head, tail, chars, lines) 468 text.bell.assert_called_once() 469 eq(text.get('6.0', '11.0'), chars) 470 eq(text.get('sel.first', 'sel.last'), chars) 471 text.tag_remove('sel', '1.0', 'end') 472 473 # Alter selected lines by changing lines and adding a newline. 474 newstring = 'added line 1\n\n\n\n' 475 newlines = newstring.split('\n') 476 set_('7.0', '10.0', chars, newlines) 477 # Selection changed. 478 eq(text.get('sel.first', 'sel.last'), newstring) 479 # Additional line added, so last index is changed. 480 eq(text.get('7.0', '11.0'), newstring) 481 # Before and after lines unchanged. 482 eq(text.get('6.0', '7.0-1c'), line6) 483 eq(text.get('11.0', '12.0-1c'), line10) 484 text.tag_remove('sel', '1.0', 'end') 485 486 text.bell = save_bell 487 488 def test_indent_region_event(self): 489 indent = self.formatter.indent_region_event 490 text = self.text 491 eq = self.assertEqual 492 493 text.tag_add('sel', '7.0', '10.0') 494 indent() 495 # Blank lines aren't affected by indent. 496 eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n')) 497 498 def test_dedent_region_event(self): 499 dedent = self.formatter.dedent_region_event 500 text = self.text 501 eq = self.assertEqual 502 503 text.tag_add('sel', '7.0', '10.0') 504 dedent() 505 # Blank lines aren't affected by dedent. 506 eq(text.get('7.0', '10.0'), ('\ndef compare(self):\n if a > b:\n')) 507 508 def test_comment_region_event(self): 509 comment = self.formatter.comment_region_event 510 text = self.text 511 eq = self.assertEqual 512 513 text.tag_add('sel', '7.0', '10.0') 514 comment() 515 eq(text.get('7.0', '10.0'), ('##\n## def compare(self):\n## if a > b:\n')) 516 517 def test_uncomment_region_event(self): 518 comment = self.formatter.comment_region_event 519 uncomment = self.formatter.uncomment_region_event 520 text = self.text 521 eq = self.assertEqual 522 523 text.tag_add('sel', '7.0', '10.0') 524 comment() 525 uncomment() 526 eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n')) 527 528 # Only remove comments at the beginning of a line. 529 text.tag_remove('sel', '1.0', 'end') 530 text.tag_add('sel', '3.0', '4.0') 531 uncomment() 532 eq(text.get('3.0', '3.end'), (' # Class comment.')) 533 534 self.formatter.set_region('3.0', '4.0', '', ['# Class comment.', '']) 535 uncomment() 536 eq(text.get('3.0', '3.end'), (' Class comment.')) 537 538 @mock.patch.object(ft.FormatRegion, "_asktabwidth") 539 def test_tabify_region_event(self, _asktabwidth): 540 tabify = self.formatter.tabify_region_event 541 text = self.text 542 eq = self.assertEqual 543 544 text.tag_add('sel', '7.0', '10.0') 545 # No tabwidth selected. 546 _asktabwidth.return_value = None 547 self.assertIsNone(tabify()) 548 549 _asktabwidth.return_value = 3 550 self.assertIsNotNone(tabify()) 551 eq(text.get('7.0', '10.0'), ('\n\t def compare(self):\n\t\t if a > b:\n')) 552 553 @mock.patch.object(ft.FormatRegion, "_asktabwidth") 554 def test_untabify_region_event(self, _asktabwidth): 555 untabify = self.formatter.untabify_region_event 556 text = self.text 557 eq = self.assertEqual 558 559 text.tag_add('sel', '7.0', '10.0') 560 # No tabwidth selected. 561 _asktabwidth.return_value = None 562 self.assertIsNone(untabify()) 563 564 _asktabwidth.return_value = 2 565 self.formatter.tabify_region_event() 566 _asktabwidth.return_value = 3 567 self.assertIsNotNone(untabify()) 568 eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n')) 569 570 @mock.patch.object(ft, "askinteger") 571 def test_ask_tabwidth(self, askinteger): 572 ask = self.formatter._asktabwidth 573 askinteger.return_value = 10 574 self.assertEqual(ask(), 10) 575 576 577class IndentsTest(unittest.TestCase): 578 579 @mock.patch.object(ft, "askyesno") 580 def test_toggle_tabs(self, askyesno): 581 editor = DummyEditwin(None, None) # usetabs == False. 582 indents = ft.Indents(editor) 583 askyesno.return_value = True 584 585 indents.toggle_tabs_event(None) 586 self.assertEqual(editor.usetabs, True) 587 self.assertEqual(editor.indentwidth, 8) 588 589 indents.toggle_tabs_event(None) 590 self.assertEqual(editor.usetabs, False) 591 self.assertEqual(editor.indentwidth, 8) 592 593 @mock.patch.object(ft, "askinteger") 594 def test_change_indentwidth(self, askinteger): 595 editor = DummyEditwin(None, None) # indentwidth == 4. 596 indents = ft.Indents(editor) 597 598 askinteger.return_value = None 599 indents.change_indentwidth_event(None) 600 self.assertEqual(editor.indentwidth, 4) 601 602 askinteger.return_value = 3 603 indents.change_indentwidth_event(None) 604 self.assertEqual(editor.indentwidth, 3) 605 606 askinteger.return_value = 5 607 editor.usetabs = True 608 indents.change_indentwidth_event(None) 609 self.assertEqual(editor.indentwidth, 3) 610 611 612class RstripTest(unittest.TestCase): 613 614 @classmethod 615 def setUpClass(cls): 616 requires('gui') 617 cls.root = Tk() 618 cls.root.withdraw() 619 cls.text = Text(cls.root) 620 cls.editor = MockEditor(text=cls.text) 621 cls.do_rstrip = ft.Rstrip(cls.editor).do_rstrip 622 623 @classmethod 624 def tearDownClass(cls): 625 del cls.text, cls.do_rstrip, cls.editor 626 cls.root.update_idletasks() 627 cls.root.destroy() 628 del cls.root 629 630 def tearDown(self): 631 self.text.delete('1.0', 'end-1c') 632 633 def test_rstrip_lines(self): 634 original = ( 635 "Line with an ending tab \n" 636 "Line ending in 5 spaces \n" 637 "Linewithnospaces\n" 638 " indented line\n" 639 " indented line with trailing space \n" 640 " \n") 641 stripped = ( 642 "Line with an ending tab\n" 643 "Line ending in 5 spaces\n" 644 "Linewithnospaces\n" 645 " indented line\n" 646 " indented line with trailing space\n") 647 648 self.text.insert('1.0', original) 649 self.do_rstrip() 650 self.assertEqual(self.text.get('1.0', 'insert'), stripped) 651 652 def test_rstrip_end(self): 653 text = self.text 654 for code in ('', '\n', '\n\n\n'): 655 with self.subTest(code=code): 656 text.insert('1.0', code) 657 self.do_rstrip() 658 self.assertEqual(text.get('1.0','end-1c'), '') 659 for code in ('a\n', 'a\n\n', 'a\n\n\n'): 660 with self.subTest(code=code): 661 text.delete('1.0', 'end-1c') 662 text.insert('1.0', code) 663 self.do_rstrip() 664 self.assertEqual(text.get('1.0','end-1c'), 'a\n') 665 666 667if __name__ == '__main__': 668 unittest.main(verbosity=2, exit=2) 669