diff --git a/src/darker/black_diff.py b/src/darker/black_diff.py index 4db6bd0a5..ec559f178 100644 --- a/src/darker/black_diff.py +++ b/src/darker/black_diff.py @@ -43,7 +43,6 @@ from black import ( TargetVersion, find_pyproject_toml, - format_str, parse_pyproject_toml, re_compile_maybe_verbose, ) @@ -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): @@ -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( diff --git a/src/darker/linewise_black.py b/src/darker/linewise_black.py new file mode 100644 index 000000000..df35e24f9 --- /dev/null +++ b/src/darker/linewise_black.py @@ -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] diff --git a/src/darker/tests/test_black_diff.py b/src/darker/tests/test_black_diff.py index dac35f256..9246cda5a 100644 --- a/src/darker/tests/test_black_diff.py +++ b/src/darker/tests/test_black_diff.py @@ -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(): diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 21aad60e9..8dcc144cb 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -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()]) @@ -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()])