From 4fae8cdaeac117c6a567f9305a22fd0cff400452 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Mon, 11 Jun 2012 23:07:51 +1000 Subject: [PATCH] Close #13857: Added textwrap.indent() function (initial patch by Ezra Berch) --- Doc/library/textwrap.rst | 35 ++++++++-- Doc/whatsnew/3.3.rst | 8 +++ Lib/test/test_textwrap.py | 140 +++++++++++++++++++++++++++++++++++++- Lib/textwrap.py | 21 +++++- Misc/ACKS | 1 + Misc/NEWS | 3 + 6 files changed, 201 insertions(+), 7 deletions(-) diff --git a/Doc/library/textwrap.rst b/Doc/library/textwrap.rst index a74789ce6bb..50c710c522e 100644 --- a/Doc/library/textwrap.rst +++ b/Doc/library/textwrap.rst @@ -12,7 +12,7 @@ The :mod:`textwrap` module provides two convenience functions, :func:`wrap` and :func:`fill`, as well as :class:`TextWrapper`, the class that does all the work, -and a utility function :func:`dedent`. If you're just wrapping or filling one +and two utility functions, :func:`dedent` and :func:`indent`. If you're just wrapping or filling one or two text strings, the convenience functions should be good enough; otherwise, you should use an instance of :class:`TextWrapper` for efficiency. @@ -45,9 +45,10 @@ Text is preferably wrapped on whitespaces and right after the hyphens in hyphenated words; only then will long words be broken if necessary, unless :attr:`TextWrapper.break_long_words` is set to false. -An additional utility function, :func:`dedent`, is provided to remove -indentation from strings that have unwanted whitespace to the left of the text. - +Two additional utility function, :func:`dedent` and :func:`indent`, are +provided to remove indentation from strings that have unwanted whitespace +to the left of the text and to add an arbitrary prefix to selected lines +in a block of text. .. function:: dedent(text) @@ -72,6 +73,32 @@ indentation from strings that have unwanted whitespace to the left of the text. print(repr(dedent(s))) # prints 'hello\n world\n' +.. function:: indent(text, prefix, predicate=None) + + Add *prefix* to the beginning of selected lines in *text*. + + Lines are separated by calling ``text.splitlines(True)``. + + By default, *prefix* is added to all lines that do not consist + solely of whitespace (including any line endings). + + For example:: + + >>> s = 'hello\n\n \nworld' + >>> indent(s, ' ') + ' hello\n\n \n world' + + The optional *predicate* argument can be used to control which lines + are indented. For example, it is easy to add *prefix* to even empty + and whitespace-only lines:: + + >>> print(indent(s, '+ ', lambda line: True)) + + hello + + + + + + world + + .. class:: TextWrapper(**kwargs) The :class:`TextWrapper` constructor accepts a number of optional keyword diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index cd57a393061..f52d5ae29d3 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -1406,6 +1406,14 @@ sys (:issue:`11223`) +textwrap +-------- + +* The :mod:`textwrap` module has a new :func:`~textwrap.indent` that makes + it straightforward to add a common prefix to selected lines in a block + of text. + + (:issue:`13857`) time ---- diff --git a/Lib/test/test_textwrap.py b/Lib/test/test_textwrap.py index bbd0882bda6..bb4a8519172 100644 --- a/Lib/test/test_textwrap.py +++ b/Lib/test/test_textwrap.py @@ -11,7 +11,7 @@ import unittest from test import support -from textwrap import TextWrapper, wrap, fill, dedent +from textwrap import TextWrapper, wrap, fill, dedent, indent class BaseTestCase(unittest.TestCase): @@ -594,11 +594,147 @@ def foo(): self.assertEqual(expect, dedent(text)) +# Test textwrap.indent +class IndentTestCase(unittest.TestCase): + # The examples used for tests. If any of these change, the expected + # results in the various test cases must also be updated. + # The roundtrip cases are separate, because textwrap.dedent doesn't + # handle Windows line endings + ROUNDTRIP_CASES = ( + # Basic test case + "Hi.\nThis is a test.\nTesting.", + # Include a blank line + "Hi.\nThis is a test.\n\nTesting.", + # Include leading and trailing blank lines + "\nHi.\nThis is a test.\nTesting.\n", + ) + CASES = ROUNDTRIP_CASES + ( + # Use Windows line endings + "Hi.\r\nThis is a test.\r\nTesting.\r\n", + # Pathological case + "\nHi.\r\nThis is a test.\n\r\nTesting.\r\n\n", + ) + + def test_indent_nomargin_default(self): + # indent should do nothing if 'prefix' is empty. + for text in self.CASES: + self.assertEqual(indent(text, ''), text) + + def test_indent_nomargin_explicit_default(self): + # The same as test_indent_nomargin, but explicitly requesting + # the default behaviour by passing None as the predicate + for text in self.CASES: + self.assertEqual(indent(text, '', None), text) + + def test_indent_nomargin_all_lines(self): + # The same as test_indent_nomargin, but using the optional + # predicate argument + predicate = lambda line: True + for text in self.CASES: + self.assertEqual(indent(text, '', predicate), text) + + def test_indent_no_lines(self): + # Explicitly skip indenting any lines + predicate = lambda line: False + for text in self.CASES: + self.assertEqual(indent(text, ' ', predicate), text) + + def test_roundtrip_spaces(self): + # A whitespace prefix should roundtrip with dedent + for text in self.ROUNDTRIP_CASES: + self.assertEqual(dedent(indent(text, ' ')), text) + + def test_roundtrip_tabs(self): + # A whitespace prefix should roundtrip with dedent + for text in self.ROUNDTRIP_CASES: + self.assertEqual(dedent(indent(text, '\t\t')), text) + + def test_roundtrip_mixed(self): + # A whitespace prefix should roundtrip with dedent + for text in self.ROUNDTRIP_CASES: + self.assertEqual(dedent(indent(text, ' \t \t ')), text) + + def test_indent_default(self): + # Test default indenting of lines that are not whitespace only + prefix = ' ' + expected = ( + # Basic test case + " Hi.\n This is a test.\n Testing.", + # Include a blank line + " Hi.\n This is a test.\n\n Testing.", + # Include leading and trailing blank lines + "\n Hi.\n This is a test.\n Testing.\n", + # Use Windows line endings + " Hi.\r\n This is a test.\r\n Testing.\r\n", + # Pathological case + "\n Hi.\r\n This is a test.\n\r\n Testing.\r\n\n", + ) + for text, expect in zip(self.CASES, expected): + self.assertEqual(indent(text, prefix), expect) + + def test_indent_explicit_default(self): + # Test default indenting of lines that are not whitespace only + prefix = ' ' + expected = ( + # Basic test case + " Hi.\n This is a test.\n Testing.", + # Include a blank line + " Hi.\n This is a test.\n\n Testing.", + # Include leading and trailing blank lines + "\n Hi.\n This is a test.\n Testing.\n", + # Use Windows line endings + " Hi.\r\n This is a test.\r\n Testing.\r\n", + # Pathological case + "\n Hi.\r\n This is a test.\n\r\n Testing.\r\n\n", + ) + for text, expect in zip(self.CASES, expected): + self.assertEqual(indent(text, prefix, None), expect) + + def test_indent_all_lines(self): + # Add 'prefix' to all lines, including whitespace-only ones. + prefix = ' ' + expected = ( + # Basic test case + " Hi.\n This is a test.\n Testing.", + # Include a blank line + " Hi.\n This is a test.\n \n Testing.", + # Include leading and trailing blank lines + " \n Hi.\n This is a test.\n Testing.\n", + # Use Windows line endings + " Hi.\r\n This is a test.\r\n Testing.\r\n", + # Pathological case + " \n Hi.\r\n This is a test.\n \r\n Testing.\r\n \n", + ) + predicate = lambda line: True + for text, expect in zip(self.CASES, expected): + self.assertEqual(indent(text, prefix, predicate), expect) + + def test_indent_empty_lines(self): + # Add 'prefix' solely to whitespace-only lines. + prefix = ' ' + expected = ( + # Basic test case + "Hi.\nThis is a test.\nTesting.", + # Include a blank line + "Hi.\nThis is a test.\n \nTesting.", + # Include leading and trailing blank lines + " \nHi.\nThis is a test.\nTesting.\n", + # Use Windows line endings + "Hi.\r\nThis is a test.\r\nTesting.\r\n", + # Pathological case + " \nHi.\r\nThis is a test.\n \r\nTesting.\r\n \n", + ) + predicate = lambda line: not line.strip() + for text, expect in zip(self.CASES, expected): + self.assertEqual(indent(text, prefix, predicate), expect) + + def test_main(): support.run_unittest(WrapTestCase, LongWordTestCase, IndentTestCases, - DedentTestCase) + DedentTestCase, + IndentTestCase) if __name__ == '__main__': test_main() diff --git a/Lib/textwrap.py b/Lib/textwrap.py index 66ccf2b3021..7024d4d245a 100644 --- a/Lib/textwrap.py +++ b/Lib/textwrap.py @@ -7,7 +7,7 @@ import re -__all__ = ['TextWrapper', 'wrap', 'fill', 'dedent'] +__all__ = ['TextWrapper', 'wrap', 'fill', 'dedent', 'indent'] # Hardcode the recognized whitespace characters to the US-ASCII # whitespace characters. The main reason for doing this is that in @@ -386,6 +386,25 @@ def dedent(text): text = re.sub(r'(?m)^' + margin, '', text) return text + +def indent(text, prefix, predicate=None): + """Adds 'prefix' to the beginning of selected lines in 'text'. + + If 'predicate' is provided, 'prefix' will only be added to the lines + where 'predicate(line)' is True. If 'predicate' is not provided, + it will default to adding 'prefix' to all non-empty lines that do not + consist solely of whitespace characters. + """ + if predicate is None: + def predicate(line): + return line.strip() + + def prefixed_lines(): + for line in text.splitlines(True): + yield (prefix + line if predicate(line) else line) + return ''.join(prefixed_lines()) + + if __name__ == "__main__": #print dedent("\tfoo\n\tbar") #print dedent(" \thello there\n \t how are you?") diff --git a/Misc/ACKS b/Misc/ACKS index 9c2483c17c1..b89493b79a3 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -82,6 +82,7 @@ Alexander “Саша” Belopolsky Eli Bendersky Andrew Bennetts Andy Bensky +Ezra Berch Michel Van den Bergh Julian Berman Brice Berna diff --git a/Misc/NEWS b/Misc/NEWS index c4dde35f012..13a13204d9a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -21,6 +21,9 @@ Core and Builtins Library ------- +- Issue #13857: Added textwrap.indent() function (initial patch by Ezra + Berch) + - Issue #2736: Added datetime.timestamp() method. - Issue #13854: Make multiprocessing properly handle non-integer