Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment with getting reformatted chunks from Black #221

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/darker/black_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from black import (
TargetVersion,
find_pyproject_toml,
format_str,
parse_pyproject_toml,
re_compile_maybe_verbose,
)
Expand All @@ -54,6 +53,7 @@
from black.files import gen_python_files
from black.report import Report

from darker.linewise_black import format_str_to_chunks
from darker.utils import TextDocument

if sys.version_info >= (3, 8):
Expand Down Expand Up @@ -187,7 +187,8 @@ def run_black(src_contents: TextDocument, black_config: BlackConfig) -> TextDocu
# https://github.com/psf/black/pull/2484 lands in Black.
contents_for_black = src_contents.string_with_newline("\n")
if contents_for_black.strip():
dst_contents = format_str(contents_for_black, mode=Mode(**mode))
dst_chunks = format_str_to_chunks(contents_for_black, mode=Mode(**mode))
dst_contents = "".join(line for chunk in dst_chunks for line in chunk)
else:
dst_contents = "\n" if "\n" in src_contents.string else ""
return TextDocument.from_str(
Expand Down
63 changes: 63 additions & 0 deletions src/darker/linewise_black.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Re-implementation of :func:`black.format_str` as a line generator"""

from typing import Generator, List

from black import decode_bytes, detect_target_versions, get_future_imports
# `FileMode as Mode` required to satisfy mypy==0.782. Strange.
from black import FileMode as Mode
from black.comments import normalize_fmt_off
from black.linegen import LineGenerator, transform_line
from black.lines import EmptyLineTracker, Line
from black.mode import Feature, supports_feature
from black.parsing import lib2to3_parse


def format_str_to_chunks( # pylint: disable=too-many-locals
src_contents: str, *, mode: Mode
) -> Generator[List[str], None, None]:
"""Reformat a string and yield each line of new contents

This is a re-implementation of :func:`black.format_str` modified to be a generator
which yields each resulting chunk as a list of lines instead of concatenating them
into a single string.

"""
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
future_imports = get_future_imports(src_node)
versions = mode.target_versions or detect_target_versions(src_node)
normalize_fmt_off(src_node)
lines = LineGenerator(
mode=mode,
remove_u_prefix="unicode_literals" in future_imports
or supports_feature(versions, Feature.UNICODE_LITERALS),
)
elt = EmptyLineTracker(is_pyi=mode.is_pyi)
empty_line = str(Line(mode=mode))
empty_line_len = len(empty_line)
after = 0
split_line_features = {
feature
for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
if supports_feature(versions, feature)
}
num_chars = 0
for current_line in lines.visit(src_node):
if after:
yield after * [empty_line]
num_chars += after * empty_line_len
before, after = elt.maybe_empty_lines(current_line)
if before:
yield before * [empty_line]
num_chars += before * empty_line_len
lines = [
str(line)
for line in transform_line(
current_line, mode=mode, features=split_line_features
)
]
yield lines
num_chars += sum(len(line) for line in lines)
if not num_chars:
normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8"))
if "\n" in normalized_content:
yield [newline]
6 changes: 3 additions & 3 deletions src/darker/tests/test_black_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,12 @@ def test_run_black(encoding, newline):
def test_run_black_always_uses_unix_newlines(newline):
"""Content is always passed to Black with Unix newlines"""
src = TextDocument.from_str(f"print ( 'touché' ){newline}")
with patch.object(black_diff, "format_str") as format_str:
format_str.return_value = 'print("touché")\n'
with patch.object(black_diff, "format_str_to_chunks") as format_str_to_chunks:
format_str_to_chunks.return_value = [['print("touché")\n']]

_ = run_black(src, BlackConfig())

format_str.assert_called_once_with("print ( 'touché' )\n", mode=ANY)
format_str_to_chunks.assert_called_once_with("print ( 'touché' )\n", mode=ANY)


def test_run_black_ignores_excludes():
Expand Down
12 changes: 8 additions & 4 deletions src/darker/tests/test_command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,8 +582,10 @@ def test_black_options_skip_string_normalization(git_repo, config, options, expe
added_files["main.py"].write_bytes(b"bar")
mode_class_mock = Mock(wraps=black_diff.Mode)
# Speed up tests by mocking `format_str` to skip running Black
format_str = Mock(return_value="bar")
with patch.multiple(black_diff, Mode=mode_class_mock, format_str=format_str):
format_str_to_chunks = Mock(return_value=[["bar"]])
with patch.multiple(
black_diff, Mode=mode_class_mock, format_str_to_chunks=format_str_to_chunks
):

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

Expand All @@ -608,8 +610,10 @@ def test_black_options_skip_magic_trailing_comma(git_repo, config, options, expe
added_files["main.py"].write_bytes(b"a = [1, 2,]")
mode_class_mock = Mock(wraps=black_diff.Mode)
# Speed up tests by mocking `format_str` to skip running Black
format_str = Mock(return_value="a = [1, 2,]")
with patch.multiple(black_diff, Mode=mode_class_mock, format_str=format_str):
format_str_to_chunks = Mock(return_value=[["a = [1, 2,]"]])
with patch.multiple(
black_diff, Mode=mode_class_mock, format_str_to_chunks=format_str_to_chunks
):

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

Expand Down