Skip to content

Commit 5799b7f

Browse files
DanielNoordjspaezp
andcommitted
Do a double (or triple) pass on each file
Co-authored-by: J. Sebastian Paez <[email protected]>
1 parent ad9f251 commit 5799b7f

File tree

3 files changed

+150
-19
lines changed

3 files changed

+150
-19
lines changed

pydocstringformatter/run.py

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from pydocstringformatter import __version__, _formatting, _utils
1111
from pydocstringformatter._configuration.arguments_manager import ArgumentsManager
12+
from pydocstringformatter._utils.exceptions import UnstableResultError
1213

1314

1415
class _Run:
@@ -64,7 +65,7 @@ def format_file(self, filename: Path) -> bool:
6465
# the same later on.
6566
newlines = file.newlines
6667

67-
formatted_tokens, is_changed = self.format_file_tokens(tokens)
68+
formatted_tokens, is_changed = self.format_file_tokens(tokens, filename)
6869

6970
if is_changed:
7071
try:
@@ -112,30 +113,91 @@ def get_enabled_formatters(self) -> dict[str, _formatting.Formatter]:
112113
return enabled
113114

114115
def format_file_tokens(
115-
self, tokens: list[tokenize.TokenInfo]
116+
self, tokens: list[tokenize.TokenInfo], filename: Path
116117
) -> tuple[list[tokenize.TokenInfo], bool]:
117-
"""Format a list of tokens."""
118+
"""Format a list of tokens.
119+
120+
tokens: List of tokens to format.
121+
filename: Name of the file the tokens are from.
122+
123+
Returns:
124+
A tuple containing [1] the formatted tokens in a list
125+
and [2] a boolean indicating if the tokens were changed.
126+
127+
Raises:
128+
UnstableResultError::
129+
If the formatters are not able to get to a stable result.
130+
It reports what formatters are still modifying the tokens.
131+
"""
118132
formatted_tokens: list[tokenize.TokenInfo] = []
119133
is_changed = False
120134

121135
for index, tokeninfo in enumerate(tokens):
122136
new_tokeninfo = tokeninfo
123137

124138
if _utils.is_docstring(new_tokeninfo, tokens[index - 1]):
125-
for _, formatter in self.enabled_formatters.items():
126-
new_tokeninfo = formatter.treat_token(new_tokeninfo)
127-
formatted_tokens.append(new_tokeninfo)
139+
new_tokeninfo, changers = self.apply_formatters(new_tokeninfo)
140+
is_changed = is_changed or bool(changers)
141+
142+
# Run formatters again (3rd time) to check if the result is stable
143+
_, changers = self._apply_formatters_once(
144+
new_tokeninfo,
145+
)
146+
147+
if changers:
148+
conflicting_formatters = {
149+
k: v
150+
for k, v in self.enabled_formatters.items()
151+
if k in changers
152+
}
153+
template = _utils.create_gh_issue_template(
154+
new_tokeninfo, conflicting_formatters, str(filename)
155+
)
128156

129-
if tokeninfo != new_tokeninfo:
130-
is_changed = True
157+
raise UnstableResultError(template)
158+
159+
formatted_tokens.append(new_tokeninfo)
131160

132161
return formatted_tokens, is_changed
133162

163+
def apply_formatters(
164+
self, token: tokenize.TokenInfo
165+
) -> tuple[tokenize.TokenInfo, set[str]]:
166+
"""Apply the formatters twice to a token.
167+
168+
Also tracks which formatters changed the token.
169+
170+
Returns:
171+
A tuple containing:
172+
[1] the formatted token and
173+
[2] a set of formatters that changed the token.
174+
"""
175+
token, changers = self._apply_formatters_once(token)
176+
if changers:
177+
token, changers2 = self._apply_formatters_once(token)
178+
changers.update(changers2)
179+
return token, changers
180+
181+
def _apply_formatters_once(
182+
self, token: tokenize.TokenInfo
183+
) -> tuple[tokenize.TokenInfo, set[str]]:
184+
"""Applies formatters to a token and keeps track of what changes it.
185+
186+
token: Token to apply formatters to
187+
188+
Returns:
189+
A tuple containing [1] the formatted token and [2] a set
190+
of formatters that changed the token.
191+
"""
192+
changers: set[str] = set()
193+
for formatter_name, formatter in self.enabled_formatters.items():
194+
if (new_token := formatter.treat_token(token)) != token:
195+
changers.add(formatter_name)
196+
token = new_token
197+
198+
return token, changers
199+
134200
def format_files(self, filepaths: list[Path]) -> bool:
135201
"""Format a list of files."""
136-
is_changed = False
137-
138-
for file in filepaths:
139-
is_changed = self.format_file(file) or is_changed
140-
141-
return is_changed
202+
is_changed = [self.format_file(file) for file in filepaths]
203+
return any(is_changed)

tests/data/format/linewrap_summary/function_docstrings.py.out

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
def func():
2-
"""A very long summary line that needs to be wrapped, A very long summary line that
2+
"""A very long summary line that needs to be wrapped, A very long summary line that.
3+
34
needs to be wrapped.
45

56
A description that is not too long.
67
"""
78

89

910
def func():
10-
"""A very long multi-line summary line that needs to be wrapped, A very long multi-
11+
"""A very long multi-line summary line that needs to be wrapped, A very long multi-.
12+
1113
line summary line that needs to be wrapped.
1214

1315
A very long summary line that needs to be wrapped.
@@ -28,13 +30,15 @@ def func():
2830
# Since the ending quotes will be appended on the same line this
2931
# exceeds the max length.
3032
def func():
31-
"""A multi-line summary that can be on one line, Something that is just too
33+
"""A multi-line summary that can be on one line, Something that is just too.
34+
3235
longgg.
3336
"""
3437

3538

3639
def func():
37-
"""A multi-line summary that can be on one line, Something that is just too
40+
"""A multi-line summary that can be on one line, Something that is just too.
41+
3842
long.
3943
"""
4044

@@ -46,6 +50,7 @@ def func():
4650
# Regression for bug found in pylint
4751
# We should re-add the quotes to line length if they will never be on the first line.
4852
class LinesChunk:
49-
"""The LinesChunk object computes and stores the hash of some consecutive stripped
53+
"""The LinesChunk object computes and stores the hash of some consecutive stripped.
54+
5055
lines of a lineset.
5156
"""

tests/test_conflicting_formatters.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Generator
4+
from contextlib import contextmanager
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from pydocstringformatter import _formatting
10+
from pydocstringformatter import _testutils as test_utils
11+
from pydocstringformatter._formatting import Formatter
12+
from pydocstringformatter._utils import UnstableResultError
13+
from pydocstringformatter.run import _Run
14+
15+
16+
@contextmanager
17+
def patched_run(formatters: list[Formatter]) -> Generator[type[_Run], None, None]:
18+
"""Patches formatters so Run only uses the provided formatters."""
19+
old_formatters = _formatting.FORMATTERS
20+
_formatting.FORMATTERS = formatters
21+
yield _Run
22+
_formatting.FORMATTERS = old_formatters
23+
24+
25+
@pytest.mark.parametrize(
26+
"formatters,expected_errors",
27+
[
28+
(
29+
[test_utils.MakeAFormatter(), test_utils.MakeBFormatter()],
30+
["Conflicting formatters"],
31+
),
32+
(
33+
[test_utils.MakeBFormatter(), test_utils.AddBFormatter()],
34+
["not able to make stable changes"],
35+
),
36+
(
37+
[
38+
test_utils.MakeBFormatter(),
39+
test_utils.AddBFormatter(),
40+
test_utils.MakeAFormatter(),
41+
],
42+
["Conflicting formatters:", "Diff too intricate to compute"],
43+
),
44+
],
45+
)
46+
def test_conflicting_formatters(
47+
formatters: list[Formatter],
48+
expected_errors: list[str],
49+
tmp_path: Path,
50+
) -> None:
51+
"""Tests that conflicting formatters raise an error."""
52+
tmp_file = tmp_path / "test.py"
53+
with open(tmp_file, "w", encoding="utf-8") as f:
54+
content = [
55+
'"""AAA AA AAA"""',
56+
]
57+
f.writelines(content)
58+
59+
with patched_run(formatters) as run:
60+
with pytest.raises(UnstableResultError) as err:
61+
run([str(tmp_file)])
62+
63+
for expect_err in expected_errors:
64+
assert expect_err in str(err.value), str(err.value)

0 commit comments

Comments
 (0)