summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/textwrap.rst35
-rw-r--r--Doc/whatsnew/3.3.rst8
-rw-r--r--Lib/test/test_textwrap.py140
-rw-r--r--Lib/textwrap.py21
-rw-r--r--Misc/ACKS1
-rw-r--r--Misc/NEWS3
6 files changed, 201 insertions, 7 deletions
diff --git a/Doc/library/textwrap.rst b/Doc/library/textwrap.rst
index a74789c..50c710c 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 cd57a39..f52d5ae 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 bbd0882..bb4a851 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 66ccf2b..7024d4d 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 9c2483c..b89493b 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 c4dde35..13a1320 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