Repository: PyCQA/docformatter
Pull request #105 (2022-08-11 00:43:53)

feat: adjust for tab when wrapping
When tabs are used to indent lines and adjusts the wrap length to account for the number of characters a tab represents.

Closes #69


Patch:
diff --git a/README.rst b/README.rst
index a18c91d..7bf11cb 100644
--- a/README.rst
+++ b/README.rst
@@ -207,8 +207,10 @@ Below is the help output::
                             (default: False)
       --force-wrap
                             force descriptions to be wrapped even if it may result
-                            in a mess
-                            (default: False)
+                            in a mess (default: False)
+      --tab_width width
+                            tabs in indentation are this many characters when
+                            wrapping lines (default: 1)
       --range start_line end_line
                             apply docformatter to docstrings between these lines; 
                             line numbers are indexed at 1
@@ -295,7 +297,7 @@ PyCharm
 
 Head over to ``Preferences > Tools > File Watchers``, click the ``+`` icon and configure *docformatter* as shown below:
 
-.. image:: ./images/pycharm-file-watcher-configurations.png
+.. image:: https://github.com/PyCQA/docformatter/blob/master/docs/images/pycharm-file-watcher-configurations.png?raw=true
    :alt: PyCharm file watcher configurations
 
 GitHub Actions
diff --git a/docformatter.py b/docformatter.py
index ad8f08a..2e7422a 100755
--- a/docformatter.py
+++ b/docformatter.py
@@ -191,6 +191,22 @@ class Configurator:
             "set to 0 to disable wrapping "
             "(default: %(default)s)",
         )
+        self.parser.add_argument(
+            "--force-wrap",
+            action="store_true",
+            default=bool(self.flargs_dct.get("force-wrap", False)),
+            help="force descriptions to be wrapped even if it may "
+            "result in a mess (default: %(default)s)",
+        )
+        self.parser.add_argument(
+            "--tab_width",
+            type=int,
+            dest="tab_width",
+            metavar="width",
+            default=int(self.flargs_dct.get("tab-width", 1)),
+            help="tabs in indentation are this many characters when "
+                 "wrapping lines (default: %(default)s)",
+        )
         self.parser.add_argument(
             "--blank",
             dest="post_description_blank",
@@ -231,13 +247,6 @@ class Configurator:
             "one-line docstring wraps to two or more lines "
             "(default: %(default)s)",
         )
-        self.parser.add_argument(
-            "--force-wrap",
-            action="store_true",
-            default=bool(self.flargs_dct.get("force-wrap", False)),
-            help="force descriptions to be wrapped even if it may "
-            "result in a mess (default: %(default)s)",
-        )
         self.parser.add_argument(
             "--range",
             metavar="line",
@@ -412,12 +421,14 @@ def _format_code(
     source,
     summary_wrap_length=79,
     description_wrap_length=72,
+    force_wrap=False,
+    tab_width=1,
     pre_summary_newline=False,
     pre_summary_space=False,
     make_summary_multi_line=False,
     close_quotes_on_newline=False,
     post_description_blank=False,
-    force_wrap=False,
+
     line_range=None,
     length_range=None,
     strict=True,
@@ -462,12 +473,13 @@ def _format_code(
                 token_string,
                 summary_wrap_length=summary_wrap_length,
                 description_wrap_length=description_wrap_length,
+                force_wrap=force_wrap,
+                tab_width=tab_width,
                 pre_summary_newline=pre_summary_newline,
                 pre_summary_space=pre_summary_space,
                 make_summary_multi_line=make_summary_multi_line,
                 close_quotes_on_newline=close_quotes_on_newline,
                 post_description_blank=post_description_blank,
-                force_wrap=force_wrap,
                 strict=strict,
             )
 
@@ -500,12 +512,13 @@ def format_docstring(
     docstring,
     summary_wrap_length=0,
     description_wrap_length=0,
+    force_wrap=False,
+    tab_width=1,
     pre_summary_newline=False,
     pre_summary_space=False,
     make_summary_multi_line=False,
     close_quotes_on_newline=False,
     post_description_blank=False,
-    force_wrap=False,
     strict=True,
 ):
     """Return formatted version of docstring.
@@ -542,6 +555,11 @@ def format_docstring(
         # Something is probably not right with the splitting.
         return docstring
 
+    # Compensate for textwrap counting each tab in indentation as 1 character.
+    tab_compensation = indentation.count('\t') * (tab_width - 1)
+    summary_wrap_length -= tab_compensation
+    description_wrap_length -= tab_compensation
+
     if description:
         # Compensate for triple quotes by temporarily prepending 3 spaces.
         # This temporary prepending is undone below.
@@ -970,12 +988,13 @@ def _format_code_with_args(source, args):
         source,
         summary_wrap_length=args.wrap_summaries,
         description_wrap_length=args.wrap_descriptions,
+        force_wrap=args.force_wrap,
+        tab_width=args.tab_width,
         pre_summary_newline=args.pre_summary_newline,
         pre_summary_space=args.pre_summary_space,
         make_summary_multi_line=args.make_summary_multi_line,
         close_quotes_on_newline=args.close_quotes_on_newline,
         post_description_blank=args.post_description_blank,
-        force_wrap=args.force_wrap,
         line_range=args.line_range,
         strict=not args.non_strict,
     )


Test patch:
diff --git a/images/pycharm-file-watcher-configurations.png b/docs/images/pycharm-file-watcher-configurations.png
similarity index 100%
rename from images/pycharm-file-watcher-configurations.png
rename to docs/images/pycharm-file-watcher-configurations.png
diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py
index b63e51a..f5394e0 100644
--- a/tests/test_format_docstring.py
+++ b/tests/test_format_docstring.py
@@ -421,7 +421,7 @@ num_iterations is the number of updates - instead of a better definition of conv
             )
         )
 
-    @pytest.mark.xfail
+    @pytest.mark.unit
     def test_format_docstring_with_summary_only_and_wrap_and_tab_indentation(
         self,
     ):