├── .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 | Undent 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 | image/svg+xml 46 | 54 | 62 | 70 | 78 | 86 | 94 | 104 | 110 | 113 | 121 | 122 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------