Skip to content

Commit 74a3544

Browse files
committed
Reimplement black.format_str as a line generator
This is an experiment. Still concatenates the lines afterwards without making any use of the extra information we could get.
1 parent 26325c1 commit 74a3544

File tree

4 files changed

+75
-9
lines changed

4 files changed

+75
-9
lines changed

src/darker/black_diff.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@
4343
from black import (
4444
TargetVersion,
4545
find_pyproject_toml,
46-
format_str,
4746
parse_pyproject_toml,
4847
re_compile_maybe_verbose,
4948
)
5049
from black.const import DEFAULT_EXCLUDES, DEFAULT_INCLUDES
5150
from black.files import gen_python_files
5251
from black.report import Report
52+
from darker.linewise_black import format_str_to_lines
5353

5454
from darker.utils import TextDocument
5555

@@ -181,7 +181,8 @@ def run_black(src_contents: TextDocument, black_config: BlackConfig) -> TextDocu
181181
# https://github.com/psf/black/pull/2484 lands in Black.
182182
contents_for_black = src_contents.string_with_newline("\n")
183183
if contents_for_black.strip():
184-
dst_contents = format_str(contents_for_black, mode=Mode(**mode))
184+
dst_lines = format_str_to_lines(contents_for_black, mode=Mode(**mode))
185+
dst_contents = "".join(dst_lines)
185186
else:
186187
dst_contents = "\n" if "\n" in src_contents.string else ""
187188
return TextDocument.from_str(

src/darker/linewise_black.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Re-implementation of :func:`black.format_str` as a line generator"""
2+
3+
from typing import Generator
4+
from black import get_future_imports, detect_target_versions, decode_bytes
5+
from black.lines import Line, EmptyLineTracker
6+
from black.linegen import transform_line, LineGenerator
7+
from black.comments import normalize_fmt_off
8+
from black.mode import Mode
9+
from black.mode import Feature, supports_feature
10+
from black.parsing import lib2to3_parse
11+
12+
13+
def format_str_to_lines(
14+
src_contents: str, *, mode: Mode
15+
) -> Generator[str, None, None]: # pylint: disable=too-many-locals
16+
"""Reformat a string and yield each line of new contents
17+
18+
This is a re-implementation of :func:`black.format_str` modified to be a generator
19+
which yields each resulting line instead of concatenating them into a single string.
20+
21+
"""
22+
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
23+
future_imports = get_future_imports(src_node)
24+
if mode.target_versions:
25+
versions = mode.target_versions
26+
else:
27+
versions = detect_target_versions(src_node)
28+
normalize_fmt_off(src_node)
29+
lines = LineGenerator(
30+
mode=mode,
31+
remove_u_prefix="unicode_literals" in future_imports
32+
or supports_feature(versions, Feature.UNICODE_LITERALS),
33+
)
34+
elt = EmptyLineTracker(is_pyi=mode.is_pyi)
35+
empty_line = str(Line(mode=mode))
36+
empty_line_len = len(empty_line)
37+
after = 0
38+
split_line_features = {
39+
feature
40+
for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
41+
if supports_feature(versions, feature)
42+
}
43+
num_chars = 0
44+
for current_line in lines.visit(src_node):
45+
for _ in range(after):
46+
yield empty_line
47+
num_chars += after * empty_line_len
48+
before, after = elt.maybe_empty_lines(current_line)
49+
for _ in range(before):
50+
yield empty_line
51+
num_chars += before * empty_line_len
52+
for line in transform_line(
53+
current_line, mode=mode, features=split_line_features
54+
):
55+
line_str = str(line)
56+
yield line_str
57+
num_chars += len(line_str)
58+
if not num_chars:
59+
normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8"))
60+
if "\n" in normalized_content:
61+
yield newline

src/darker/tests/test_black_diff.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,12 @@ def test_run_black(encoding, newline):
174174
def test_run_black_always_uses_unix_newlines(newline):
175175
"""Content is always passed to Black with Unix newlines"""
176176
src = TextDocument.from_str(f"print ( 'touché' ){newline}")
177-
with patch.object(black_diff, "format_str") as format_str:
178-
format_str.return_value = 'print("touché")\n'
177+
with patch.object(black_diff, "format_str_to_lines") as format_str_to_lines:
178+
format_str_to_lines.return_value = ['print("touché")\n']
179179

180180
_ = run_black(src, BlackConfig())
181181

182-
format_str.assert_called_once_with("print ( 'touché' )\n", mode=ANY)
182+
format_str_to_lines.assert_called_once_with("print ( 'touché' )\n", mode=ANY)
183183

184184

185185
def test_run_black_ignores_excludes():

src/darker/tests/test_command_line.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,10 @@ def test_black_options_skip_string_normalization(git_repo, config, options, expe
470470
added_files["main.py"].write_bytes(b"bar")
471471
mode_class_mock = Mock(wraps=black_diff.Mode)
472472
# Speed up tests by mocking `format_str` to skip running Black
473-
format_str = Mock(return_value="bar")
474-
with patch.multiple(black_diff, Mode=mode_class_mock, format_str=format_str):
473+
format_str_to_lines = Mock(return_value=["bar"])
474+
with patch.multiple(
475+
black_diff, Mode=mode_class_mock, format_str_to_lines=format_str_to_lines
476+
):
475477

476478
main(options + [str(path) for path in added_files.values()])
477479

@@ -496,8 +498,10 @@ def test_black_options_skip_magic_trailing_comma(git_repo, config, options, expe
496498
added_files["main.py"].write_bytes(b"a = [1, 2,]")
497499
mode_class_mock = Mock(wraps=black_diff.Mode)
498500
# Speed up tests by mocking `format_str` to skip running Black
499-
format_str = Mock(return_value="a = [1, 2,]")
500-
with patch.multiple(black_diff, Mode=mode_class_mock, format_str=format_str):
501+
format_str_to_lines = Mock(return_value=["a = [1, 2,]"])
502+
with patch.multiple(
503+
black_diff, Mode=mode_class_mock, format_str_to_lines=format_str_to_lines
504+
):
501505

502506
main(options + [str(path) for path in added_files.values()])
503507

0 commit comments

Comments
 (0)