diff --git a/src/docformatter/format.py b/src/docformatter/format.py index 3634f65..a2b1453 100644 --- a/src/docformatter/format.py +++ b/src/docformatter/format.py @@ -1071,12 +1071,6 @@ def _do_format_docstring( # noqa PLR0911 # Something probably isn't right with the splitting. return docstring - # Compensate for textwrap counting each tab in indentation as 1 - # character. - tab_compensation = indentation.count("\t") * (self.args.tab_width - 1) - self.args.wrap_summaries -= tab_compensation - self.args.wrap_descriptions -= tab_compensation - if description: return self._do_format_multiline_docstring( indentation, @@ -1114,12 +1108,16 @@ def _do_format_oneline_docstring( str The formatted docstring. """ + # Keep tab compensation local to this docstring. + wrap_length = self.args.wrap_summaries - indentation.count("\t") * ( + self.args.tab_width - 1 + ) if self.args.make_summary_multi_line: beginning = f"{open_quote}\n{indentation}" ending = f'\n{indentation}"""' summary_wrapped = _wrappers.do_wrap_summary( _strings.do_normalize_summary(contents, self.args.non_cap), - wrap_length=self.args.wrap_summaries, + wrap_length=wrap_length, initial_indent=indentation, subsequent_indent=indentation, ).strip() @@ -1129,7 +1127,7 @@ def _do_format_oneline_docstring( open_quote + _strings.do_normalize_summary(contents, self.args.non_cap) + '"""', - wrap_length=self.args.wrap_summaries, + wrap_length=wrap_length, initial_indent=indentation, subsequent_indent=indentation, ).strip() @@ -1165,6 +1163,7 @@ def _do_format_multiline_docstring( str The formatted docstring. """ + tab_compensation = indentation.count("\t") * (self.args.tab_width - 1) # Compensate for triple quotes by temporarily prepending 3 spaces. # This temporary prepending is undone below. initial_indent = ( @@ -1173,14 +1172,14 @@ def _do_format_multiline_docstring( pre_summary = "\n" + indentation if self.args.pre_summary_newline else "" summary = _wrappers.do_wrap_summary( _strings.do_normalize_summary(summary, self.args.non_cap), - wrap_length=self.args.wrap_summaries, + wrap_length=self.args.wrap_summaries - tab_compensation, initial_indent=initial_indent, subsequent_indent=indentation, ).lstrip() description = _wrappers.do_wrap_description( description, indentation=indentation, - wrap_length=self.args.wrap_descriptions, + wrap_length=self.args.wrap_descriptions - tab_compensation, force_wrap=self.args.force_wrap, strict=self.args.non_strict, rest_sections=self.args.rest_section_adorns, diff --git a/tests/formatter/test_tab_wrapping_state.py b/tests/formatter/test_tab_wrapping_state.py new file mode 100644 index 0000000..6b04388 --- /dev/null +++ b/tests/formatter/test_tab_wrapping_state.py @@ -0,0 +1,103 @@ +"""Keep tab-width compensation local to each formatted docstring.""" + +import ast +import copy +import sys + +import pytest + +from docformatter import Formatter + + +@pytest.mark.integration +@pytest.mark.parametrize( + "args", + [ + ["--tab-width", "4", "--wrap-summaries", "40", "--wrap-descriptions", "40", ""], + [ + "--tab-width", + "8", + "--wrap-summaries", + "40", + "--wrap-descriptions", + "40", + "--make-summary-multi-line", + "", + ], + [ + "--tab-width", + "4", + "--wrap-summaries", + "40", + "--wrap-descriptions", + "40", + "--black", + "", + ], + ["--tab-width", "4", "--wrap-summaries", "0", "--wrap-descriptions", "40", ""], + ["--tab-width", "4", "--wrap-summaries", "40", "--wrap-descriptions", "0", ""], + ["--tab-width", "4", "--wrap-summaries", "0", "--wrap-descriptions", "0", ""], + ], +) +@pytest.mark.parametrize("indentation", ["\t", "\t\t"]) +@pytest.mark.parametrize("with_description", [False, True]) +def test_tab_wrapping_is_independent_of_previous_docstrings( + args, test_args, indentation, with_description +): + """Repeated docstrings and files must not consume the configured wrap widths.""" + widths = (test_args.wrap_summaries, test_args.wrap_descriptions) + summary = "Return the requested value with several words in the summary." + docstring = '"""' + summary + if with_description: + docstring += ( + "\n\n" + + indentation + + "Describe the returned value with enough words to require wrapping." + + "\n" + + indentation + ) + docstring += '"""' + source = f"def function_0():\n{indentation}{docstring}\n{indentation}pass\n\n" + original_args = copy.copy(test_args) + formatter = Formatter(test_args, sys.stderr, sys.stdin, sys.stdout) + + expected = formatter._do_format_code(source) + assert (test_args.wrap_summaries, test_args.wrap_descriptions) == widths + ast.parse(expected) + assert all( + line.startswith(indentation) for line in expected.splitlines()[1:] if line + ) + if all(width > 0 for width in widths): + assert all( + len(line.expandtabs(test_args.tab_width)) <= min(widths) + for line in expected.splitlines() + ) + if widths[0] == 0: + assert summary in expected + if with_description and widths[1] == 0: + assert ( + "Describe the returned value with enough words to require wrapping." + in expected + ) + assert formatter._do_format_code(expected) == expected + # The same Formatter is reused when processing multiple files. + assert formatter._do_format_code(source) == expected + + repeated_source = "".join( + source.replace("function_0", f"function_{i}") for i in range(13) + ) + repeated_expected = "".join( + expected.replace("function_0", f"function_{i}") for i in range(13) + ) + formatted = formatter._do_format_code(repeated_source) + assert formatted == repeated_expected + ast.parse(formatted) + assert formatter._do_format_code(formatted) == formatted + assert (test_args.wrap_summaries, test_args.wrap_descriptions) == widths + + # A preceding tab-indented file must not affect a space-indented file. + space_source = source.replace("\t", " ") + fresh_formatter = Formatter(original_args, sys.stderr, sys.stdin, sys.stdout) + assert formatter._do_format_code(space_source) == fresh_formatter._do_format_code( + space_source + )