]> granicus.if.org Git - python/commitdiff
Close #13857: Added textwrap.indent() function (initial patch by Ezra
authorNick Coghlan <ncoghlan@gmail.com>
Mon, 11 Jun 2012 13:07:51 +0000 (23:07 +1000)
committerNick Coghlan <ncoghlan@gmail.com>
Mon, 11 Jun 2012 13:07:51 +0000 (23:07 +1000)
  Berch)

Doc/library/textwrap.rst
Doc/whatsnew/3.3.rst
Lib/test/test_textwrap.py
Lib/textwrap.py
Misc/ACKS
Misc/NEWS

index a74789ce6bb594b5bf0f9c868440ecff5317fdc3..50c710c522e4e840e467405f986b0edae6c8edb6 100644 (file)
@@ -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
index cd57a393061c358a6e0092d4e3eb892991c12263..f52d5ae29d3a97febed2e96a1c7bcecdad7508b7 100644 (file)
@@ -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
 ----
index bbd0882bda66259f2548d7801a40a9ff5d2a72f1..bb4a851917202cf127f01eea1983e87cb2df036a 100644 (file)
@@ -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()
index 66ccf2b30219b55ed6107760b5efca9eb68a7dca..7024d4d245aed6e0c919c6150b37354eaef258f2 100644 (file)
@@ -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?")
index 9c2483c17c18655a58dabc39bc123ff58d207b14..b89493b79a313e72f2a565c0d30c19cc666a45f0 100644 (file)
--- 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
index c4dde35f0125ed44ec42fe7cbe7eef4bac68fe9b..13a13204d9a5ee8f848615810aa7c1bce558b8e9 100644 (file)
--- 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