Repository: PyCQA/docformatter
Pull request #74 (2021-03-31 22:20:41)

Bugfix --wrap-summaries 0 now fully disables summary wrapping
Fixes https://github.com/myint/docformatter/issues/73


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0be11fa..448faff 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,7 @@ jobs:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
       run: |
-        python setup.py install
+        pip install .[tomli]
         pip install --quiet coverage coveralls
     - name: Run tests
       run: make coverage
diff --git a/.gitignore b/.gitignore
index 0463754..d7d48e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,5 +11,6 @@ build/
 dist/
 htmlcov/
 .coverage
+.python-version
 .idea/
 .vscode/
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 71adf43..65db8d2 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -10,3 +10,4 @@ Patches
 - Peter Boothe (https://github.com/pboothe)
 - Kapshuna Alexander (https://github.com/kapsh)
 - Serhiy Yevtushenko (https://github.com/serhiy-yevtushenko)
+- Doyle Rowland (https://github.com/weibullguy)
diff --git a/Makefile b/Makefile
index 8c6a8c9..ff1eebc 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,6 @@ check:
 		setup.py
 	pylint \
 		--reports=no \
-		--disable=bad-continuation \
 		--disable=fixme \
 		--disable=import-outside-toplevel \
 		--disable=inconsistent-return-statements \
diff --git a/README.rst b/README.rst
index b3aa9c6..828feec 100644
--- a/README.rst
+++ b/README.rst
@@ -130,7 +130,8 @@ Below is the help output::
     usage: docformatter [-h] [-i | -c] [-r] [--wrap-summaries length]
                         [--wrap-descriptions length] [--blank]
                         [--pre-summary-newline] [--make-summary-multi-line]
-                        [--force-wrap] [--range line line] [--version]
+                        [--force-wrap] [--range start_line end_line]
+                        [--docstring-length min_length max_length] [--version]
                         files [files ...]
 
     Formats docstrings to follow PEP 257.
@@ -160,9 +161,13 @@ Below is the help output::
                             line docstring
       --force-wrap          force descriptions to be wrapped even if it may result
                             in a mess
-      --range line line     apply docformatter to docstrings between these lines;
+      --range start_line end_line
+                            apply docformatter to docstrings between these lines;
                             line numbers are indexed at 1
+      --docstring-length min_length max_length
+                            apply docformatter to docstrings of given length range
       --version             show program's version number and exit
+      --config CONFIG       path to file containing docformatter options
 
 
 Possible exit codes:
@@ -170,6 +175,20 @@ Possible exit codes:
 - **1** - if any error encountered
 - **3** - if any file needs to be formatted (in ``--check`` mode)
 
+docformatter options can also be stored in a configuration file.  Currently only
+pyproject.toml is supported.  Add section [tool.docformatter] with options listed using
+the same name as command line options.  For example::
+
+      [tool.docformatter]
+      recursive = true
+      wrap-summaries = 82
+      blank = true
+
+Command line options take precedence.  The configuration file can be passed with a full
+path, otherwise docformatter will look in the current directory.  For example::
+
+      docformatter --config ~/.secret/path/to/pyproject.toml
+
 Wrapping descriptions
 =====================
 
diff --git a/docformatter.py b/docformatter.py
index 07213e7..6bbf87c 100755
--- a/docformatter.py
+++ b/docformatter.py
@@ -41,6 +41,12 @@ import tokenize
 import sysconfig
 import untokenize
 
+try:
+    import tomli
+
+    TOMLI_INSTALLED = True
+except ImportError:
+    TOMLI_INSTALLED = False
 
 __version__ = '1.4'
 
@@ -56,6 +62,7 @@ HEURISTIC_MIN_LIST_ASPECT_RATIO = .4
 CR = '\r'
 LF = '\n'
 CRLF = '\r\n'
+QUOTE_TYPES = ('"""', "'''", )
 
 _PYTHON_LIBS = set(sysconfig.get_paths().values())
 
@@ -137,7 +144,7 @@ def _format_code(source,
 
         if (
             token_type == tokenize.STRING and
-            token_string.startswith(('"', "'")) and
+            token_string.startswith(QUOTE_TYPES) and
             (previous_token_type == tokenize.INDENT or
                 only_comments_so_far) and
             is_in_range(line_range, start[0], end[0]) and
@@ -193,7 +200,7 @@ def format_docstring(indentation, docstring,
     contents = strip_docstring(docstring)
 
     # Skip if there are nested triple double quotes
-    if contents.count('"""'):
+    if contents.count(QUOTE_TYPES[0]):
         return docstring
 
     # Do not modify things that start with doctests.
@@ -237,25 +244,25 @@ def format_docstring(indentation, docstring,
             post_description=('\n' if post_description_blank else ''),
             indentation=indentation)
     else:
-        if make_summary_multi_line:
-            beginning = '"""\n' + indentation
-            ending = '\n' + indentation + '"""'
-            summary_wrapped = wrap_summary(
-                normalize_summary(contents),
-                wrap_length=summary_wrap_length,
-                initial_indent=indentation,
-                subsequent_indent=indentation).strip()
-            return '{beginning}{summary}{ending}'.format(
-                beginning=beginning,
-                summary=summary_wrapped,
-                ending=ending
-            )
-        else:
+        if not make_summary_multi_line:
             return wrap_summary('"""' + normalize_summary(contents) + '"""',
                                 wrap_length=summary_wrap_length,
                                 initial_indent=indentation,
                                 subsequent_indent=indentation).strip()
 
+        beginning = '"""\n' + indentation
+        ending = '\n' + indentation + '"""'
+        summary_wrapped = wrap_summary(
+            normalize_summary(contents),
+            wrap_length=summary_wrap_length,
+            initial_indent=indentation,
+            subsequent_indent=indentation).strip()
+        return '{beginning}{summary}{ending}'.format(
+            beginning=beginning,
+            summary=summary_wrapped,
+            ending=ending
+        )
+
 
 def reindent(text, indentation):
     """Return reindented text that matches indentation."""
@@ -448,22 +455,33 @@ def normalize_line_endings(lines, newline):
     return ''.join([normalize_line(line, newline) for line in lines])
 
 
-def strip_docstring(docstring):
-    """Return contents of docstring."""
+def strip_docstring(docstring: str) -> str:
+    """Return contents of docstring.
+
+    :param docstring: the docstring, including the opening and closing triple
+        quotes.
+    :return: docstring with the triple quotes removed.
+    :rtype: str
+    """
     docstring = docstring.strip()
-    quote_types = ["'''", '"""', "'", '"']
 
-    for quote in quote_types:
+    for quote in QUOTE_TYPES:
         if docstring.startswith(quote) and docstring.endswith(quote):
             return docstring.split(quote, 1)[1].rsplit(quote, 1)[0].strip()
 
-    raise ValueError('We only handle strings that start with quotes')
+    raise ValueError("docformatter only handles strings that start with "
+                     "triple quotes")
+
+
+def unwrap_summary(summary):
+    """Return summary with newlines removed in preparation for wrapping."""
+    return re.sub(r'\s*\n\s*', ' ', summary)
 
 
 def normalize_summary(summary):
     """Return normalized docstring summary."""
-    # Remove newlines
-    summary = re.sub(r'\s*\n\s*', ' ', summary.rstrip())
+    # remove trailing whitespace
+    summary = summary.rstrip()
 
     # Add period at end of sentence
     if (
@@ -480,7 +498,7 @@ def wrap_summary(summary, initial_indent, subsequent_indent, wrap_length):
     """Return line-wrapped summary text."""
     if wrap_length > 0:
         return '\n'.join(
-            textwrap.wrap(summary,
+            textwrap.wrap(unwrap_summary(summary),
                           width=wrap_length,
                           initial_indent=initial_indent,
                           subsequent_indent=subsequent_indent)).strip()
@@ -594,12 +612,12 @@ def format_file(filename, args, standard_out):
                 output_file.write(formatted_source)
         else:
             import difflib
-            diff = difflib.unified_diff(
-                source.splitlines(),
-                formatted_source.splitlines(),
-                'before/' + filename,
-                'after/' + filename,
-                lineterm='')
+            diff = difflib.unified_diff(source.splitlines(),
+                                        formatted_source.splitlines(),
+                                        f'before/{filename}',
+                                        f'after/{filename}',
+                                        lineterm='')
+
             standard_out.write('\n'.join(list(diff) + ['']))
 
     return FormatResult.ok
@@ -618,9 +636,45 @@ def _format_code_with_args(source, args):
         line_range=args.line_range)
 
 
+def find_config_file(args):
+    """Find the configuration file the user specified."""
+    config_files = ["pyproject.toml"]
+    flargs = {}
+
+    config_file = args[args.index('--config') + 1]
+
+    if os.path.isfile(config_file):
+        argfile = os.path.basename(config_file)
+        for f in config_files:
+            if argfile == f:
+                flargs = read_configuration_from_file(config_file)
+
+    return flargs
+
+
+def read_configuration_from_file(configfile):
+    """Read docformatter options from a configuration file."""
+    flargs = {}
+    fullpath, ext = os.path.splitext(configfile)
+    filename = os.path.basename(fullpath)
+
+    if ext == ".toml" and TOMLI_INSTALLED and filename == "pyproject":
+        with open(configfile, "rb") as f:
+            config = tomli.load(f)
+
+        result = config.get("tool", {}).get("docformatter", None)
+        if result is not None:
+            flargs = {k: v if isinstance(v, list) else str(v)
+                      for k, v in result.items()}
+
+    return flargs
+
+
 def _main(argv, standard_out, standard_error, standard_in):
     """Run internal main entry point."""
     import argparse
+
+    flargs = find_config_file(argv) if "--config" in argv else {}
     parser = argparse.ArgumentParser(description=__doc__, prog='docformatter')
     changes = parser.add_mutually_exclusive_group()
     changes.add_argument('-i', '--in-place', action='store_true',
@@ -630,44 +684,59 @@ def _main(argv, standard_out, standard_error, standard_in):
                          help='only check and report incorrectly formatted '
                               'files')
     parser.add_argument('-r', '--recursive', action='store_true',
+                        default=bool(flargs.get('recursive', False)),
                         help='drill down directories recursively')
     parser.add_argument('-e', '--exclude', nargs='*',
                         help='exclude directories and files by names')
-    parser.add_argument('--wrap-summaries', default=79, type=int,
+    parser.add_argument('--wrap-summaries',
+                        default=int(flargs.get('wrap-summaries', 79)),
+                        type=int,
                         metavar='length',
                         help='wrap long summary lines at this length; '
                              'set to 0 to disable wrapping '
                              '(default: %(default)s)')
-    parser.add_argument('--wrap-descriptions', default=72, type=int,
+    parser.add_argument('--wrap-descriptions',
+                        default=int(flargs.get('wrap-descriptions', 72)),
+                        type=int,
                         metavar='length',
                         help='wrap descriptions at this length; '
                              'set to 0 to disable wrapping '
                              '(default: %(default)s)')
     parser.add_argument('--blank', dest='post_description_blank',
                         action='store_true',
+                        default=bool(flargs.get('blank', False)),
                         help='add blank line after description')
     parser.add_argument('--pre-summary-newline',
                         action='store_true',
+                        default=bool(flargs.get('pre-summary-newline', False)),
                         help='add a newline before the summary of a '
                              'multi-line docstring')
     parser.add_argument('--make-summary-multi-line',
                         action='store_true',
+                        default=bool(flargs.get('make-summary-multi-line',
+                                                False)),
                         help='add a newline before and after the summary of a '
                              'one-line docstring')
     parser.add_argument('--force-wrap', action='store_true',
+                        default=bool(flargs.get('force-wrap', False)),
                         help='force descriptions to be wrapped even if it may '
                              'result in a mess')
     parser.add_argument('--range', metavar='line', dest='line_range',
-                        default=None, type=int, nargs=2,
+                        default=flargs.get('range', None), type=int, nargs=2,
                         help='apply docformatter to docstrings between these '
                              'lines; line numbers are indexed at 1')
     parser.add_argument('--docstring-length', metavar='length',
                         dest='length_range',
-                        default=None, type=int, nargs=2,
+                        default=flargs.get('docstring-length', None),
+                        type=int, nargs=2,
                         help='apply docformatter to docstrings of given '
-                             'length')
-    parser.add_argument('--version', action='version',
-                        version='%(prog)s ' + __version__)
+                             'length range')
+    parser.add_argument('--version',
+                        action='version',
+                        version=f'%(prog)s {__version__}')
+
+    parser.add_argument('--config',
+                        help='path to file containing docformatter options')
     parser.add_argument('files', nargs='+',
                         help="files to format or '-' for standard in")
 
diff --git a/setup.py b/setup.py
index 51da2bc..a9aae38 100755
--- a/setup.py
+++ b/setup.py
@@ -22,27 +22,31 @@ def version():
 
 
 setup(name='docformatter',
-        version=version(),
-        description='Formats docstrings to follow PEP 257.',
-        long_description=Path('README.rst').read_text(),
-        license='Expat License',
-        author='Steven Myint',
-        url='https://github.com/myint/docformatter',
-        classifiers=['Intended Audience :: Developers',
-                    'Environment :: Console',
-                    'Programming Language :: Python :: 3',
-                    'Programming Language :: Python :: 3.6',
-                    'Programming Language :: Python :: 3.7',
-                    'Programming Language :: Python :: 3.8',
-                    'Programming Language :: Python :: 3.9',
-                    'Programming Language :: Python :: 3.10',
-                    'Programming Language :: Python :: Implementation',
-                    'Programming Language :: Python :: Implementation :: PyPy',
-                    'Programming Language :: Python :: Implementation :: CPython',
-                    'License :: OSI Approved :: MIT License'],
-        keywords='PEP 257, pep257, style, formatter, docstrings',
-        py_modules=['docformatter'],
-        entry_points={
-            'console_scripts': ['docformatter = docformatter:main']},
-        install_requires=['untokenize'],
-        test_suite='test_docformatter')
+      version=version(),
+      description='Formats docstrings to follow PEP 257.',
+      long_description=Path('README.rst').read_text(),
+      license='Expat License',
+      author='Steven Myint',
+      url='https://github.com/myint/docformatter',
+      classifiers=[
+          'Intended Audience :: Developers',
+          'Environment :: Console',
+          'Programming Language :: Python :: 3',
+          'Programming Language :: Python :: 3.6',
+          'Programming Language :: Python :: 3.7',
+          'Programming Language :: Python :: 3.8',
+          'Programming Language :: Python :: 3.9',
+          'Programming Language :: Python :: 3.10',
+          'Programming Language :: Python :: Implementation',
+          'Programming Language :: Python :: Implementation :: PyPy',
+          'Programming Language :: Python :: Implementation :: CPython',
+          'License :: OSI Approved :: MIT License',
+      ],
+      keywords='PEP 257, pep257, style, formatter, docstrings',
+      py_modules=['docformatter'],
+      entry_points={
+          'console_scripts': ['docformatter = docformatter:main']},
+      install_requires=['untokenize'],
+      extras_require={"tomli": ["tomli"]},
+      test_suite='test_docformatter')
+


Test patch:
diff --git a/test_docformatter.py b/test_docformatter.py
index 0908135..bd2f0fc 100755
--- a/test_docformatter.py
+++ b/test_docformatter.py
@@ -86,14 +86,6 @@ class TestUnits(unittest.TestCase):
     def test_strip_docstring_with_empty_string(self):
         self.assertEqual('', docformatter.strip_docstring('""""""'))
 
-    def test_strip_docstring_with_escaped_quotes(self):
-        self.assertEqual("hello\\'",
-                         docformatter.strip_docstring("'hello\\''"))
-
-    def test_strip_docstring_with_escaped_double_quotes(self):
-        self.assertEqual('hello\\"',
-                         docformatter.strip_docstring('"hello\\""'))
-
     def test_strip_docstring_with_unhandled(self):
         with self.assertRaises(ValueError):
             docformatter.strip_docstring('r"""foo"""')
@@ -207,8 +199,7 @@ Hello.
     """''',
             docformatter.format_docstring(
                 '    ',
-                docstring='''"""Foo bar
-        bing bang.
+                docstring='''"""Foo bar bing bang.
 
         >>> tests = DocTestFinder().find(_TestClass)
         >>> runner = DocTestRunner(verbose=False)
@@ -788,30 +779,62 @@ def foo():
     """
 ''')))
 
-    def test_format_code_with_double_quote(self):
+    def test_ignore_code_with_single_quote(self):
+        """Single single quote on first line of code should remain untouched.
+
+        See requirement 1, always use triple double quotes.
+        See issue #66 for example of docformatter breaking code when encountering
+        single quote.
+        """
         self.assertEqual(
-            '''\
+                '''\
 def foo():
-    """Just a regular string."""
+    'Just a regular string'
 ''',
             docformatter.format_code(
                 '''\
 def foo():
-    "Just a regular string"
+    'Just a regular string'
 '''))
 
-    def test_format_code_with_single_quote(self):
+    def test_ignore_string_with_escaped_quotes(self):
+        """Strings beginning with single single quotes should raise a ValueError.
+
+        See requirement 1, always use triple double quotes.
+        See issue #66 for example of docformatter breaking code when encountering
+        single quote.
+        """
+        with self.assertRaises(ValueError):
+            docformatter.strip_docstring("'hello\\''")
+
+    def test_ignore_code_with_double_quote(self):
+        """Single double quotes on first line of code should remain untouched.
+
+        See requirement 1, always use triple double quotes.
+        See issue #66 for example of docformatter breaking code when encountering
+        single quote.
+        """
         self.assertEqual(
-            '''\
+                '''\
 def foo():
-    """Just a regular string."""
+    "Just a regular string"
 ''',
             docformatter.format_code(
                 '''\
 def foo():
-    'Just a regular string'
+    "Just a regular string"
 '''))
 
+    def test_strip_docstring_with_escaped_double_quotes(self):
+        """Strings beginning with single double quotes should raise a ValueError.
+
+        See requirement 1, always use triple double quotes.
+        See issue #66 for example of docformatter breaking code when encountering
+        single quote.
+        """
+        with self.assertRaises(ValueError):
+            docformatter.strip_docstring('"hello\\""')
+
     def test_format_code_with_should_skip_nested_triple_quotes(self):
         line = '''\
 def foo():
@@ -1048,22 +1071,32 @@ Try this and this and this and this and this and this and this at
                 (text, ''),
                 docformatter.split_summary_and_description(text))
 
+    def test_unwrap_summary(self):
+        self.assertEqual(
+            'This is a sentence.',
+            docformatter.unwrap_summary('This \n\t is\na sentence.'))
+
     def test_normalize_summary(self):
         self.assertEqual(
             'This is a sentence.',
-            docformatter.normalize_summary('This \n\t is\na sentence'))
+            docformatter.normalize_summary('This is a sentence '))
+
+    def test_normalize_summary_multiline(self):
+        self.assertEqual(
+            'This \n\t is\na sentence.',
+            docformatter.normalize_summary('This \n\t is\na sentence '))
 
     def test_normalize_summary_with_different_punctuation(self):
         summary = 'This is a question?'
         self.assertEqual(
             summary,
             docformatter.normalize_summary(summary))
-        
+
     def test_normalize_summary_formatted_as_title(self):
         summary = '# This is a title'
         self.assertEqual(
             summary,
-            docformatter.normalize_summary(summary))    
+            docformatter.normalize_summary(summary))
 
     def test_detect_encoding_with_bad_encoding(self):
         with temporary_file('# -*- coding: blah -*-\n') as filename:
@@ -1271,6 +1304,25 @@ num_iterations is the number of updates - instead of a better definition of conv
             self.assertEqual(test_exclude_nothing, ['/root/folder_one/one.py', '/root/folder_one/folder_three/three.py',
                                                     '/root/folder_two/two.py'])
 
+    def test_read_config_file(self):
+        f = open('pyproject.toml', 'w+')
+        try:
+            f.write('[tool.docformatter]\nrecursive = true\nwrap-summaries = 82\n')
+            f.close()
+            self.assertEqual(docformatter.find_config_file(['--config',
+                                                            './tests/_data/pyproject.toml']),
+                         {'recursive': 'True', 'wrap-summaries': '82'})
+        finally:
+            os.remove(f.name)
+
+    def test_missing_config_file(self):
+        self.assertEqual(docformatter.find_config_file(['--config', 'pyproject.toml']),
+                         {})
+
+    def test_unsupported_config_file(self):
+        self.assertEqual(docformatter.find_config_file(['--config', 'tox.ini']),
+                         {})
+
 class TestSystem(unittest.TestCase):
 
     def test_diff(self):
@@ -1347,7 +1399,7 @@ def foo():
         with temporary_file('''\
 def foo():
     """
-    Hello world
+    Hello world 
     """
 ''') as filename:
             process = run_docformatter([filename])
@@ -1355,10 +1407,10 @@ def foo():
 @@ -1,4 +1,2 @@
  def foo():
 -    """
--    Hello world
+-    Hello world 
 -    """
 +    """Hello world."""
-''', '\n'.join(process.communicate()[0].decode().split('\n')[2:]))
+''', '\n'.join(process.communicate()[0].decode().replace("\r", "").split('\n')[2:]))
 
     def test_end_to_end_with_wrapping(self):
         with temporary_file('''\
@@ -1377,7 +1429,7 @@ def foo():
 -    """
 +    """Hello world this is a summary
 +    that will get wrapped."""
-''', '\n'.join(process.communicate()[0].decode().split('\n')[2:]))
+''', '\n'.join(process.communicate()[0].decode().replace("\r", "").split('\n')[2:]))
 
     def test_end_to_end_with_no_wrapping(self):
         with temporary_file('''\
@@ -1392,7 +1444,41 @@ def foo():
                                         filename])
             self.assertEqual(
                 '',
-                '\n'.join(process.communicate()[0].decode().split('\n')[2:]))
+                '\n'.join(process.communicate()[0].decode().replace("\r", "").split('\n')[2:]))
+
+    def test_end_to_end_with_no_wrapping_2(self):
+        with temporary_file('''\
+def foo():
+    """Hello world is a long sentence that will not
+    be wrapped because I turned wrapping off.
+
+    Hello world is a long sentence that will not
+    be wrapped because I turned wrapping off.
+    """
+''') as filename:
+            process = run_docformatter(['--wrap-summaries=0',
+                                        '--wrap-description=0',
+                                        filename])
+            self.assertEqual(
+                '',
+                '\n'.join(process.communicate()[0].decode().replace("\r", "").split('\n')[2:]))
+
+    def test_end_to_end_no_wrapping_period(self):
+        with temporary_file('''\
+def foo():
+    """Wrapping is off, but it will still add
+    the trailing period  """
+''') as filename:
+            process = run_docformatter(['--wrap-summaries=0',
+                                        filename])
+            self.assertEqual('''\
+@@ -1,3 +1,3 @@
+ def foo():
+     """Wrapping is off, but it will still add
+-    the trailing period  """
++    the trailing period."""
+''', '\n'.join(process.communicate()[0].decode().replace("\r", "").split('\n')[2:]))
+
 
     def test_end_to_end_all_options(self):
         with temporary_file('''\
@@ -1423,7 +1509,7 @@ def foo():
  
 -
      """
-''', '\n'.join(process.communicate()[0].decode().split('\n')[2:]))
+''', '\n'.join(process.communicate()[0].decode().replace("\r", "").split('\n')[2:]))
 
     def test_invalid_range(self):
         process = run_docformatter(['--range', '0', '1', os.devnull])
@@ -1445,7 +1531,7 @@ def foo():
         result = process.communicate('''\
 """
 Hello world"""
-'''.encode())[0].decode()
+'''.encode())[0].decode().replace("\r", "")
 
         self.assertEqual(0, process.returncode)
 
@@ -1506,6 +1592,30 @@ Print my path and return error code
             self.assertEqual(stderr.getvalue().strip(), filename,
                              msg='Changed file should be reported')
 
+    def test_cli_override_config_file(self):
+        f = open('pyproject.toml', 'w+')
+        try:
+            f.write('[tool.docformatter]\nrecursive = true\nwrap-summaries = 82\n')
+            f.close()
+            with temporary_file('''\
+def foo():
+    """
+    Hello world
+    """
+''') as filename:
+                process = run_docformatter(['--wrap-summaries=79',
+                                            filename])
+                self.assertEqual('''\
+@@ -1,4 +1,2 @@
+ def foo():
+-    """
+-    Hello world
+-    """
++    """Hello world."""
+''', '\n'.join(process.communicate()[0].decode().split('\n')[2:]))
+        finally:
+            os.remove(f.name)
+
 
 def generate_random_docstring(max_indentation_length=32,
                               max_word_length=20,
diff --git a/tests/_data/pyproject.toml b/tests/_data/pyproject.toml
new file mode 100644
index 0000000..5503646
--- /dev/null
+++ b/tests/_data/pyproject.toml
@@ -0,0 +1,4 @@
+[tool.docformatter]
+recursive = true
+wrap-summaries = 82
+