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