Issue #18226: Add docstrings and unittests for idlelib/FormatParagraph.py.
Move comment code to a separate function so it can be separately tested.
Original patches by Todd Rovito and Phil Webster.
diff --git a/Lib/idlelib/FormatParagraph.py b/Lib/idlelib/FormatParagraph.py
index 557d8a9..ae4e6e7 100644
--- a/Lib/idlelib/FormatParagraph.py
+++ b/Lib/idlelib/FormatParagraph.py
@@ -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,41 +33,31 @@
         self.editwin = None
 
     def format_paragraph_event(self, event):
-        maxformatwidth = int(idleConf.GetOption('main','FormatParagraph',
-                                                'paragraph', type='int'))
+        """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 = idleConf.GetOption(
+                'main', 'FormatParagraph', 'paragraph', type='int')
         text = self.editwin.text
         first, last = self.editwin.get_selection_indices()
         if first and last:
             data = text.get(first, last)
-            comment_header = ''
+            comment_header = get_comment_header(data)
         else:
             first, last, comment_header, data = \
                     find_paragraph(text, text.index("insert"))
         if comment_header:
-            # Reformat the comment lines - convert to text sans header.
-            lines = data.split("\n")
-            lines = map(lambda st, l=len(comment_header): st[l:], lines)
-            data = "\n".join(lines)
-            # Reformat to maxformatwidth chars or a 20 char width, whichever is greater.
-            format_width = max(maxformatwidth - len(comment_header), 20)
-            newdata = reformat_paragraph(data, format_width)
-            # re-split and re-insert the comment header.
-            newdata = newdata.split("\n")
-            # If the block ends in a \n, we dont want the comment
-            # prefix inserted after it. (Im not sure it makes sense to
-            # reformat a comment block that isnt made of complete
-            # lines, but whatever!)  Can't think of a clean solution,
-            # so we hack away
-            block_suffix = ""
-            if not newdata[-1]:
-                block_suffix = "\n"
-                newdata = newdata[:-1]
-            builder = lambda item, prefix=comment_header: prefix+item
-            newdata = '\n'.join(map(builder, newdata)) + block_suffix
+            newdata = reformat_comment(data, maxformatwidth, comment_header)
         else:
-            # Just a normal text format
             newdata = reformat_paragraph(data, maxformatwidth)
         text.tag_remove("sel", "1.0", "end")
+
         if newdata != data:
             text.mark_set("insert", first)
             text.undo_block_start()
@@ -79,31 +70,44 @@
         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, if any, 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)
+    line = text.get("%d.0" % lineno, "%d.end" % 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)
+        line = text.get("%d.0" % lineno, "%d.end" % 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)
+        line = text.get("%d.0" % lineno, "%d.end" % 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)
+    line = text.get("%d.0" % lineno, "%d.end" % lineno)
     while lineno > 0 and \
               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)
+        line = text.get("%d.0" % lineno, "%d.end" % lineno)
     first = "%d.0" % (lineno+1)
+
     return first, last, comment_header, text.get(first, last)
 
+# This should perhaps be replaced with textwrap.wrap
 def reformat_paragraph(data, limit):
+    """Return data reformatted to specified width (limit)."""
     lines = data.split("\n")
     i = 0
     n = len(lines)
@@ -126,7 +130,7 @@
             if not word:
                 continue # Can happen when line ends in whitespace
             if len((partial + word).expandtabs()) > limit and \
-               partial != indent1:
+                   partial != indent1:
                 new.append(partial.rstrip())
                 partial = indent2
             partial = partial + word + " "
@@ -138,13 +142,50 @@
     new.extend(lines[i:])
     return "\n".join(new)
 
+def reformat_comment(data, limit, comment_header):
+    """Return data reformatted to specified width with comment header."""
+
+    # Remove header from the comment lines
+    lc = len(comment_header)
+    data = "\n".join(line[lc:] for line in data.split("\n"))
+    # Reformat to maxformatwidth chars or a 20 char width,
+    # whichever is greater.
+    format_width = max(limit - len(comment_header), 20)
+    newdata = reformat_paragraph(data, format_width)
+    # re-split and re-insert the comment header.
+    newdata = newdata.split("\n")
+    # If the block ends in a \n, we dont want the comment prefix
+    # inserted after it. (Im not sure it makes sense to reformat a
+    # comment block that is not made of complete lines, but whatever!)
+    # Can't think of a clean solution, so we hack away
+    block_suffix = ""
+    if not newdata[-1]:
+        block_suffix = "\n"
+        newdata = newdata[:-1]
+    return '\n'.join(comment_header+line for line in newdata) + block_suffix
+
 def is_all_white(line):
+    """Return True if line is empty or all whitespace."""
+
     return re.match(r"^\s*$", line) is not None
 
 def get_indent(line):
-    return re.match(r"^(\s*)", line).group()
+    """Return the initial space or tab indent of line."""
+    return re.match(r"^([ \t]*)", line).group()
 
 def get_comment_header(line):
-    m = re.match(r"^(\s*#*)", line)
+    """Return string with leading whitespace and '#' from line or ''.
+
+    A null return indicates that the line is not a comment line. A non-
+    null return, such as '    #', will be used to find the other lines of
+    a comment block with the same  indent.
+    """
+    m = re.match(r"^([ \t]*#*)", 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)