diff -r ddff866d820d Lib/idlelib/FormatParagraph.py --- a/Lib/idlelib/FormatParagraph.py Thu Jul 18 02:31:21 2013 +0200 +++ b/Lib/idlelib/FormatParagraph.py Fri Jul 19 15:45:00 2013 -0400 @@ -1,18 +1,19 @@ -# Extension to format a paragraph +"""Extension to format a paragraph or selection to a max width. -# Does basic, standard text formatting, and also understands Python -# comment blocks. Thus, for editing Python source code, this -# extension is really only suitable for reformatting these comment -# blocks or triple-quoted strings. +Does basic, standard text formatting, and also understands Python +comment blocks. Thus, for editing Python source code, this +extension is really only suitable for reformatting these comment +blocks or triple-quoted strings. -# Known problems with comment reformatting: -# * If there is a selection marked, and the first line of the -# selection is not complete, the block will probably not be detected -# as comments, and will have the normal "text formatting" rules -# applied. -# * If a comment block has leading whitespace that mixes tabs and -# spaces, they will not be considered part of the same block. -# * Fancy comments, like this bulleted list, arent handled :-) +Known problems with comment reformatting: +* If there is a selection marked, and the first line of the + selection is not complete, the block will probably not be detected + as comments, and will have the normal "text formatting" rules + applied. +* If a comment block has leading whitespace that mixes tabs and + spaces, they will not be considered part of the same block. +* Fancy comments, like this bulleted list, aren't handled :-) +""" import re from idlelib.configHandler import idleConf @@ -32,6 +33,15 @@ self.editwin = None def format_paragraph_event(self, event): + """Formats paragraph to a max width specified in idleConf. + + If text is selected, format_paragraph_event will start breaking lines + at the max width, starting from the beginning selection. + + If no text is selected, format_paragraph_event uses the current + cursor location to determine the paragraph (lines of text surrounded + by blank lines) and formats it. + """ maxformatwidth = int(idleConf.GetOption('main', 'FormatParagraph', 'paragraph', type='int')) text = self.editwin.text @@ -80,20 +90,30 @@ return "break" def find_paragraph(text, mark): + """Returns the start/stop indices enclosing the paragraph that mark is in. + + Also returns the comment format string and paragraph of text between the + start/stop indices. + """ lineno, col = map(int, mark.split(".")) line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) + + # Look for start of next paragraph if the index passed in is a blank line while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line): lineno = lineno + 1 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) first_lineno = lineno comment_header = get_comment_header(line) comment_header_len = len(comment_header) + + # Once start line found, search for end of paragraph (a blank line) while get_comment_header(line)==comment_header and \ not is_all_white(line[comment_header_len:]): lineno = lineno + 1 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) last = "%d.0" % lineno - # Search back to beginning of paragraph + + # Search back to beginning of paragraph (first blank line before) lineno = first_lineno - 1 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) while lineno > 0 and \ @@ -102,9 +122,11 @@ lineno = lineno - 1 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) first = "%d.0" % (lineno+1) + return first, last, comment_header, text.get(first, last) def reformat_paragraph(data, limit): + """Returns data string reformatted to specified width (limit).""" lines = data.split("\n") i = 0 n = len(lines) @@ -140,12 +162,25 @@ return "\n".join(new) def is_all_white(line): + """Return True if line consists entirely of whitespace.""" return re.match(r"^\s*$", line) is not None def get_indent(line): + """Return the initial whitespace substring of line.""" return re.match(r"^(\s*)", line).group() def get_comment_header(line): + """Attempts to return string with leading whitespace and '#' from line. + + For example, the string " #" could be returned, then used to format + the rest of the lines in the paragraph with the same indent. + """ m = re.match(r"^(\s*#*)", line) if m is None: return "" return m.group(1) + +if __name__ == "__main__": + from test import support; support.use_resources = ['gui'] + import unittest + unittest.main('idlelib.idle_test.test_formatparagraph', + verbosity=2, exit=False) diff -r ddff866d820d Lib/idlelib/idle_test/test_formatparagraph.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/idle_test/test_formatparagraph.py Fri Jul 19 15:45:00 2013 -0400 @@ -0,0 +1,433 @@ +import unittest +from idlelib import FormatParagraph as fp +from idlelib.EditorWindow import EditorWindow as Editor +from tkinter import Tk +from test.support import requires + + +class Is_Get_Test(unittest.TestCase): + + """Test the is_ and get_ functions in FormatParagraph.""" + + @classmethod + def setUpClass(cls): + cls.test_comment = '# This is a comment' + cls.test_nocomment = 'This is not a comment' + cls.trailingws_comment = '# This is a comment ' + cls.leadingws_comment = ' # This is a comment' + cls.leadingws_nocomment = ' This is not a comment' + + def test_is_all_white(self): + Equal = self.assertEqual + Equal(fp.is_all_white('\t\n\r\f\v'), True) + Equal(fp.is_all_white(self.test_comment), False) + + def test_get_indent(self): + Equal = self.assertEqual + Equal(fp.get_indent(self.test_comment), '') + Equal(fp.get_indent(self.trailingws_comment), '') + Equal(fp.get_indent(self.leadingws_comment), ' ') + Equal(fp.get_indent(self.leadingws_nocomment), ' ') + + def test_get_comment_header(self): + Equal = self.assertEqual + + # Test comment strings + Equal(fp.get_comment_header(self.test_comment), '#') + Equal(fp.get_comment_header(self.trailingws_comment), '#') + Equal(fp.get_comment_header(self.leadingws_comment), ' #') + + # Test non-comment strings + Equal(fp.get_comment_header(self.leadingws_nocomment), ' ') + Equal(fp.get_comment_header(self.test_nocomment), '') + + +class FindTest(unittest.TestCase): + + """Test the find_paragraph function in FormatParagraph. + + Using the runcase() function, find_paragraph() is called with 'mark' set at + every possible index before and inside the test paragraph. + + It appears that code with the same indentation as a quted string is grouped + as part of the same paragraph, which is probably incorrect behavior. + """ + + @classmethod + def setUpClass(cls): + requires('gui') + cls.editor = Editor(root=Tk()) + cls.text = cls.editor.text + + def runcase(self, inserttext, stopline, expected): + # Check that find_paragraph returns the expected paragraph when + # the mark index is set to every possible position up to but not + # including the stop line + text = self.text + text.insert('1.0', inserttext) + for line in range(1, stopline): + linelength = len(text.get("%d.0" % line, "%d.0 lineend" % line)) + for col in range(0, linelength): + tempindex = "%d.%d" % (line, col) + self.assertEqual(fp.find_paragraph(text, tempindex), expected) + text.delete('1.0', 'end') + + def test_find_paragraph(self): + comment = ( + "# Comment block with no blank lines before\n" + "# Comment line\n" + "# Comment line\n" + "\n") + self.runcase(comment, 4, ('1.0', '4.0', '#', comment[0:73])) + + comment = ( + "\n" + "# Comment block with whitespace line before and after\n" + "# Comment line\n" + "\n") + self.runcase(comment, 4, ('2.0', '4.0', '#', comment[1:70])) + + comment = ( + "\n" + " # Indented comment block with whitespace before and after\n" + " # Comment line\n" + "\n") + self.runcase(comment, 4, ('2.0', '4.0', ' #', comment[1:82])) + + comment = ( + "\n" + "# Single line comment\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:23])) + + comment = ( + "\n" + " # Single line comment with leading whitespace\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:51])) + + comment = ( + "\n" + "# Comment immediately followed by code\n" + "x = 42\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:40])) + + comment = ( + "\n" + " # Indented comment immediately followed by code\n" + "x = 42\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', ' #', comment[1:53])) + + comment = ( + "\n" + "# Comment immediately followed by indented code\n" + " x = 42\n" + "\n") + self.runcase(comment, 3, ('2.0', '3.0', '#', comment[1:49])) + + comment = ( + "\n" + "# Comment block with whitespace line before and not after\n" + "# Comment line") + # Text widget adds newline at the end + self.runcase(comment, 4, ('2.0', '4.0', '#', comment[1:]+'\n')) + + teststring = ( + '"""String with no blank lines before\n' + 'String line\n' + 'String line\n' + '"""\n' + '\n') + self.runcase(teststring, 5, ('1.0', '5.0', '', teststring[0:65])) + + teststring = ( + "\n" + '"""String with whitespace line before and after\n' + 'String line.\n' + '"""\n' + '\n') + self.runcase(teststring, 5, ('2.0', '5.0', '', teststring[1:66])) + + teststring = ( + '\n' + ' """Indented string with whitespace before and after\n' + ' Comment string.\n' + ' """\n' + '\n') + self.runcase(teststring, 5, ('2.0', '5.0', ' ', teststring[1:85])) + + teststring = ( + '\n' + '"""Single line string."""\n' + '\n') + self.runcase(teststring, 3, ('2.0', '3.0', '', teststring[1:27])) + + teststring = ( + '\n' + ' """Single line string with leading whitespace."""\n' + '\n') + self.runcase(teststring, 3, ('2.0', '3.0', ' ', teststring[1:55])) + + # I suspect the following test case represents incorrect or at least + # undesired behavior by find_paragraph. The string and code lines have + # the same comment header, so they are treated as the same paragraph. + # When FormatParagraph is run in IDLE, 'x = 42' is pulled up onto the + # same line as the string. + teststring = ( + '\n' + '"""String immediately followed by code."""\n' + 'x = 42\n' + '\n') + self.runcase(teststring, 3, ('2.0', '4.0', '', teststring[1:51])) + # desired result: + # self.runcase(teststring, 3, ('2.0', '3.0', '', teststring[1:44])) + + teststring = ( + '\n' + ' """Indented string immediately followed by code."""\n' + ' x = 42\n' + '\n') + self.runcase(teststring, 3, ('2.0', '4.0', ' ', teststring[1:68])) + # Same issue as previous test, desired result: + # self.runcase(teststring, 3, ('2.0', '3.0', ' ', teststring[1:57])) + + teststring = ( + '\n' + '"""String with whitespace line before and not after\n' + 'String line\n' + '"""') + # Text widget adds a newline at the end + self.runcase(teststring, 4, ('2.0', '5.0', '', teststring[1:]+'\n')) + + +class ReformatFunctionTest(unittest.TestCase): + """Tests the reformat_paragraph function without the editor window.""" + + def test_reformat_paragrah(self): + Equal = self.assertEqual + + for linewidth in range(1, 7): + result = fp.reformat_paragraph("# hello world", linewidth) + expected = ("#\nhello\nworld") + Equal(result, expected) + + for linewidth in range(7, 13): + result = fp.reformat_paragraph("# hello world", linewidth) + expected = ("# hello\nworld") + Equal(result, expected) + + # Test with leading newline + for linewidth in range(1, 7): + result = fp.reformat_paragraph("\n# hello world", linewidth) + expected = ("\n#\nhello\nworld") + Equal(result, expected) + + for linewidth in range(7, 13): + result = fp.reformat_paragraph("\n# hello world", linewidth) + expected = ("\n# hello\nworld") + Equal(result, expected) + + for linewidth in range(7, 13): + result = fp.reformat_paragraph("\n# hello world", linewidth) + expected = ("\n# hello\nworld") + Equal(result, expected) + + # Test with line width longer than string + for linewidth in range(13, 15): + result = fp.reformat_paragraph("# hello world", linewidth) + expected = ("# hello world") + Equal(result, expected) + + +class ReformatClassTest(unittest.TestCase): + + """Test the reformatting of text inside a text widget by FormatParagraph""" + + @classmethod + def setUpClass(cls): + requires('gui') + cls.editor = Editor(root=Tk()) + cls.text = cls.editor.text + cls.formatter = fp.FormatParagraph(cls.editor) + + cls.test_string = ( + " \"\"\"this is a test of a reformat for a triple " + "quoted string will it reformat to less than 80 " + "characters for me?\"\"\"") + cls.multiline_test_string = ( + " \"\"\"The first line is under the max width.\n" + " The second line's length is way over the max width. It goes " + "on and on until it is over 100 characters long.\n" + " Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + " The fourth line is short like the first line.\n" + " \"\"\"") + cls.multiline_test_comment = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and on until it is over 100 characters long.\n" + "# Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fourth line is short like the first line.") + cls.multiparagraph_test_comment = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and on until it is over 100 characters long.\n" + "\n" + "# Same thing with the fourth line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fifth line is short like the first line.") + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + + def test_format_paragraph_event(self): + # Test formatting a long triple quoted string with no selection + # and cursor ('insert') at '1.0' + self.text.mark_set('insert', '1.0') + self.text.insert('1.0', self.test_string) + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + # FormatParagraph adds a newline after formatting and the text + # widget also adds a newline character, hence the two newlines + expected = ( + " \"\"\"this is a test of a reformat for a triple quoted " + "string will it\n" + " reformat to less than 80 characters for me?\"\"\"\n\n") + self.assertEqual(result, expected) + self.text.delete('1.0', 'end') + + # Test formatting a long triple quoted string with selection from + # 1.15 to 'end' + self.text.insert('1.0', self.test_string) + self.text.tag_add('sel', '1.15', '1.117') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + expected = ( + " \"\"\"this is a test of a reformat for a triple " + "quoted string will it reformat to\n" + "less than 80 characters for me?\"\"\"\n") + self.assertEqual(result, expected) + self.text.delete('1.0', 'end') + + # Test formatting a long triple quoted string when multiple lines + # inside a paragraph are selected + self.text.insert('1.0', self.multiline_test_string) + self.text.tag_add('sel', '2.0', '4.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + # The expected result moves the three quotation marks to the last line + # It would be a good idea to modify FormatParagraph to leave on their + # own line. + expected = ( + " \"\"\"The first line is under the max width.\n" + " The second line's length is way over the max width. It goes " + "on and\n" + " on until it is over 100 characters long. Same thing with the " + "third\n" + " line. It is also way over the max width, but FormatParagraph " + "will\n" + " fix it.\n" + " The fourth line is short like the first line.\n" + " \"\"\"\n") + self.assertEqual(result, expected) + self.text.delete('1.0', 'end') + + # Test formatting a comment block when nothing is selected + # and cursor ('insert') at '1.0' + self.text.insert('1.0', self.multiline_test_comment) + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + expected = ( + "# The first line is under the max width. The second line's " + "length is\n" + "# way over the max width. It goes on and on until it is over " + "100\n" + "# characters long. Same thing with the third line. It is also " + "way over\n" + "# the max width, but FormatParagraph will fix it. The fourth " + "line is\n" + "# short like the first line.\n\n") + self.assertEqual(result, expected) + self.text.delete('1.0', 'end') + + # Test formatting a comment block when lines 3 and 4 are selected + self.text.insert('1.0', self.multiline_test_comment) + self.text.tag_add('sel', '3.0', '5.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + # FormatParagraph should reformat to the following, but it does not + # handle selections in comment blocks properly. + # expected = ( + # "# The first line is under the max width.\n" + # "# The second line's length is way over the max width. It goes on " + # "and on until it is over 100 characters long.\n" + # "# Same thing with the third line. It is also way over the max " + # "width,\n" + # "# but FormatParagraph will fix it. The fourth line is short like " + # "the\n" + # "# first line.\n\n") + expected = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes " + "on and on until it is over 100 characters long.\n" + "# Same thing with the third line. It is also way over the max " + "width,\n" + "but FormatParagraph will fix it. # The fourth line is short " + "like the\n" + "first line.\n\n") + self.assertEqual(result, expected) + self.text.delete('1.0', 'end') + + # Test formatting a single long line in a larger comment block when + # using line 2 + self.text.insert('1.0', self.multiline_test_comment) + self.text.tag_add('sel', '2.0', '3.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + # This test experiences the same comment block/selection bug as the + # previous test + # expected = ( + # "# The first line is under the max width.\n" + # "# The second line's length is way over the max width. It goes on " + # "and\n" + # "# on until it is over 100 characters long.\n" + # "# Same thing with the third line. It is also way over the max " + # "width, but FormatParagraph will fix it.\n" + # "# The fourth line is short like the first line.\n") + expected = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and\n" + "on until it is over 100 characters long.\n" + "# Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fourth line is short like the first line.\n") + self.assertEqual(result, expected) + self.text.delete('1.0', 'end') + + # Test formatting a comment block with whitespace after to ensure + # FormatParagraph stops formatting when it sees whitespace + self.text.insert('1.0', self.multiparagraph_test_comment) + self.text.mark_set('insert', '1.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + expected = ( + "# The first line is under the max width. The second line's " + "length is\n" + "# way over the max width. It goes on and on until it is over " + "100\n" + "# characters long.\n" + "\n" + "# Same thing with the fourth line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fifth line is short like the first line.\n") + self.assertEqual(result, expected) + self.text.delete('1.0', 'end') + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2)