├── .gitignore
├── LICENSE.txt
├── README.md
├── logo.svg
├── setup.py
└── undent
├── __init__.py
├── __version__.py
└── api.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .#*
3 | \#*
4 | .tox
5 | dist/
6 | .eggs/
7 | build/
8 | *.pyc
9 | *.pyo
10 | *.egg
11 | *.egg-info
12 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2018 Ansgar Grunseid
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Undent
6 |
7 | Undent turns strings stored in source code, which may have newlines,
8 | tabs, and whitespace inserted to adhere to code formatting and linting
9 | rules, into human-readable strings.
10 |
11 | Strings in source code may be indented, bookended with whitespace, or
12 | contain newlines to adhere to code style guidelines. `undent()`
13 | gracefully removes such code style formatting and returns the original,
14 | intended string for human consumption.
15 |
16 | To accomplish this, `undent()`
17 |
18 | 1. [Dedents](https://docs.python.org/3/library/textwrap.html#textwrap.dedent)
19 | the multiline string to remove common indentation.
20 |
21 | 2. Strips preceeding and trailing whitespace, while preserving
22 | post-dedent indentation, to remove whitespace added for source code
23 | formatting.
24 |
25 | 3. Unwraps paragraphs to remove newlines inserted for source code
26 | formatting. E.g. newlines inserted to adhere to PEP 8's 79
27 | characters per line limit.
28 |
29 | Or, optionally line wrap paragraphs to a custom width, e.g. 72
30 | character per line.
31 |
32 |
33 | ### Usage
34 |
35 | Just import `undent()` and give it a multiline string.
36 |
37 | ```python
38 | from undent import undent
39 |
40 | def createEmail():
41 | name = 'Billy'
42 | emailAddr = 'billy@gmail.com'
43 | email = undent(f'''
44 | Hi {name}!
45 |
46 | Thank you for registering with email address
47 |
48 | {emailAddr}
49 |
50 | We'd love to hear from you; please email us
51 | and say hello!''')
52 |
53 | return email
54 | ```
55 |
56 | Above, `undent()` dedents, formats, and wraps the multiline string into
57 | a beautiful, human-readable string.
58 |
59 | ```
60 | Hi Billy!
61 |
62 | Thank you for registering with email address
63 |
64 | billy@gmail.com.
65 |
66 | We'd love to hear from you; please email us and say hello!
67 | ```
68 |
69 | `undent(s, width=False, strip=True)` takes two, optional arguments.
70 |
71 | #### width
72 |
73 | (default: `False`) `width` is the maximum length of wrapped lines, in
74 | characters, or `False` to unwrap lines. If `width` is an integer, as
75 | long as there are no individual words in the input string longer than
76 | `width`, no output line will be wider than `width` characters. Examples:
77 |
78 | ```python
79 | undent('Once upon a time, there was a little girl named Goldilocks.', width=30)
80 | ```
81 |
82 | returns
83 |
84 | ```
85 | Once upon a time, there was a
86 | little girl named Goldilocks.
87 | ```
88 |
89 | Conversely,
90 |
91 | ```python
92 | undent('''Once upon a time, there was a
93 | little girl named Goldilocks.''', width=False)
94 | ```
95 |
96 | returns
97 |
98 | ```
99 | Once upon a time, there was a little girl named Goldilocks.
100 | ```
101 |
102 | #### strip
103 |
104 | (default: `True`) `strip` determines whether or not to remove preceeding
105 | and trailing whitespace. Examples:
106 |
107 | ```python
108 | undent('''
109 | Once upon a time, there was a
110 | little girl named Goldilocks.
111 | ''', strip=True)
112 | ```
113 |
114 | returns
115 |
116 | ```
117 | Once upon a time, there was a little girl named Goldilocks.
118 | ````
119 |
120 | Alternatively
121 |
122 | ```python
123 | undent('''
124 |
125 | Once upon a time, there was a
126 | little girl named Goldilocks.
127 |
128 | ''', strip=False)
129 | ```
130 |
131 | returns
132 |
133 | ```
134 |
135 |
136 | Once upon a time, there was a little girl named Goldilocks.
137 |
138 |
139 | ```
140 |
141 |
142 | ### Examples
143 |
144 | #### `undent()` [dedents](https://docs.python.org/3/library/textwrap.html#textwrap.dedent) the string so indentation added for source code formatting isn't preserved.
145 |
146 | ```python
147 | if True:
148 | print(undent('''common
149 |
150 | indentation
151 |
152 | is removed'''))
153 | ```
154 |
155 | outputs
156 |
157 | ```
158 | common
159 |
160 | indentation
161 |
162 | is removed
163 | ```
164 |
165 | #### `undent()` strips preceeding and trailing whitespace, while preserving post-dedent indentation, so whitespace added for source code formatting isn't unintentionally preserved.
166 |
167 | ```python
168 | if True:
169 | print(undent('''
170 | preceeding
171 |
172 | and trailing
173 |
174 | whitespace is removed
175 | '''))
176 | ```
177 |
178 | outputs
179 |
180 | ```
181 | preceeding
182 |
183 | and trailing
184 |
185 | whitespace is removed
186 | ```
187 |
188 | #### `undent()` unwraps paragraphs so newlines inserted for source code formatting aren't unintentionally preserved in the output, e.g. newlines inserted to avoid lines wider than PEP 8's 80 characters per line.
189 |
190 | ```python
191 | if someIndentation:
192 | if moreIndentation:
193 | if evenDeeperIndentation:
194 | print(undent(f'''
195 | Once upon a time, there was a little girl named
196 | Goldilocks. She went for a walk in the forest. Pretty
197 | soon, she came upon a house. She knocked and, when no
198 | one answered, she walked right in.
199 |
200 | At the table in the kitchen, there were three bowls of
201 | porridge. Goldilocks was hungry. She tasted the porridge
202 | from the first bowl.
203 | '''))
204 | ```
205 |
206 | outputs
207 |
208 | ```
209 | Once upon a time, there was a little girl named Goldilocks. She went for a walk in the forest. Pretty soon, she came upon a house. She knocked and, when no one answered, she walked right in.
210 |
211 | At the table in the kitchen, there were three bowls of porridge. Goldilocks was hungry. She tasted the porridge from the first bowl.
212 | ```
213 |
214 | #### Or, optionally line wrap output paragraphs to your intended width, e.g. 72 character per line.
215 |
216 | ```python
217 | if someIndentation:
218 | if moreIndentation:
219 | if evenDeeperIndentation:
220 | width = 72
221 | print(undent(f'''
222 | Once upon a time, there was a little girl named
223 | Goldilocks. She went for a walk in the forest. Pretty
224 | soon, she came upon a house. She knocked and, when no
225 | one answered, she walked right in.
226 |
227 | At the table in the kitchen, there were three bowls of
228 | porridge. Goldilocks was hungry. She tasted the porridge
229 | from the first bowl.
230 | ''', width))
231 | ```
232 |
233 | outputs
234 |
235 | ```
236 | Once upon a time, there was a little girl named Goldilocks. She went for
237 | a walk in the forest. Pretty soon, she came upon a house. She knocked
238 | and, when no one answered, she walked right in.
239 |
240 | At the table in the kitchen, there were three bowls of porridge.
241 | Goldilocks was hungry. She tasted the porridge from the first bowl.
242 | ```
243 |
244 |
245 | ### Installation
246 |
247 | Installing Undent with pip is easy.
248 |
249 | ```
250 | $ pip install undent
251 | ```
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Undent - Dedent and format multiline strings into human-readable output
6 | #
7 | # Ansgar Grunseid
8 | # grunseid.com
9 | # grunseid@gmail.com
10 | #
11 | # License: MIT
12 | #
13 |
14 | import os
15 | import sys
16 | from os.path import dirname, join as pjoin
17 | from setuptools import setup, find_packages, Command
18 | from setuptools.command.test import test as TestCommand
19 |
20 |
21 | MYNAME = 'undent'
22 |
23 |
24 | meta = {}
25 | with open(pjoin(MYNAME, '__version__.py')) as f:
26 | exec(f.read(), meta)
27 |
28 |
29 |
30 | class Publish(Command):
31 | """Publish to PyPI with twine."""
32 | user_options = []
33 |
34 | def initialize_options(self):
35 | pass
36 |
37 | def finalize_options(self):
38 | pass
39 |
40 | def run(self):
41 | os.system('python setup.py sdist bdist_wheel --universal')
42 |
43 | sdist = 'dist/%s-%s.tar.gz' % (MYNAME, meta['__version__'])
44 | wheel = (
45 | 'dist/%s-%s-py2.py3-none-any.whl' % (MYNAME, meta['__version__']))
46 | rc = os.system('twine upload "%s" "%s"' % (sdist, wheel))
47 |
48 | sys.exit(rc)
49 |
50 |
51 | setup(
52 | name=meta['__title__'],
53 | license=meta['__license__'],
54 | version=meta['__version__'],
55 | author=meta['__author__'],
56 | author_email=meta['__contact__'],
57 | url=meta['__url__'],
58 | description=meta['__description__'],
59 | long_description='\n\n'.join([
60 | meta['__title__'] + ' - ' + meta['__description__'],
61 | 'Information and documentation can be found at %s.' % meta['__url__']]),
62 | platforms=['any'],
63 | packages=find_packages(),
64 | include_package_data=True,
65 | classifiers=[
66 | 'License :: OSI Approved :: MIT License',
67 | 'Natural Language :: English',
68 | 'Intended Audience :: Developers',
69 | 'Topic :: Software Development :: Libraries',
70 | 'Development Status :: 5 - Production/Stable',
71 | 'Programming Language :: Python',
72 | 'Programming Language :: Python :: 2',
73 | 'Programming Language :: Python :: 2.7',
74 | 'Programming Language :: Python :: 3',
75 | 'Programming Language :: Python :: 3.4',
76 | 'Programming Language :: Python :: 3.5',
77 | 'Programming Language :: Python :: 3.6',
78 | 'Programming Language :: Python :: 3.7',
79 | 'Programming Language :: Python :: 3.8',
80 | 'Programming Language :: Python :: 3.9',
81 | 'Programming Language :: Python :: Implementation :: PyPy',
82 | 'Programming Language :: Python :: Implementation :: CPython',
83 | ],
84 | tests_require=[
85 | 'flake8',
86 | ],
87 | install_requires=[],
88 | cmdclass={
89 | 'publish': Publish,
90 | },
91 | )
92 |
--------------------------------------------------------------------------------
/undent/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | #
4 | # Undent - Dedent and format multiline strings into human-readable output
5 | #
6 | # Ansgar Grunseid
7 | # grunseid.com
8 | # grunseid@gmail.com
9 | #
10 | # License: MIT
11 | #
12 |
13 |
14 | from undent.api import undent
15 | from undent.__version__ import (
16 | __title__,
17 | __version__,
18 | __license__,
19 | __author__,
20 | __contact__,
21 | __url__,
22 | __description__)
23 |
--------------------------------------------------------------------------------
/undent/__version__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | #
4 | # Undent - Dedent and format multiline strings into human-readable output
5 | #
6 | # Ansgar Grunseid
7 | # grunseid.com
8 | # grunseid@gmail.com
9 | #
10 | # License: MIT
11 | #
12 |
13 | __title__ = 'undent'
14 | __version__ = '0.2'
15 | __license__ = 'MIT'
16 | __author__ = 'Ansgar Grunseid'
17 | __contact__ = 'grunseid@gmail.com'
18 | __url__ = 'https://github.com/gruns/undent'
19 | __description__ = (
20 | 'Dedent and format multiline strings into human-readable output.')
21 |
--------------------------------------------------------------------------------
/undent/api.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | #
4 | # Undent - Dedent and format multiline strings into human-readable output
5 | #
6 | # Ansgar Grunseid
7 | # grunseid.com
8 | # grunseid@gmail.com
9 | #
10 | # License: MIT
11 | #
12 |
13 | import os
14 | from textwrap import dedent, fill
15 |
16 | try:
17 | from icecream import ic
18 | except ImportError: # Graceful fallback if IceCream isn't installed.
19 | ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # noqa
20 |
21 |
22 | DEFAULT_WRAP_WIDTH = 70 # same as textwrap's default
23 |
24 |
25 | def getIndentation(line):
26 | return line[0:len(line) - len(line.lstrip())]
27 |
28 |
29 | def splitIntoParagraphs(s):
30 | """
31 | Split into paragraphs and the number of newlines before each
32 | paragraph, so whitespace between paragraphs can be preserved. Ex:
33 |
34 | splitIntoParagraphs('''a
35 |
36 | b
37 |
38 |
39 | c''')
40 |
41 | return [(0, 'a'), (1, 'b'), (2, 'c')].
42 | """
43 | paragraphs = []
44 |
45 | paragraphLines = []
46 | numNewlinesBeforeParagraph = 0
47 | for line in s.splitlines():
48 | if not line.strip() and paragraphLines: # end of current paragraph
49 | paragraph = os.linesep.join(paragraphLines)
50 | paragraphs.append((numNewlinesBeforeParagraph, paragraph))
51 | paragraphLines = []
52 | numNewlinesBeforeParagraph = 1
53 | elif not line.strip(): # another empty line before the next paragraph
54 | numNewlinesBeforeParagraph += 1
55 | elif (paragraphLines and # new paragraph with different indentation
56 | getIndentation(line) != getIndentation(paragraphLines[-1])):
57 | paragraph = os.linesep.join(paragraphLines)
58 | paragraphs.append((numNewlinesBeforeParagraph, paragraph))
59 | paragraphLines = [line]
60 | numNewlinesBeforeParagraph = 0
61 | else: # a new line in the current paragraph
62 | paragraphLines.append(line)
63 |
64 | if numNewlinesBeforeParagraph or paragraphLines:
65 | paragraph = os.linesep.join(paragraphLines)
66 | paragraphs.append((numNewlinesBeforeParagraph, paragraph))
67 |
68 | return paragraphs
69 |
70 |
71 | def combineParagraphs(paragraphs):
72 | expanded = [(os.linesep * numNewlines) + p for numNewlines, p in paragraphs]
73 | return os.linesep.join(expanded)
74 |
75 |
76 | def unwrap(paragraph):
77 | toks = [
78 | line.rstrip() if i == 0 else line.strip()
79 | for i, line in enumerate(paragraph.splitlines())]
80 | unwrapped = ' '.join(toks).rstrip()
81 | return unwrapped
82 |
83 |
84 | def lstripEmptyLines(s):
85 | """
86 | Only strip empty lines to preserve initial indentation. Ex
87 |
88 | lstripEmptyLines('''
89 |
90 |
91 | foo
92 | blah''')
93 |
94 | returns ' foo\nblah'.
95 | """
96 | lines = []
97 | for line in s.splitlines():
98 | if lines or line.strip():
99 | lines.append(line)
100 |
101 | s = os.linesep.join(lines)
102 | return s
103 |
104 |
105 | def undent(s, width=False, strip=True):
106 | s = dedent(s)
107 |
108 | if strip:
109 | s = lstripEmptyLines(s) # preserve indentation; only strip empty lines
110 | s = s.rstrip()
111 |
112 | if width is False: # unwrap
113 | paragraphs = [
114 | (newlines, unwrap(p)) for newlines, p in splitIntoParagraphs(s)]
115 | s = combineParagraphs(paragraphs)
116 | elif width:
117 | width = DEFAULT_WRAP_WIDTH if width is True else width
118 | paragraphs = [
119 | (newlines, fill(p, width)) for newlines, p in splitIntoParagraphs(s)]
120 | s = combineParagraphs(paragraphs)
121 |
122 | return s
123 |
--------------------------------------------------------------------------------