diff --git a/Doc/library/textwrap.rst b/Doc/library/textwrap.rst index d12968dee91f3c3..a7f015d9f16351c 100644 --- a/Doc/library/textwrap.rst +++ b/Doc/library/textwrap.rst @@ -19,7 +19,7 @@ functions should be good enough; otherwise, you should use an instance of replace_whitespace=True, fix_sentence_endings=False, \ break_long_words=True, drop_whitespace=True, \ break_on_hyphens=True, tabsize=8, max_lines=None, \ - placeholder=' [...]') + fold_space_newline=False, placeholder=' [...]') Wraps the single paragraph in *text* (a string) so every line is at most *width* characters long. Returns a list of output lines, without final @@ -37,7 +37,8 @@ functions should be good enough; otherwise, you should use an instance of replace_whitespace=True, fix_sentence_endings=False, \ break_long_words=True, drop_whitespace=True, \ break_on_hyphens=True, tabsize=8, \ - max_lines=None, placeholder=' [...]') + max_lines=None, fold_space_newline=False, \ + placeholder=' [...]') Wraps the single paragraph in *text*, and returns a single string containing the wrapped paragraph. :func:`fill` is shorthand for :: @@ -50,7 +51,7 @@ functions should be good enough; otherwise, you should use an instance of .. function:: shorten(text, width, *, fix_sentence_endings=False, \ break_long_words=True, break_on_hyphens=True, \ - placeholder=' [...]') + placeholder=' [...]', fold_space_newline=False) Collapse and truncate the given *text* to fit in the given *width*. @@ -207,6 +208,18 @@ hyphenated words; only then will long words be broken if necessary, unless be split into paragraphs (using :meth:`str.splitlines` or similar) which are wrapped separately. + .. attribute:: fold_space_newline + + (default: ``False``) If true, and *replace_whitespace* is also ``True``, + newlines will be folded into adjacent spaces if present (if no adjacent + spaces, newlines are replaced by a space as normal). + + .. note:: + + When combined with *drop_whitespace* set to ``False``, this option allows + for stable text wrapping, i.e. wrapping can be applied multiple times with + no changes after the first application. Stable wrapping is not guaranteed with any + other options in this class. .. attribute:: drop_whitespace diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 78ddc627fcf4d70..75d5e4ef3340d53 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -175,6 +175,10 @@ Important deprecations, removals or restrictions: New Features ============ +* New option *fold_space_newline* (default: ``False``) was added to + :class:`~textwrap.TextWrapper`. When set to true, newlines may be folded + into adjacent spaces; see class documentation for details. + .. _whatsnew312-pep695: PEP 695: Type Parameter Syntax diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py index e216ae67f68b4d1..3ff434ddb5266e5 100644 --- a/Lib/idlelib/idle_test/test_calltip.py +++ b/Lib/idlelib/idle_test/test_calltip.py @@ -100,12 +100,13 @@ class SB: __call__ = None non-overlapping occurrences o...''') def test_signature_wrap(self): + # Test revised for 3.11, bpo-32397. if textwrap.TextWrapper.__doc__ is not None: self.assertEqual(get_spec(textwrap.TextWrapper), '''\ (width=70, initial_indent='', subsequent_indent='', expand_tabs=True, replace_whitespace=True, fix_sentence_endings=False, break_long_words=True, - drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None, - placeholder=' [...]') + drop_whitespace=True, break_on_hyphens=True, tabsize=8, fold_space_newline=False, + *, max_lines=None, placeholder=' [...]') Object for wrapping/filling text. The public interface consists of the wrap() and fill() methods; the other methods are just there for subclasses to override in order to tweak the default behaviour. diff --git a/Lib/test/test_textwrap.py b/Lib/test/test_textwrap.py index aca1f427656bb50..dd7a5bda682888d 100644 --- a/Lib/test/test_textwrap.py +++ b/Lib/test/test_textwrap.py @@ -411,6 +411,39 @@ def test_drop_whitespace_whitespace_indent(self): self.check_wrap("abcd efgh", 6, [" abcd", " efgh"], initial_indent=" ", subsequent_indent=" ") + def test_replace_whitespace(self): + # Check that special whitespace chars like \n are replaced with a space. + self.check_wrap("abcd\nefgh", 10, ["abcd efgh"]) + self.check_wrap("abcd \nefgh", 10, ["abcd efgh"]) + + self.check_wrap("abcd \nefgh", 6, ["abcd ", "efgh"], + drop_whitespace=False) + + self.check_wrap("abcd \nefgh", 10, ["abcd \nefgh"], + replace_whitespace=False) + + def test_fold_space_newline(self): + # Check that \n and \r\n are folded into adjacent space if present, + # otherwise \n and \r\n would be replaced with spaces. + self.check_wrap("abcd\nefgh", 10, ["abcd efgh"], + fold_space_newline=True) + + self.check_wrap("abcd \nefgh", 10, ["abcd efgh"], + fold_space_newline=True) + + # this is the test for combination of arguments that allow "stable" + # wrapping (see docstring for _munge_whitespace()) + self.check_wrap("abcd \nefgh", 6, ["abcd ", "efgh"], + fold_space_newline=True, drop_whitespace=False) + + self.check_wrap("abcd\n efgh", 10, ["abcd efgh"], + fold_space_newline=True) + + self.check_wrap("abcd\r\n efgh", 12, ["abcd efgh"]) + + self.check_wrap("abcd\r\n efgh", 10, ["abcd efgh"], + fold_space_newline=True) + def test_split(self): # Ensure that the standard _split() method works as advertised # in the comments diff --git a/Lib/textwrap.py b/Lib/textwrap.py index 2f213e34c2c329f..24c65c9c194ea93 100644 --- a/Lib/textwrap.py +++ b/Lib/textwrap.py @@ -57,6 +57,10 @@ class TextWrapper: compound words. drop_whitespace (default: true) Drop leading and trailing whitespace from lines. + fold_space_newline (default: false) + When replacing whitespace (replace_whitespace=True), fold newlines into + adjoining space character if present. This option allows to perform stable + text wrapping when combined with drop_whitespace=False. max_lines (default: None) Truncate wrapped lines. placeholder (default: ' [...]') @@ -120,6 +124,7 @@ def __init__(self, drop_whitespace=True, break_on_hyphens=True, tabsize=8, + fold_space_newline=False, *, max_lines=None, placeholder=' [...]'): @@ -135,7 +140,7 @@ def __init__(self, self.tabsize = tabsize self.max_lines = max_lines self.placeholder = placeholder - + self.fold_space_newline = fold_space_newline # -- Private methods ----------------------------------------------- # (possibly useful for subclasses to override) @@ -146,10 +151,20 @@ def _munge_whitespace(self, text): Munge whitespace in text: expand tabs and convert all other whitespace characters to spaces. Eg. " foo\\tbar\\n\\nbaz" becomes " foo bar baz". + + If `fold_space_newline=True`, fold newlines into adjacent space (if + present). This allows for stable wrapping when combined with + `drop_whitespace=False`, i.e. repeated wrapping operation results in + the same output. """ if self.expand_tabs: text = text.expandtabs(self.tabsize) if self.replace_whitespace: + if self.fold_space_newline: + if re.search(r' \r?\n', text): + text = re.sub(r' \r?\n', ' ', text) + if re.search(r'\r?\n ', text): + text = re.sub(r'\r?\n ', ' ', text) text = text.translate(self.unicode_whitespace_trans) return text diff --git a/Misc/NEWS.d/next/Library/2021-08-22-22-46-11.bpo-32397.5wU1QU.rst b/Misc/NEWS.d/next/Library/2021-08-22-22-46-11.bpo-32397.5wU1QU.rst new file mode 100644 index 000000000000000..ba613bc34af97b3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-08-22-22-46-11.bpo-32397.5wU1QU.rst @@ -0,0 +1,3 @@ +New option *fold_space_newline* (default: ``False``) was added to +:class:`~textwrap.TextWrapper`. When set to true, newlines may be folded +into adjacent spaces; see class documentation for details.