Index: Lib/test/test_textwrap.py IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- Lib/test/test_textwrap.py (revision 95565:37905786b34b90e51a451ceef6da84454b7714dc) +++ Lib/test/test_textwrap.py (revision 95565+:37905786b34b+) @@ -430,6 +430,28 @@ self.check_wrap(text, 10, ["yaba", "daba-doo"], break_on_hyphens=False) + def test_beautiful(self): + # Ensure that the beautiful attribute works + + text = """\ +This text could be a lot more beautiful I am sure. +Not sure this is going to do it. +That is why I am testing it. +Makes sense, no?""" + + expect = ["This text could be a lot more beautiful I am", + "sure. Not sure this is going to do it. That", + "is why I am testing it. Makes sense, no?"] + + self.check_wrap(text, 60, expect, beautiful=True) + + expect = ["This text could be a lot more beautiful I am", + " sure. Not sure this is going to do it. That", + " is why I am testing it. Makes sense, no?"] + + self.check_wrap(text, 60, expect, beautiful=True, + drop_whitespace=False) + def test_bad_width(self): # Ensure that width <= 0 is caught. text = "Whatever, it doesn't matter." Index: Lib/textwrap.py IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- Lib/textwrap.py (revision 95565:37905786b34b90e51a451ceef6da84454b7714dc) +++ Lib/textwrap.py (revision 95565+:37905786b34b+) @@ -66,6 +66,9 @@ Truncate wrapped lines. placeholder (default: ' [...]') Append to the last line of truncated text. + beautiful (default: false) + Whether to run the text through _beautify, which redistributes + the wrapping so that the text is more aesthetically pleasing """ unicode_whitespace_trans = {} @@ -123,6 +126,7 @@ break_long_words=True, drop_whitespace=True, break_on_hyphens=True, + beautiful=False, tabsize=8, *, max_lines=None, @@ -139,6 +143,7 @@ self.tabsize = tabsize self.max_lines = max_lines self.placeholder = placeholder + self.beautiful=beautiful # -- Private methods ----------------------------------------------- @@ -259,6 +264,10 @@ # from a stack of chucks. chunks.reverse() + # If self.beautiful is true, whitespace will be dropped in + # self._beautify, thus it should not be dropped here. + drop_whitespace = self.drop_whitespace and not self.beautiful + while chunks: # Start the list of chunks that will make up the current line. @@ -277,7 +286,7 @@ # First chunk on line is whitespace -- drop it, unless this # is the very beginning of the text (ie. no lines started yet). - if self.drop_whitespace and chunks[-1].strip() == '' and lines: + if drop_whitespace and chunks[-1].strip() == '' and lines: del chunks[-1] while chunks: @@ -299,7 +308,7 @@ cur_len = sum(map(len, cur_line)) # If the last chunk on this line is all whitespace, drop it. - if self.drop_whitespace and cur_line and cur_line[-1].strip() == '': + if drop_whitespace and cur_line and cur_line[-1].strip() == '': cur_len -= len(cur_line[-1]) del cur_line[-1] @@ -333,11 +342,90 @@ break return lines - + def _split_chunks(self, text): text = self._munge_whitespace(text) return self._split(text) + def _beautify(self, lines): + """_beautify(lines : [list]) -> [list] + + Clean up a paragraph of wrapped text by minimizing the space + at the ends of all the lines. The algorithm used in _wrap_chunks + employs the minimum length algorithm for text wrapping, which + puts as many words onto a line as possible. While quick, this + tends to create a ragged, less aesthetically-pleasing result. + This function goes back over the wrapped text in reverse and + better distributes the lines. It does so by calculating the "cost" + of a line, which is defined as the square of how much space is + left at the end of the line. + """ + changed = True + lines = [self._split(l) for l in lines] + + # The function makes decisions to change lines based on the cost + # difference between removing a word from one line and placing + # it on another. Thus whenever a line is changed, every line below + # it needs to be redistributed. So this loop goes over the lines + # until no more changes can be made. + while changed: + + # Reset changed to False so we don't get an infinite loop. + changed = False + + # Since raggedness appears mainly in the bottom lines, start + # from the bottom and move upward. + for lineno in range(len(lines)-1, 0, -1): + line_here, line_above = lines[lineno], lines[lineno - 1] + + # Continue to move chunks from the line above (line_above) + # to this line (line_here) until we reach maximum efficiency. + while True: + cost_before = (self._line_cost(line_here) + + self._line_cost(line_above)) + + line_here = line_above[-1:] + line_here + line_above = line_above[:-1] + + cost_after = (self._line_cost(line_here) + + self._line_cost(line_above)) + + # Subtracting after from before means positive differences + # represent an increase in efficiency. + cost_diff = cost_before - cost_after + + # Does the new arrangement improve the cost? + if cost_diff > 0.0: + # Yes, put them back in the original list of lines. + lines[lineno] = line_here + lines[lineno - 1] = line_above + changed = True + else: + # No, we've reached maximum efficiency. + break + + # Drop whitespace on the beginning and ends of lines if specified. + if self.drop_whitespace: + return [''.join(line).strip() for line in lines] + else: + return [''.join(line) for line in lines] + + def _line_cost(self, string): + """_cost(string : [str/list]) -> [float] + + Calculate the cost of a line of text (either in string form + or a list of chunks) by squaring the amount of remaining + space on the line. + """ + # Allow for a list of chunks as input. + if isinstance(string, list): + string = ''.join(string) + + # Return infinity if the line goes beyond maximum width. + if len(string) > self.width: + return float('inf') + return float((self.width - len(string)) ** 2) + # -- Public interface ---------------------------------------------- def wrap(self, text): @@ -352,7 +440,11 @@ chunks = self._split_chunks(text) if self.fix_sentence_endings: self._fix_sentence_endings(chunks) - return self._wrap_chunks(chunks) + lines = self._wrap_chunks(chunks) + + if self.beautiful: + lines = self._beautify(lines) + return lines def fill(self, text): """fill(text : string) -> string