From 34874c21572f3f30438fc7d5784ed435ed370978 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 12 Jun 2020 21:21:39 -0700 Subject: [PATCH 01/18] Add a --diff option to match black behavior. This allows for example to use darker as a formatter for vscode with minimal changes as vscode sends the --quiet and --diff flags to the formatter. "python.formatting.provider": "black", "python.formatting.blackPath": "/path/to/darker" --- src/darker/__main__.py | 23 ++++++++++++++++++++--- src/darker/command_line.py | 3 +++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index 670c7ba46..8dff34998 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -20,7 +20,7 @@ MAX_CONTEXT_LINES = 1000 -def format_edited_parts(srcs: Iterable[Path], isort: bool) -> None: +def format_edited_parts(srcs: Iterable[Path], isort: bool, print_diff=False) -> None: """Black (and optional isort) formatting for chunks with edits since the last commit 1. run isort on each edited file @@ -107,7 +107,24 @@ def format_edited_parts(srcs: Iterable[Path], isort: bool) -> None: # 10. A re-formatted Python file which produces an identical AST was # created successfully - write an updated file logger.info("Writing %s bytes into %s", len(result_str), src) - src.write_text(result_str) + if print_diff: + from difflib import unified_diff + + difflines = list( + unified_diff( + open(src).read().splitlines(), + result_str.splitlines(), + src.as_posix(), + src.as_posix(), + ) + ) + if len(difflines) > 2: + h1, h2, *rest = list(difflines) + print(h1, end="") + print(h2, end="") + print("\n".join(rest)) + else: + src.write_text(result_str) if not remaining_srcs: break @@ -134,7 +151,7 @@ def main(argv: List[str] = None) -> None: exit(1) paths = {Path(p) for p in args.src} - format_edited_parts(paths, args.isort) + format_edited_parts(paths, args.isort, args.diff) if __name__ == "__main__": diff --git a/src/darker/command_line.py b/src/darker/command_line.py index ce89003ba..6313eda01 100644 --- a/src/darker/command_line.py +++ b/src/darker/command_line.py @@ -27,6 +27,9 @@ def parse_command_line(argv: List[str]) -> Namespace: isort_help = ["Also sort imports using the `isort` package"] if not isort: isort_help.append(f". {ISORT_INSTRUCTION} to enable usage of this option.") + parser.add_argument( + "--diff", action="store_true", help="print diff instead of modifying the file", + ) parser.add_argument( "-i", "--isort", action="store_true", help="".join(isort_help), ) From 849fd3cc1c2ce056b78718f0f2e8f4eb3c2b7e89 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Mon, 29 Jun 2020 22:49:29 +0300 Subject: [PATCH 02/18] No print_diff default for format_edited_parts() Also add type hint for `print_diff` parameter. --- src/darker/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index 8dff34998..9bbaba098 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -20,7 +20,7 @@ MAX_CONTEXT_LINES = 1000 -def format_edited_parts(srcs: Iterable[Path], isort: bool, print_diff=False) -> None: +def format_edited_parts(srcs: Iterable[Path], isort: bool, print_diff: bool) -> None: """Black (and optional isort) formatting for chunks with edits since the last commit 1. run isort on each edited file From 98f4333a769da7df14386968712899e6aa145906 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:30:09 +0300 Subject: [PATCH 03/18] Add print_diff argument to docstring --- src/darker/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index 80457d2b5..10f3756d2 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -46,6 +46,8 @@ def format_edited_parts( :param srcs: Directories and files to re-format :param isort: ``True`` to also run ``isort`` first on each changed file :param black_args: Command-line arguments to send to ``black.FileMode`` + :param print_diff: ``True`` to output diffs instead of modifying source files + """ remaining_srcs: Set[Path] = set(srcs) git_root = get_common_root(srcs) From dfac9ea7d6c7add3ad2866336095a753b57627b7 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:30:24 +0300 Subject: [PATCH 04/18] `--isort --print-diff` not implemented --- src/darker/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index 10f3756d2..d40490c91 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -54,6 +54,8 @@ def format_edited_parts( # 1. run isort if isort: + if print_diff: + raise NotImplementedError('--isort is not supported with --print-diff') changed_files = git_diff_name_only(remaining_srcs, git_root) apply_isort(changed_files) From 9ad4015465814430c7033113e28917b5d4f78e13 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:31:00 +0300 Subject: [PATCH 05/18] Make isort quiet Without the quite flag it prints out a "Fixing" line. Suppressing that will make `--diff` tests cleaner. --- src/darker/import_sorting.py | 3 ++- src/darker/tests/test_main.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/darker/import_sorting.py b/src/darker/import_sorting.py index 4565da978..057171db4 100644 --- a/src/darker/import_sorting.py +++ b/src/darker/import_sorting.py @@ -15,7 +15,7 @@ def apply_isort(srcs: List[Path]) -> None: logger.debug( f"SortImports({str(src)!r}, multi_line_output=3, " f"include_trailing_comma=True, force_grid_wrap=0, use_parentheses=True," - f" line_length=88)" + f" line_length=88, quiet=True)" ) _ = SortImports( str(src), @@ -24,4 +24,5 @@ def apply_isort(srcs: List[Path]) -> None: force_grid_wrap=0, use_parentheses=True, line_length=88, + quiet=True, ) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index 50cac7607..e65bd2565 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -49,4 +49,5 @@ def test_isort_option_with_isort_calls_sortimports(run_isort): line_length=88, multi_line_output=3, use_parentheses=True, + quiet=True, ) From f572ba3c8f38c22c53b2a7980b7b5fc0f9d1c433 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:31:50 +0300 Subject: [PATCH 06/18] Add tests for the `--diff` option Also adding tests for other options as well. --- src/darker/tests/test_command_line.py | 29 +++++++++ src/darker/tests/test_main.py | 84 ++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/darker/tests/test_command_line.py b/src/darker/tests/test_command_line.py index 74747f18e..fd13a2c35 100644 --- a/src/darker/tests/test_command_line.py +++ b/src/darker/tests/test_command_line.py @@ -1,4 +1,5 @@ import re +from pathlib import Path from textwrap import dedent from unittest.mock import call, patch @@ -78,3 +79,31 @@ def test_black_options(monkeypatch, tmpdir, git_repo, options, expect): _, expect_args, expect_kwargs = expect FileMode.assert_called_once_with(*expect_args, **expect_kwargs) + + +@pytest.mark.parametrize( + 'options, expect', + [ + (['a.py'], ({Path('a.py')}, False, {}, False)), + (['--isort', 'a.py'], ({Path('a.py')}, True, {}, False)), + ( + ['--config', 'my.cfg', 'a.py'], + ({Path('a.py')}, False, {'config': 'my.cfg'}, False), + ), + ( + ['--line-length', '90', 'a.py'], + ({Path('a.py')}, False, {'line_length': 90}, False), + ), + ( + ['--skip-string-normalization', 'a.py'], + ({Path('a.py')}, False, {'skip_string_normalization': True}, False), + ), + (['--diff', 'a.py'], ({Path('a.py')}, False, {}, True)), + ], +) +def test_options(options, expect): + with patch('darker.__main__.format_edited_parts') as format_edited_parts: + + main(options) + + format_edited_parts.assert_called_once_with(*expect) diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index e65bd2565..828323793 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -7,7 +7,6 @@ import darker.__main__ import darker.import_sorting -from darker.tests.git_diff_example_output import CHANGE_SECOND_LINE def test_isort_option_without_isort(tmpdir, without_isort, caplog): @@ -51,3 +50,86 @@ def test_isort_option_with_isort_calls_sortimports(run_isort): use_parentheses=True, quiet=True, ) + + +def test_format_edited_parts_empty(): + with pytest.raises(ValueError): + + darker.__main__.format_edited_parts([], False, {}, True) + + +def test_format_edited_parts_isort_print_diff(): + with pytest.raises(NotImplementedError): + + darker.__main__.format_edited_parts([Path('test.py')], True, {}, True) + + +A_PY = ['import sys', 'import os', "print( '42')", ''] +A_PY_BLACK = ['import sys', 'import os', '', 'print("42")', ''] +A_PY_BLACK_ISORT = ['import os', 'import sys', '', 'print("42")', ''] + +A_PY_DIFF_BLACK = [ + '--- /a.py', + '+++ /a.py', + '@@ -1,3 +1,4 @@', + '', + ' import sys', + ' import os', + "-print( '42')", + '+', + '+print("42")', + '', +] + +A_PY_DIFF_BLACK_NO_STR_NORMALIZE = [ + '--- /a.py', + '+++ /a.py', + '@@ -1,3 +1,4 @@', + '', + ' import sys', + ' import os', + "-print( '42')", + '+', + "+print('42')", + '', +] + + +@pytest.mark.parametrize( + 'srcs, isort, black_args, print_diff, expect_stdout, expect_a_py', + [ + (['a.py'], False, {}, True, A_PY_DIFF_BLACK, A_PY), + (['a.py'], True, {}, False, [''], A_PY_BLACK_ISORT), + ( + ['a.py'], + False, + {'skip_string_normalization': True}, + True, + A_PY_DIFF_BLACK_NO_STR_NORMALIZE, + A_PY, + ), + (['a.py'], False, {}, False, [''], A_PY_BLACK), + ], +) +def test_format_edited_parts( + git_repo, + monkeypatch, + capsys, + srcs, + isort, + black_args, + print_diff, + expect_stdout, + expect_a_py, +): + monkeypatch.chdir(git_repo.root) + paths = git_repo.add({'a.py': '\n', 'b.py': '\n'}, commit='Initial commit') + paths['a.py'].write('\n'.join(A_PY)) + paths['b.py'].write('print(42 )\n') + darker.__main__.format_edited_parts( + [Path(src) for src in srcs], isort, black_args, print_diff + ) + stdout = capsys.readouterr().out.replace(str(git_repo.root), '') + assert stdout.split('\n') == expect_stdout + assert paths['a.py'].readlines(cr=False) == expect_a_py + assert paths['b.py'].readlines(cr=False) == ['print(42 )', ''] From 95bd9c935308c19b9c01638b2c3e8356ee9c03e3 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:35:57 +0300 Subject: [PATCH 07/18] Skip string normalization for Darker code We may later choose to enable it again, but for now let's keep apostrophes intact in literal strings. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 019b0d848..dcf704160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ [build-system] # Minimum requirements for the build system to execute. requires = ["setuptools", "wheel"] # PEP 508 specifications. + +[tool.black] +skip-string-normalization = true From e4c758a0137111b49ae1af1dfa9fae58dfaf6235 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:39:31 +0300 Subject: [PATCH 08/18] No need to re-read original file in --diff --- src/darker/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index d40490c91..c99508a86 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -121,7 +121,7 @@ def format_edited_parts( difflines = list( unified_diff( - open(src).read().splitlines(), + edited, result_str.splitlines(), src.as_posix(), src.as_posix(), From eada5879b384f4f307049b51f42908a2a2303f98 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:42:00 +0300 Subject: [PATCH 09/18] No need to re-split new source code in `--diff` The lines are already in `chosen_lines`. --- src/darker/__main__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index c99508a86..e87232d10 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -121,10 +121,7 @@ def format_edited_parts( difflines = list( unified_diff( - edited, - result_str.splitlines(), - src.as_posix(), - src.as_posix(), + edited, chosen_lines, src.as_posix(), src.as_posix(), ) ) if len(difflines) > 2: From 8468c9b74fa85b07508ce6e6a624d13cf3b24c94 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:43:47 +0300 Subject: [PATCH 10/18] Import `difflib` globally --- src/darker/__main__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index e87232d10..1a6fbdd43 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -2,6 +2,7 @@ import logging import sys +from difflib import unified_diff from pathlib import Path from typing import Dict, Iterable, List, Set, Union @@ -117,8 +118,6 @@ def format_edited_parts( # created successfully - write an updated file logger.info("Writing %s bytes into %s", len(result_str), src) if print_diff: - from difflib import unified_diff - difflines = list( unified_diff( edited, chosen_lines, src.as_posix(), src.as_posix(), From 8261a1c26d42428aa7bcb7df98222c66ace3171a Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:45:08 +0300 Subject: [PATCH 11/18] `difflines` is already a list --- src/darker/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index 1a6fbdd43..ccf23d773 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -124,7 +124,7 @@ def format_edited_parts( ) ) if len(difflines) > 2: - h1, h2, *rest = list(difflines) + h1, h2, *rest = difflines print(h1, end="") print(h2, end="") print("\n".join(rest)) From d391f80f8432acc168d8a08bb2c4e1eab38c56c8 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:46:55 +0300 Subject: [PATCH 12/18] `--diff` help text like in `Black --help` --- src/darker/command_line.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/darker/command_line.py b/src/darker/command_line.py index 889392f54..0c7ed1e72 100644 --- a/src/darker/command_line.py +++ b/src/darker/command_line.py @@ -28,7 +28,9 @@ def parse_command_line(argv: List[str]) -> Namespace: if not isort: isort_help.append(f". {ISORT_INSTRUCTION} to enable usage of this option.") parser.add_argument( - "--diff", action="store_true", help="print diff instead of modifying the file", + "--diff", + action="store_true", + help="Don't write the files back, just output a diff for each file on stdout", ) parser.add_argument( "-i", "--isort", action="store_true", help="".join(isort_help), From 5ceb083d493001511e4fd470e10c795ac73191cc Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:48:32 +0300 Subject: [PATCH 13/18] Change log enry for `--diff` --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e89b91346..c7f2dfaef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ - Feature: Add support for black config - Feature: Add support for ``-l``/``--line-length`` and ``-S``/``--skip-string-normalization`` +- Feature: ``--diff`` outputs a diff for each file on standard output + 0.2.0 / 2020-03-11 ------------------ From d490daeded869d099ac6325758b1071465ea9e8c Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:50:15 +0300 Subject: [PATCH 14/18] Add @Carreau to `CONTRIBUTORS.rst` --- CONTRIBUTORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 0599eea06..1c706998d 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -7,3 +7,4 @@ - Alexander Tishin (@Mystic-Mirage) - Antti Kaihola (@akaihola) - Correy Lim (@CorreyL) +- Matthias Bussonnier (@Carreau) From 65f0e492c816fe8208b610be7ae223a0a97d0a8e Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Tue, 30 Jun 2020 00:53:07 +0300 Subject: [PATCH 15/18] Add instructions for VSCode --- README.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.rst b/README.rst index ffeacbb54..d4ef07bc0 100644 --- a/README.rst +++ b/README.rst @@ -178,6 +178,30 @@ PyCharm/IntelliJ IDEA __ https://plugins.jetbrains.com/plugin/7177-file-watchers +Visual Studio Code +------------------ + +1. Install ``darker``:: + + $ pip install darker + +2. Locate your ``darker`` installation folder. + + On macOS / Linux / BSD:: + + $ which darker + /usr/local/bin/darker # possible location + + On Windows:: + + $ where darker + %LocalAppData%\Programs\Python\Python36-32\Scripts\darker.exe # possible location + +3. Add these configuration options:: + + "python.formatting.provider": "black", + "python.formatting.blackPath": "" + How does it work? ================= From 9adfc0bc55d1f69fee216215048911e4e1666647 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Wed, 1 Jul 2020 22:50:23 +0300 Subject: [PATCH 16/18] Create parent directories in Git repo fixture --- src/darker/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darker/tests/conftest.py b/src/darker/tests/conftest.py index 08acbd9f5..092967a03 100644 --- a/src/darker/tests/conftest.py +++ b/src/darker/tests/conftest.py @@ -32,7 +32,7 @@ def add( for relative_path in paths_and_contents } for relative_path, content in paths_and_contents.items(): - absolute_paths[relative_path].write(content) + absolute_paths[relative_path].write(content, ensure=True) check_call(["git", "add", relative_path], cwd=self.root) if commit: check_call(["git", "commit", "-m", commit], cwd=self.root) From 8a4e13ec9eb5bc4fb18c451845dda6486db764ce Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Thu, 2 Jul 2020 00:20:38 +0300 Subject: [PATCH 17/18] Better `isort` integration - `--isort` and `--diff` now work together - diff original unmodified and user-edited versions of files in the Git directory using Python's difflib, not by parsing `git diff` output - process each edited file individually - only run `isort` for edited files - write back `isort` modifications together with `black` modifications, and skip writing if there are errors - remove code that became unused - avoid extra conversions between source code as a string and a list of line strings - add some tests for previously untested functions --- src/darker/__main__.py | 82 ++++---- src/darker/black_diff.py | 136 +++---------- src/darker/diff.py | 138 +++++++++++++ src/darker/git.py | 48 +++++ src/darker/git_diff.py | 208 -------------------- src/darker/import_sorting.py | 35 ++-- src/darker/tests/git_diff_example_output.py | 36 ---- src/darker/tests/test_black_diff.py | 156 +-------------- src/darker/tests/test_diff.py | 147 ++++++++++++++ src/darker/tests/test_git.py | 77 ++++++++ src/darker/tests/test_git_diff.py | 162 --------------- src/darker/tests/test_import_sorting.py | 10 + src/darker/tests/test_main.py | 61 +++--- src/darker/tests/test_verification.py | 12 +- src/darker/verification.py | 3 +- 15 files changed, 564 insertions(+), 747 deletions(-) create mode 100644 src/darker/diff.py create mode 100644 src/darker/git.py delete mode 100644 src/darker/git_diff.py create mode 100644 src/darker/tests/test_diff.py create mode 100644 src/darker/tests/test_git.py delete mode 100644 src/darker/tests/test_git_diff.py create mode 100644 src/darker/tests/test_import_sorting.py diff --git a/src/darker/__main__.py b/src/darker/__main__.py index a351f2aa0..e1aa512a7 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -4,17 +4,17 @@ import sys from difflib import unified_diff from pathlib import Path -from typing import Dict, Iterable, List, Set, Union +from typing import Dict, Iterable, List, Union -from darker.black_diff import diff_and_get_opcodes, opcodes_to_chunks, run_black +from darker.black_diff import run_black from darker.chooser import choose_lines from darker.command_line import ISORT_INSTRUCTION, parse_command_line -from darker.git_diff import ( - GitDiffParseError, - get_edit_linenums, - git_diff, - git_diff_name_only, +from darker.diff import ( + diff_and_get_opcodes, + opcodes_to_chunks, + opcodes_to_edit_linenums, ) +from darker.git import git_diff_name_only, git_get_unmodified_content from darker.import_sorting import SortImports, apply_isort from darker.utils import get_common_root, joinlines from darker.verification import NotEquivalentError, verify_ast_unchanged @@ -35,7 +35,7 @@ def format_edited_parts( """Black (and optional isort) formatting for chunks with edits since the last commit 1. run isort on each edited file - 2. do a ``git diff -U0 ...`` for all file & dir paths on the command line + 2. diff HEAD and worktree for all file & dir paths on the command line 3. extract line numbers in each edited to-file for changed lines 4. run black on the contents of each edited to-file 5. get a diff between the edited to-file and the reformatted content @@ -55,32 +55,43 @@ def format_edited_parts( :param print_diff: ``True`` to output diffs instead of modifying source files """ - remaining_srcs: Set[Path] = set(srcs) git_root = get_common_root(srcs) + changed_files = git_diff_name_only(srcs, git_root) + head_srcs = { + src: git_get_unmodified_content(src, git_root) for src in changed_files + } + worktree_srcs = {src: (git_root / src).read_text() for src in changed_files} # 1. run isort if isort: - if print_diff: - raise NotImplementedError('--isort is not supported with --print-diff') - changed_files = git_diff_name_only(remaining_srcs, git_root) - apply_isort(changed_files) - - for context_lines in range(MAX_CONTEXT_LINES + 1): - - # 2. do the git diff - logger.debug("Looking at %s", ", ".join(str(s) for s in remaining_srcs)) - logger.debug("Git root: %s", git_root) - git_diff_result = git_diff(remaining_srcs, git_root, context_lines) - - # 3. extract changed line numbers for each to-file - remaining_srcs = set() - for src_relative, edited_linenums in get_edit_linenums(git_diff_result): + edited_srcs = { + src: apply_isort(edited_content) + for src, edited_content in worktree_srcs.items() + } + else: + edited_srcs = worktree_srcs + + for src_relative, edited_content in edited_srcs.items(): + for context_lines in range(MAX_CONTEXT_LINES + 1): src = git_root / src_relative - if not edited_linenums: - continue + edited = edited_content.splitlines() + head_lines = head_srcs[src_relative] + + # 2. diff HEAD and worktree for all file & dir paths on the command line + edited_opcodes = diff_and_get_opcodes(head_lines, edited) + + # 3. extract line numbers in each edited to-file for changed lines + edited_linenums = list(opcodes_to_edit_linenums(edited_opcodes)) + if ( + isort + and not edited_linenums + and edited_content == worktree_srcs[src_relative] + ): + logger.debug("No changes in %s after isort", src) + break # 4. run black - edited, formatted = run_black(src, black_args) + formatted = run_black(src, edited_content, black_args) logger.debug("Read %s lines from edited file %s", len(edited), src) logger.debug("Black reformat resulted in %s lines", len(formatted)) @@ -104,7 +115,9 @@ def format_edited_parts( len(chosen_lines), ) try: - verify_ast_unchanged(edited, result_str, black_chunks, edited_linenums) + verify_ast_unchanged( + edited_content, result_str, black_chunks, edited_linenums + ) except NotEquivalentError: # Diff produced misaligned chunks which couldn't be reconstructed into # a partially re-formatted Python file which produces an identical AST. @@ -117,15 +130,18 @@ def format_edited_parts( "Trying again with %s lines of context for `git diff -U`", context_lines + 1, ) - remaining_srcs.add(src) + continue else: # 10. A re-formatted Python file which produces an identical AST was # created successfully - write an updated file - logger.info("Writing %s bytes into %s", len(result_str), src) + # or print the diff if print_diff: difflines = list( unified_diff( - edited, chosen_lines, src.as_posix(), src.as_posix(), + worktree_srcs[src_relative].splitlines(), + chosen_lines, + src.as_posix(), + src.as_posix(), ) ) if len(difflines) > 2: @@ -134,9 +150,9 @@ def format_edited_parts( print(h2, end="") print("\n".join(rest)) else: + logger.info("Writing %s bytes into %s", len(result_str), src) src.write_text(result_str) - if not remaining_srcs: - break + break def main(argv: List[str] = None) -> None: diff --git a/src/darker/black_diff.py b/src/darker/black_diff.py index 2fc15200a..35aed2a88 100644 --- a/src/darker/black_diff.py +++ b/src/darker/black_diff.py @@ -1,82 +1,41 @@ -"""Turn Python code into chunks of original and re-formatted code - -The functions in this module implement three steps -for converting a file with Python source code into a list of chunks. -From these chunks, the same file can be reconstructed -while choosing whether each chunk should be taken from the original untouched file -or from the version reformatted with Black. +"""Re-format Python source code using Black In examples below, a simple two-line snippet is used. The first line will be reformatted by Black, and the second left intact:: >>> from unittest.mock import Mock >>> src = Mock() - >>> src.read_text.return_value = '''\\ + >>> src_content = '''\\ ... for i in range(5): print(i) ... print("done") ... ''' First, :func:`run_black` uses Black to reformat the contents of a given file. -Original and reformatted lines are returned e.g.:: +Reformatted lines are returned e.g.:: - >>> src_lines, dst_lines = run_black(src) - >>> src_lines - ['for i in range(5): print(i)', - 'print("done")'] + >>> dst_lines = run_black(src, src_content, black_args={}) >>> dst_lines ['for i in range(5):', ' print(i)', 'print("done")'] -The output of :func:`run_black` should then be fed into :func:`diff_and_get_opcodes`. -It divides a diff between the original and reformatted content -into alternating chunks of -intact (represented by the 'equal' tag) and -modified ('delete', 'replace' or 'insert' tag) lines. -Each chunk is an opcode represented by the tag and the corresponding 0-based line ranges -in the original and reformatted content, e.g.:: - - >>> opcodes = diff_and_get_opcodes(src_lines, dst_lines) - >>> len(opcodes) - 2 - >>> opcodes[0] # split 'for' loop into two lines - ('replace', 0, 1, 0, 2) - >>> opcodes[1] # keep 'print("done")' as such - ('equal', 1, 2, 2, 3) - -Finally, :func:`opcodes_to_chunks` picks the lines -from original and reformatted content for each opcode. -It combines line content with the 1-based line offset in the original content, e.g.:: - - >>> chunks = list(opcodes_to_chunks(opcodes, src_lines, dst_lines)) - >>> len(chunks) - 2 - >>> chunks[0] # (, , ) - (1, - ['for i in range(5): print(i)'], - ['for i in range(5):', - ' print(i)']) - >>> chunks[1] - (2, - ['print("done")'], - ['print("done")']) - -By concatenating the second items in these tuples, i.e. original lines, -the original file can be reconstructed. - -By concatenating the third items, i.e. reformatted lines, -the complete output from Black can be reconstructed. - -By concatenating and choosing either the second or third item, -a mixed result with only selected regions reformatted can be reconstructed. +See :mod:`darker.diff` and :mod:`darker.chooser` +for how this result is further processed with: + +- :func:`~darker.diff.diff_and_get_opcodes` + to get a diff of the reformatting +- :func:`~darker.diff.opcodes_to_chunks` + to split the diff into chunks of original and reformatted content +- :func:`~darker.chooser.choose_lines` + to reconstruct the source code from original and reformatted chunks + based on whether reformats touch user-edited lines """ import logging -from difflib import SequenceMatcher from functools import lru_cache from pathlib import Path -from typing import Dict, Generator, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union from black import FileMode, format_str, read_pyproject_toml from click import Command, Context, Option @@ -104,12 +63,14 @@ def read_black_config(src: Path, value: Optional[str]) -> Dict[str, Union[bool, def run_black( - src: Path, black_args: Dict[str, Union[bool, int]] -) -> Tuple[List[str], List[str]]: - """Run the black formatter for the contents of the given Python file + src: Path, src_contents: str, black_args: Dict[str, Union[bool, int]] +) -> List[str]: + """Run the black formatter for the Python source code given as a string Return lines of the original file as well as the formatted content. + :param src: The originating file path for the source code + :param src_contents: The source code as a string :param black_args: Command-line arguments to send to ``black.FileMode`` """ @@ -133,59 +94,6 @@ def run_black( # from the command line arguments mode = FileMode(**effective_args) - src_contents = src.read_text() dst_contents = format_str(src_contents, mode=mode) - return src_contents.splitlines(), dst_contents.splitlines() - - -def diff_and_get_opcodes( - src_lines: List[str], dst_lines: List[str] -) -> List[Tuple[str, int, int, int, int]]: - """Return opcodes and line numbers for chunks in the diff of two lists of strings - - The opcodes are 5-tuples for each chunk with - - - the tag of the operation ('equal', 'delete', 'replace' or 'insert') - - the number of the first line in the chunk in the from-file - - the number of the last line in the chunk in the from-file - - the number of the first line in the chunk in the to-file - - the number of the last line in the chunk in the to-file - - Line numbers are zero based. - - """ - matcher = SequenceMatcher(None, src_lines, dst_lines, autojunk=False) - opcodes = matcher.get_opcodes() - logger.debug( - "Diff between edited and reformatted has %s opcode%s", - len(opcodes), - "s" if len(opcodes) > 1 else "", - ) - return opcodes - - -def opcodes_to_chunks( - opcodes: List[Tuple[str, int, int, int, int]], - src_lines: List[str], - dst_lines: List[str], -) -> Generator[Tuple[int, List[str], List[str]], None, None]: - """Convert each diff opcode to a line number and original plus modified lines - - Each chunk is a 3-tuple with - - - the 1-based number of the first line in the chunk in the from-file - - the original lines of the chunk in the from-file - - the modified lines of the chunk in the to-file - - Based on this, the patch can be constructed by choosing either original or modified - lines for each chunk and concatenating them together. - - """ - # Make sure every other opcode is an 'equal' tag - assert all( - (tag1 == "equal") != (tag2 == "equal") - for (tag1, _, _, _, _), (tag2, _, _, _, _) in zip(opcodes[:-1], opcodes[1:]) - ), opcodes - - for tag, i1, i2, j1, j2 in opcodes: - yield i1 + 1, src_lines[i1:i2], dst_lines[j1:j2] + dst_lines: List[str] = dst_contents.splitlines() + return dst_lines diff --git a/src/darker/diff.py b/src/darker/diff.py new file mode 100644 index 000000000..493f91ccf --- /dev/null +++ b/src/darker/diff.py @@ -0,0 +1,138 @@ +"""Diff text and get line numbers of changes or chunks of original and changed content + +The functions in this module implement + +- diffing text files, returning opcodes +- turning opcodes into a list of line numbers of changed lines +- turning opcodes into chunks of original and modified text + +In our case, we run a diff between original and user-edited source code. +Another diff is done between user-edited and Black-reformatted source code +as returned by :func:`black.black_diff.run_black_for_content`. + + >>> src_lines = [ + ... 'for i in range(5): print(i)', + ... 'print("done")' + ... ] + + >>> dst_lines = [ + ... 'for i in range(5):', + ... ' print(i)', + ... 'print("done")' + ... ] + +:func:`diff_and_get_opcodes`. +divides a diff between the original and reformatted content +into alternating chunks of +intact (represented by the 'equal' tag) and +modified ('delete', 'replace' or 'insert' tag) lines. +Each chunk is an opcode represented by the tag and the corresponding 0-based line ranges +in the original and reformatted content, e.g.:: + + >>> opcodes = diff_and_get_opcodes(src_lines, dst_lines) + >>> len(opcodes) + 2 + >>> opcodes[0] # split 'for' loop into two lines + ('replace', 0, 1, 0, 2) + >>> opcodes[1] # keep 'print("done")' as such + ('equal', 1, 2, 2, 3) + +:func:`opcodes_to_chunks` picks the lines +from original and reformatted content for each opcode. +It combines line content with the 1-based line offset in the original content, e.g.:: + + >>> chunks = list(opcodes_to_chunks(opcodes, src_lines, dst_lines)) + >>> len(chunks) + 2 + >>> chunks[0] # (, , ) + (1, + ['for i in range(5): print(i)'], + ['for i in range(5):', + ' print(i)']) + >>> chunks[1] + (2, + ['print("done")'], + ['print("done")']) + +By concatenating the second items in these tuples, i.e. original lines, +the original file can be reconstructed. + +By concatenating the third items, i.e. reformatted lines, +the complete output from Black can be reconstructed. + +By concatenating and choosing either the second or third item, +a mixed result with only selected regions reformatted can be reconstructed. + +""" + +import logging +from difflib import SequenceMatcher +from typing import List, Generator, Tuple + +logger = logging.getLogger(__name__) + + +def diff_and_get_opcodes( + src_lines: List[str], dst_lines: List[str] +) -> List[Tuple[str, int, int, int, int]]: + """Return opcodes and line numbers for chunks in the diff of two lists of strings + + The opcodes are 5-tuples for each chunk with + + - the tag of the operation ('equal', 'delete', 'replace' or 'insert') + - the number of the first line in the chunk in the from-file + - the number of the last line in the chunk in the from-file + - the number of the first line in the chunk in the to-file + - the number of the last line in the chunk in the to-file + + Line numbers are zero based. + + """ + matcher = SequenceMatcher(None, src_lines, dst_lines, autojunk=False) + opcodes = matcher.get_opcodes() + logger.debug( + "Diff between edited and reformatted has %s opcode%s", + len(opcodes), + "s" if len(opcodes) > 1 else "", + ) + return opcodes + + +def _validate_opcodes(opcodes: List[Tuple[str, int, int, int, int]]) -> None: + """Make sure every other opcode is an 'equal' tag""" + assert all( + (tag1 == "equal") != (tag2 == "equal") + for (tag1, _, _, _, _), (tag2, _, _, _, _) in zip(opcodes[:-1], opcodes[1:]) + ), opcodes + + +def opcodes_to_edit_linenums( + opcodes: List[Tuple[str, int, int, int, int]] +) -> Generator[int, None, None]: + """Convert diff opcode to line number of edited lines in the destination file""" + _validate_opcodes(opcodes) + for tag, _i1, _i2, j1, j2 in opcodes: + if tag != "equal": + yield from range(j1 + 1, j2 + 1) + + +def opcodes_to_chunks( + opcodes: List[Tuple[str, int, int, int, int]], + src_lines: List[str], + dst_lines: List[str], +) -> Generator[Tuple[int, List[str], List[str]], None, None]: + """Convert each diff opcode to a line number and original plus modified lines + + Each chunk is a 3-tuple with + + - the 1-based number of the first line in the chunk in the from-file + - the original lines of the chunk in the from-file + - the modified lines of the chunk in the to-file + + Based on this, the patch can be constructed by choosing either original or modified + lines for each chunk and concatenating them together. + + """ + _validate_opcodes(opcodes) + for tag, i1, i2, j1, j2 in opcodes: + yield i1 + 1, src_lines[i1:i2], dst_lines[j1:j2] diff --git a/src/darker/git.py b/src/darker/git.py new file mode 100644 index 000000000..187962d10 --- /dev/null +++ b/src/darker/git.py @@ -0,0 +1,48 @@ +"""Helpers for listing modified files and getting unmodified content from Git""" + +import logging +from pathlib import Path +from subprocess import check_output +from typing import Iterable, List, Set + +logger = logging.getLogger(__name__) + + +def git_get_unmodified_content(path: Path, cwd: Path) -> List[str]: + """Get unmodified text lines of a file at Git HEAD + + :param path: The relative path of the file in the Git repository + :param cwd: The root of the Git repository + + """ + cmd = ["git", "show", f":./{path}"] + logger.debug("[%s]$ %s", cwd, " ".join(cmd)) + return check_output(cmd, cwd=str(cwd), encoding='utf-8').splitlines() + + +def should_reformat_file(path: Path) -> bool: + return path.exists() and path.suffix == ".py" + + +def git_diff_name_only(paths: Iterable[Path], cwd: Path) -> Set[Path]: + """Run ``git diff --name-only`` and return file names from the output + + Return file names relative to the Git repository root. + + :paths: Paths to the files to diff + :cwd: The Git repository root + + """ + relative_paths = {p.resolve().relative_to(cwd) for p in paths} + cmd = [ + "git", + "diff", + "--name-only", + "--relative", + "--", + *[str(path) for path in relative_paths], + ] + logger.debug("[%s]$ %s", cwd, " ".join(cmd)) + lines = check_output(cmd, cwd=str(cwd)).decode("utf-8").splitlines() + changed_paths = (Path(line) for line in lines) + return {path for path in changed_paths if should_reformat_file(cwd / path)} diff --git a/src/darker/git_diff.py b/src/darker/git_diff.py deleted file mode 100644 index 756ecd3bc..000000000 --- a/src/darker/git_diff.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Helpers for doing a ``git diff`` and getting modified line numbers - -The :func:`git_diff_u0` runs ``git diff -U0 -- `` -in the containing directory of ````, -and returns Git output as a bytestring. - -That output can be fed into :func:`get_edit_linenums` -to obtain a list of line numbers in the to-file (modified file) -which were changed from the from-file (file before modification):: - - >>> diff_result = GitDiffResult(b'''\\ - ... diff --git mymodule.py mymodule.py - ... index a57921c..a8afb81 100644 - ... --- mymodule.py - ... +++ mymodule.py - ... @@ -1 +1,2 @@ # will pick +1,2 from this line... - ... -Old first line - ... +Replacement for - ... +first line - ... @@ -10 +11 @@ # ...and +11 from this line - ... -Old tenth line - ... +Replacement for tenth line - ... ''', ['git', 'diff']) - >>> path, linenums = next(get_edit_linenums(diff_result)) - >>> print(path) - mymodule.py - >>> linenums - [1, 2, 11] - -""" -import logging -from pathlib import Path -from subprocess import check_output -from typing import Generator, Iterable, List, NamedTuple, Tuple - -from darker.utils import Buf - -logger = logging.getLogger(__name__) - - -class GitDiffResult(NamedTuple): - output: bytes - command: List[str] - - -def git_diff(paths: Iterable[Path], cwd: Path, context_lines: int) -> GitDiffResult: - """Run ``git diff -U `` and return the output""" - relative_paths = {p.resolve().relative_to(cwd) for p in paths} - cmd = [ - "git", - "diff", - f"-U{context_lines}", - "--relative", - "--no-prefix", - "--", - *[str(path) for path in relative_paths], - ] - logger.debug("[%s]$ %s", cwd, " ".join(cmd)) - return GitDiffResult(check_output(cmd, cwd=str(cwd)), cmd) - - -def parse_range(s: str) -> Tuple[int, int]: - start_str, *length_str = s.split(",") - start_linenum = int(start_str) - if length_str: - # e.g. `+42,2` means lines 42 and 43 were edited - return start_linenum, int(length_str[0]) - else: - # e.g. `+42` means only line 42 was edited - return start_linenum, 1 - - -def get_edit_chunks_for_one_file(lines: Buf) -> Generator[Tuple[int, int], None, None]: - while lines.next_line_startswith("@@ "): - _, remove, add, ats2, *_ = next(lines).split(" ", 4) - add_linenum, num_added = parse_range(add) - while lines.next_line_startswith((" ", "-", "+")): - next(lines) - if num_added: - # e.g. `+42,0` means lines were deleted starting on line 42 - skip those - yield add_linenum, add_linenum + num_added - - -def skip_file(lines: Buf, path: Path) -> None: - logger.info("Skipping non-Python file %s", path) - while lines.next_line_startswith("@@ "): - _ = next(lines) - while lines.next_line_startswith((" ", "-", "+")): - next(lines) - - -def should_reformat_file(path: Path) -> bool: - return path.suffix == ".py" - - -class GitDiffParseError(Exception): - pass - - -def get_edit_chunks( - git_diff_result: GitDiffResult, -) -> Generator[Tuple[Path, List[Tuple[int, int]]], None, None]: - """Yield ranges of changed line numbers in Git diff to-file - - The patch must be in ``git diff -U`` format, and only contain differences for a - single file. - - Yield 2-tuples of one-based line number ranges which are - - - one-based - - start inclusive - - end exclusive - - E.g. ``[42, 7]`` means lines 42, 43, 44, 45, 46, 47 and 48 were changed. - - """ - if not git_diff_result.output: - return - lines = Buf(git_diff_result.output) - command = " ".join(git_diff_result.command) - - def expect_line(expect_startswith: str = "", catch_stop: bool = True) -> str: - if catch_stop: - try: - line = next(lines) - except StopIteration: - raise GitDiffParseError(f"Unexpected end of output from '{command}'") - else: - line = next(lines) - if not line.startswith(expect_startswith): - raise GitDiffParseError( - f"Expected an '{expect_startswith}' line, got '{line}' from '{command}'" - ) - return line - - while True: - try: - diff_git_line = expect_line("diff --git ", catch_stop=False) - except StopIteration: - return - try: - _, _, path_a, path_b = diff_git_line.split(" ") - except ValueError: - raise GitDiffParseError(f"Can't parse '{diff_git_line}'") - path = Path(path_a) - - try: - expect_line("index ") - except GitDiffParseError: - lines.seek_line(-1) - expect_line("old mode ") - expect_line("new mode ") - expect_line("index ") - expect_line(f"--- {path_a}") - expect_line(f"+++ {path_a}") - if should_reformat_file(path): - yield path, list(get_edit_chunks_for_one_file(lines)) - else: - skip_file(lines, path) - - -def get_edit_linenums( - git_diff_result: GitDiffResult, -) -> Generator[Tuple[Path, List[int]], None, None]: - """Yield changed line numbers in Git diff to-file - - The patch must be in ``git diff -U`` format, and only contain differences for a - single file. - - """ - try: - paths_and_ranges = get_edit_chunks(git_diff_result) - except GitDiffParseError: - raise RuntimeError( - "Can't get line numbers for diff output from: %s", - " ".join(git_diff_result.command), - ) - for path, ranges in paths_and_ranges: - if not ranges: - logger.debug(f"Found no edited lines for %s", path) - return - log_linenums = ( - str(start) if end == start + 1 else f"{start}-{end - 1}" - for start, end in ranges - ) - logger.debug("Found edited line(s) for %s: %s", path, ", ".join(log_linenums)) - yield path, [ - linenum - for start_linenum, end_linenum in ranges - for linenum in range(start_linenum, end_linenum) - ] - - -def git_diff_name_only(paths: Iterable[Path], cwd: Path) -> List[Path]: - """Run ``git diff --name-only`` and return file names from the output""" - relative_paths = {p.resolve().relative_to(cwd) for p in paths} - cmd = [ - "git", - "diff", - "--name-only", - "--relative", - "--", - *[str(path) for path in relative_paths], - ] - logger.debug("[%s]$ %s", cwd, " ".join(cmd)) - lines = check_output(cmd, cwd=str(cwd)).decode("utf-8").splitlines() - changed_paths = ((cwd / line).resolve() for line in lines) - return [path for path in changed_paths if should_reformat_file(path)] diff --git a/src/darker/import_sorting.py b/src/darker/import_sorting.py index 057171db4..994027148 100644 --- a/src/darker/import_sorting.py +++ b/src/darker/import_sorting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import List +from typing import List, cast try: from isort import SortImports @@ -10,19 +10,20 @@ logger = logging.getLogger(__name__) -def apply_isort(srcs: List[Path]) -> None: - for src in srcs: - logger.debug( - f"SortImports({str(src)!r}, multi_line_output=3, " - f"include_trailing_comma=True, force_grid_wrap=0, use_parentheses=True," - f" line_length=88, quiet=True)" - ) - _ = SortImports( - str(src), - multi_line_output=3, - include_trailing_comma=True, - force_grid_wrap=0, - use_parentheses=True, - line_length=88, - quiet=True, - ) +def apply_isort(content: str) -> str: + logger.debug( + "SortImports(file_contents=..., check=True, multi_line_output=3, " + "include_trailing_comma=True, force_grid_wrap=0, use_parentheses=True, " + "line_length=88, quiet=True)" + ) + result = SortImports( + file_contents=content, + check=True, + multi_line_output=3, + include_trailing_comma=True, + force_grid_wrap=0, + use_parentheses=True, + line_length=88, + quiet=True, + ) + return cast(str, result.output) diff --git a/src/darker/tests/git_diff_example_output.py b/src/darker/tests/git_diff_example_output.py index 9d936cf1a..bc1cbf24e 100644 --- a/src/darker/tests/git_diff_example_output.py +++ b/src/darker/tests/git_diff_example_output.py @@ -8,18 +8,6 @@ ''' ) -CHANGE_SECOND_LINE = dedent( - '''\ - diff --git test1.py test1.py - index 9ed6856..5a6b0d2 100644 - --- test1.py - +++ test1.py - @@ -2,1 +2,1 @@ - -original second line - +changed second line -''' -) - CHANGED = dedent( '''\ original first line @@ -27,27 +15,3 @@ original third line ''' ) - - -TWO_FILES_CHANGED = dedent( - """\ - diff --git src/darker/git_diff.py src/darker/git_diff.py - index cd3479b..237d999 100644 - --- src/darker/git_diff.py - +++ src/darker/git_diff.py - @@ -103,0 +104,4 @@ def get_edit_linenums(patch: bytes) -> Generator[int, None, None]: - + - + - +# TODO: add multiple file git diffing - + - diff --git src/darker/tests/git_diff_example_output.py src/darker/tests/git_diff_example_output.py - index 3cc7ca1..c5404dd 100644 - --- src/darker/tests/git_diff_example_output.py - +++ src/darker/tests/git_diff_example_output.py - @@ -29,0 +30,4 @@ CHANGED = dedent( - + - + - +TWO_FILES_CHANGED = 'TODO: test case for two changed files' - + - """ -) diff --git a/src/darker/tests/test_black_diff.py b/src/darker/tests/test_black_diff.py index 67428a68f..f605fc1e1 100644 --- a/src/darker/tests/test_black_diff.py +++ b/src/darker/tests/test_black_diff.py @@ -1,154 +1,8 @@ from pathlib import Path -from textwrap import dedent import pytest -from black import FileMode, format_str -from darker.black_diff import diff_and_get_opcodes, opcodes_to_chunks, read_black_config - -FUNCTIONS2_PY = dedent( - '''\ - def f( - a, - **kwargs, - ) -> A: - with cache_dir(): - if something: - result = ( - CliRunner().invoke(black.main, [str(src1), str(src2), "--diff", "--check"]) - ) - limited.append(-limited.pop()) # negate top - return A( - very_long_argument_name1=very_long_value_for_the_argument, - very_long_argument_name2=-very.long.value.for_the_argument, - **kwargs, - ) - def g(): - "Docstring." - def inner(): - pass - print("Inner defs should breathe a little.") - def h(): - def inner(): - pass - print("Inner defs should breathe a little.") -''' -) - - -def test_diff_opcodes(): - src_lines = FUNCTIONS2_PY.splitlines() - dst_lines = format_str(FUNCTIONS2_PY, mode=FileMode()).splitlines() - opcodes = diff_and_get_opcodes(src_lines, dst_lines) - assert opcodes == [ - ('replace', 0, 4, 0, 1), - ('equal', 4, 6, 1, 3), - ('replace', 6, 8, 3, 5), - ('equal', 8, 15, 5, 12), - ('insert', 15, 15, 12, 14), - ('equal', 15, 17, 14, 16), - ('insert', 17, 17, 16, 17), - ('equal', 17, 19, 17, 19), - ('insert', 19, 19, 19, 20), - ('equal', 19, 20, 20, 21), - ('insert', 20, 20, 21, 23), - ('equal', 20, 23, 23, 26), - ('insert', 23, 23, 26, 27), - ('equal', 23, 24, 27, 28), - ] - - -def test_mixed(): - src_lines = FUNCTIONS2_PY.splitlines() - dst_lines = format_str(FUNCTIONS2_PY, mode=FileMode()).splitlines() - opcodes = [ - ('replace', 0, 4, 0, 1), - ('equal', 4, 6, 1, 3), - ('replace', 6, 8, 3, 5), - ('equal', 8, 15, 5, 12), - ('insert', 15, 15, 12, 14), - ('equal', 15, 17, 14, 16), - ('insert', 17, 17, 16, 17), - ('equal', 17, 19, 17, 19), - ('insert', 19, 19, 19, 20), - ('equal', 19, 20, 20, 21), - ('insert', 20, 20, 21, 23), - ('equal', 20, 23, 23, 26), - ('insert', 23, 23, 26, 27), - ('equal', 23, 24, 27, 28), - ] - chunks = list(opcodes_to_chunks(opcodes, src_lines, dst_lines)) - assert chunks == [ - ( - 1, - ['def f(', ' a,', ' **kwargs,', ') -> A:'], - ['def f(a, **kwargs,) -> A:'], - ), - ( - 5, - [' with cache_dir():', ' if something:'], - [' with cache_dir():', ' if something:'], - ), - ( - 7, - [ - ' result = (', - ' CliRunner().invoke(black.main, [str(src1), str(src2), ' - '"--diff", "--check"])', - ], - [ - ' result = CliRunner().invoke(', - ' black.main, [str(src1), str(src2), "--diff", "--check"]', - ], - ), - ( - 9, - [ - ' )', - ' limited.append(-limited.pop()) # negate top', - ' return A(', - ' very_long_argument_name1=very_long_value_for_the_argument,', - ' very_long_argument_name2=-very.long.value.for_the_argument,', - ' **kwargs,', - ' )', - ], - [ - ' )', - ' limited.append(-limited.pop()) # negate top', - ' return A(', - ' very_long_argument_name1=very_long_value_for_the_argument,', - ' very_long_argument_name2=-very.long.value.for_the_argument,', - ' **kwargs,', - ' )', - ], - ), - (16, [], ['', '']), - (16, ['def g():', ' "Docstring."'], ['def g():', ' "Docstring."']), - (18, [], ['']), - ( - 18, - [' def inner():', ' pass'], - [' def inner():', ' pass'], - ), - (20, [], ['']), - ( - 20, - [' print("Inner defs should breathe a little.")'], - [' print("Inner defs should breathe a little.")'], - ), - (21, [], ['', '']), - ( - 21, - ['def h():', ' def inner():', ' pass'], - ['def h():', ' def inner():', ' pass'], - ), - (24, [], ['']), - ( - 24, - [' print("Inner defs should breathe a little.")'], - [' print("Inner defs should breathe a little.")'], - ), - ] +from darker.black_diff import read_black_config, run_black, run_black @pytest.mark.parametrize( @@ -180,3 +34,11 @@ def test_black_config(tmpdir, config_path, config_lines, expect): config = read_black_config(src, config_path and str(toml)) assert config == expect + + +def test_run_black(tmpdir): + src_contents = "print ( '42' )\n" + + result = run_black(Path(tmpdir / "src.py"), src_contents, {}) + + assert result == ['print("42")'] diff --git a/src/darker/tests/test_diff.py b/src/darker/tests/test_diff.py new file mode 100644 index 000000000..299fb1f3e --- /dev/null +++ b/src/darker/tests/test_diff.py @@ -0,0 +1,147 @@ +from textwrap import dedent + +from black import format_str, FileMode + +from darker.diff import ( + diff_and_get_opcodes, + opcodes_to_chunks, + opcodes_to_edit_linenums, +) + +FUNCTIONS2_PY = dedent( + """\ + def f( + a, + **kwargs, + ) -> A: + with cache_dir(): + if something: + result = ( + CliRunner().invoke(black.main, [str(src1), str(src2), "--diff", "--check"]) + ) + limited.append(-limited.pop()) # negate top + return A( + very_long_argument_name1=very_long_value_for_the_argument, + very_long_argument_name2=-very.long.value.for_the_argument, + **kwargs, + ) + def g(): + "Docstring." + def inner(): + pass + print("Inner defs should breathe a little.") + def h(): + def inner(): + pass + print("Inner defs should breathe a little.") +""" +) + +OPCODES = [ + ("replace", 0, 4, 0, 1), + ("equal", 4, 6, 1, 3), + ("replace", 6, 8, 3, 5), + ("equal", 8, 15, 5, 12), + ("insert", 15, 15, 12, 14), + ("equal", 15, 17, 14, 16), + ("insert", 17, 17, 16, 17), + ("equal", 17, 19, 17, 19), + ("insert", 19, 19, 19, 20), + ("equal", 19, 20, 20, 21), + ("insert", 20, 20, 21, 23), + ("equal", 20, 23, 23, 26), + ("insert", 23, 23, 26, 27), + ("equal", 23, 24, 27, 28), +] + + +def test_diff_and_get_opcodes(): + src_lines = FUNCTIONS2_PY.splitlines() + dst_lines = format_str(FUNCTIONS2_PY, mode=FileMode()).splitlines() + opcodes = diff_and_get_opcodes(src_lines, dst_lines) + assert opcodes == OPCODES + + +def test_opcodes_to_chunks(): + src_lines = FUNCTIONS2_PY.splitlines() + dst_lines = format_str(FUNCTIONS2_PY, mode=FileMode()).splitlines() + + chunks = list(opcodes_to_chunks(OPCODES, src_lines, dst_lines)) + + assert chunks == [ + ( + 1, + ["def f(", " a,", " **kwargs,", ") -> A:"], + ["def f(a, **kwargs,) -> A:"], + ), + ( + 5, + [" with cache_dir():", " if something:"], + [" with cache_dir():", " if something:"], + ), + ( + 7, + [ + " result = (", + " CliRunner().invoke(black.main, [str(src1), str(src2), " + '"--diff", "--check"])', + ], + [ + " result = CliRunner().invoke(", + ' black.main, [str(src1), str(src2), "--diff", "--check"]', + ], + ), + ( + 9, + [ + " )", + " limited.append(-limited.pop()) # negate top", + " return A(", + " very_long_argument_name1=very_long_value_for_the_argument,", + " very_long_argument_name2=-very.long.value.for_the_argument,", + " **kwargs,", + " )", + ], + [ + " )", + " limited.append(-limited.pop()) # negate top", + " return A(", + " very_long_argument_name1=very_long_value_for_the_argument,", + " very_long_argument_name2=-very.long.value.for_the_argument,", + " **kwargs,", + " )", + ], + ), + (16, [], ["", ""]), + (16, ["def g():", ' "Docstring."'], ["def g():", ' "Docstring."']), + (18, [], [""]), + ( + 18, + [" def inner():", " pass"], + [" def inner():", " pass"], + ), + (20, [], [""]), + ( + 20, + [' print("Inner defs should breathe a little.")'], + [' print("Inner defs should breathe a little.")'], + ), + (21, [], ["", ""]), + ( + 21, + ["def h():", " def inner():", " pass"], + ["def h():", " def inner():", " pass"], + ), + (24, [], [""]), + ( + 24, + [' print("Inner defs should breathe a little.")'], + [' print("Inner defs should breathe a little.")'], + ), + ] + + +def test_opcodes_to_edit_linenums(): + edit_linenums = list(opcodes_to_edit_linenums(OPCODES)) + + assert edit_linenums == [1, 4, 5, 13, 14, 17, 20, 22, 23, 27] diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py new file mode 100644 index 000000000..71eb14640 --- /dev/null +++ b/src/darker/tests/test_git.py @@ -0,0 +1,77 @@ +from pathlib import Path + +import pytest + +from darker.git import ( + git_get_unmodified_content, + should_reformat_file, + git_diff_name_only, +) + + +def test_get_unmodified_content(git_repo): + paths = git_repo.add({'my.txt': 'original content'}, commit='Initial commit') + paths['my.txt'].write('new content') + + original_content = git_get_unmodified_content(Path('my.txt'), cwd=git_repo.root) + + assert original_content == ['original content'] + + +@pytest.mark.parametrize( + 'path, create, expect', + [ + ('.', False, False), + ('main', True, False), + ('main.c', True, False), + ('main.py', True, True), + ('main.py', False, False), + ('main.pyx', True, False), + ('main.pyi', True, False), + ('main.pyc', True, False), + ('main.pyo', True, False), + ('main.js', True, False), + ], +) +def test_should_reformat_file(tmpdir, path, create, expect): + if create: + (tmpdir / path).ensure() + + result = should_reformat_file(Path(tmpdir / path)) + + assert result == expect + + +@pytest.mark.parametrize( + 'modify_paths, paths, expect', + [ + ({}, ['a.py'], []), + ({}, [], []), + ({'a.py': 'new'}, [], ['a.py']), + ({'a.py': 'new'}, ['b.py'], []), + ({'a.py': 'new'}, ['a.py', 'b.py'], ['a.py']), + ({'c/d.py': 'new'}, ['c/d.py', 'd/f/g.py'], ['c/d.py']), + ({'c/e.js': 'new'}, ['c/e.js'], []), + ({'a.py': 'original'}, ['a.py'], []), + ({'a.py': None}, ['a.py'], []), + ], +) +def test_git_diff_name_only(git_repo, modify_paths, paths, expect): + root = Path(git_repo.root) + git_repo.add( + { + 'a.py': 'original', + 'b.py': 'original', + 'c/d.py': 'original', + 'c/e.js': 'original', + 'd/f/g.py': 'original', + } + ) + for path, content in modify_paths.items(): + absolute_path = git_repo.root / path + if content is None: + absolute_path.remove() + else: + absolute_path.write(content, ensure=True) + result = git_diff_name_only({root / p for p in paths}, cwd=root) + assert {str(p) for p in result} == set(expect) diff --git a/src/darker/tests/test_git_diff.py b/src/darker/tests/test_git_diff.py deleted file mode 100644 index fdfeeea2d..000000000 --- a/src/darker/tests/test_git_diff.py +++ /dev/null @@ -1,162 +0,0 @@ -from pathlib import Path -from textwrap import dedent - -import pytest - -from darker.git_diff import ( - GitDiffParseError, - GitDiffResult, - get_edit_chunks, - get_edit_chunks_for_one_file, - get_edit_linenums, -) -from darker.tests.git_diff_example_output import CHANGE_SECOND_LINE, TWO_FILES_CHANGED -from darker.utils import Buf - - -def test_get_edit_linenums(): - diff_result = GitDiffResult(CHANGE_SECOND_LINE.encode("ascii"), ["git", "diff"]) - ((path, chunks),) = list(get_edit_linenums(diff_result)) - assert path == Path("test1.py") - assert chunks == [2] - - -def test_get_edit_chunks_for_one_file(): - lines = Buf( - dedent( - """\ - @@ -2,1 +2,1 @@ - -original second line - +changed second line - """ - ).encode("ascii") - ) - result = list(get_edit_chunks_for_one_file(lines)) - assert result == [(2, 3)] - - -def test_get_edit_chunks_one_file(): - diff_result = GitDiffResult(CHANGE_SECOND_LINE.encode("ascii"), ["git", "diff"]) - path, chunks = next(get_edit_chunks(diff_result)) - assert path == Path("test1.py") - assert chunks == [(2, 3)] - - -def test_get_edit_chunks_two_files(): - diff_result = GitDiffResult(TWO_FILES_CHANGED.encode("ascii"), ["git", "diff"]) - paths_and_chunks = get_edit_chunks(diff_result) - path, chunks = next(paths_and_chunks) - assert path == Path("src/darker/git_diff.py") - assert chunks == [(104, 108)] - path, chunks = next(paths_and_chunks) - assert path == Path("src/darker/tests/git_diff_example_output.py") - assert chunks == [(30, 34)] - - -def test_get_edit_chunks_empty(): - gen = get_edit_chunks(GitDiffResult(b"", ["git", "diff"])) - with pytest.raises(StopIteration): - next(gen) - - -@pytest.mark.parametrize( - "git_diff_lines", - [[], ["diff --git path_a path_b", "index ", "--- path_a", "+++ path_a"]], -) -def test_get_edit_chunks_empty_output(git_diff_lines): - git_diff_result = GitDiffResult( - "".join(f"{line}\n" for line in git_diff_lines).encode("ascii"), - ["git", "diff"], - ) - result = list(get_edit_chunks(git_diff_result)) - assert result == [] - - -@pytest.mark.parametrize( - "first_line", - ["diff --git ", "diff --git path_a", "diff --git path_a path_b path_c"], -) -def test_get_edit_chunks_cant_parse(first_line): - output = f"{first_line}\n" - git_diff_result = GitDiffResult(output.encode("ascii"), ["git", "diff"]) - with pytest.raises(GitDiffParseError) as exc: - list(get_edit_chunks(git_diff_result)) - assert str(exc.value) == f"Can't parse '{first_line}'" - - -@pytest.mark.parametrize( - "git_diff_lines, expect", - [ - (["first line doesn't have diff --git"], "diff --git ",), - ( - ["diff --git path_a path_b", "second line doesn't have old mode"], - "old mode ", - ), - ( - [ - "diff --git path_a path_b", - "old mode ", - "third line doesn't have new mode", - ], - "new mode ", - ), - ( - [ - "diff --git path_a path_b", - "old mode ", - "new mode ", - "fourth line doesn't have index", - ], - "index ", - ), - ( - [ - "diff --git path_a path_b", - "index ", - "third line doesn't have --- path_a", - ], - "--- path_a", - ), - ( - [ - "diff --git path_a path_b", - "index ", - "--- path_a", - "fourth line doesn't have +++ path_a", - ], - "+++ path_a", - ), - ], -) -def test_get_edit_chunks_unexpected_line(git_diff_lines, expect): - git_diff_result = GitDiffResult( - "".join(f"{line}\n" for line in git_diff_lines).encode("ascii"), - ["git", "diff"], - ) - with pytest.raises(GitDiffParseError) as exc: - list(get_edit_chunks(git_diff_result)) - expect_exception_message = ( - f"Expected an '{expect}' line, got '{git_diff_lines[-1]}' from 'git diff'" - ) - assert str(exc.value) == expect_exception_message - - -@pytest.mark.parametrize( - "git_diff_lines", - [ - ["diff --git path_a path_b"], - ["diff --git path_a path_b", "old mode "], - ["diff --git path_a path_b", "old mode ", "new mode "], - ["diff --git path_a path_b", "old mode ", "new mode ", "index "], - ["diff --git path_a path_b", "index "], - ["diff --git path_a path_b", "index ", "--- path_a"], - ], -) -def test_get_edit_chunks_unexpected_end(git_diff_lines): - git_diff_result = GitDiffResult( - "".join(f"{line}\n" for line in git_diff_lines).encode("ascii"), - ["git", "diff"], - ) - with pytest.raises(GitDiffParseError) as exc: - list(get_edit_chunks(git_diff_result)) - assert str(exc.value) == "Unexpected end of output from 'git diff'" diff --git a/src/darker/tests/test_import_sorting.py b/src/darker/tests/test_import_sorting.py new file mode 100644 index 000000000..9adbaa179 --- /dev/null +++ b/src/darker/tests/test_import_sorting.py @@ -0,0 +1,10 @@ +from darker.import_sorting import apply_isort + +ORIGINAL_SOURCE = "import sys\nimport os\n" +ISORTED_SOURCE = "import os\nimport sys\n" + + +def test_apply_isort(): + result = apply_isort(ORIGINAL_SOURCE) + + assert result == ISORTED_SOURCE diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index 828323793..8be228433 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -22,13 +22,12 @@ def test_isort_option_without_isort(tmpdir, without_isort, caplog): @pytest.fixture -def run_isort(tmpdir, monkeypatch, caplog): - monkeypatch.chdir(tmpdir) - check_call(["git", "init"], cwd=tmpdir) +def run_isort(git_repo, monkeypatch, caplog): + monkeypatch.chdir(git_repo.root) + paths = git_repo.add({'test1.py': 'original'}, commit='Initial commit') + paths['test1.py'].write('changed') with patch.multiple( - darker.__main__, - run_black=Mock(return_value=([], [])), - git_diff_name_only=Mock(return_value=[Path(tmpdir / 'test1.py')]), + darker.__main__, run_black=Mock(return_value=[]), verify_ast_unchanged=Mock(), ), patch("darker.import_sorting.SortImports"): darker.__main__.main(["--isort", "./test1.py"]) return SimpleNamespace( @@ -42,7 +41,8 @@ def test_isort_option_with_isort(run_isort): def test_isort_option_with_isort_calls_sortimports(run_isort): run_isort.SortImports.assert_called_once_with( - str(Path.cwd() / "test1.py"), + file_contents="changed", + check=True, force_grid_wrap=0, include_trailing_comma=True, line_length=88, @@ -58,12 +58,6 @@ def test_format_edited_parts_empty(): darker.__main__.format_edited_parts([], False, {}, True) -def test_format_edited_parts_isort_print_diff(): - with pytest.raises(NotImplementedError): - - darker.__main__.format_edited_parts([Path('test.py')], True, {}, True) - - A_PY = ['import sys', 'import os', "print( '42')", ''] A_PY_BLACK = ['import sys', 'import os', '', 'print("42")', ''] A_PY_BLACK_ISORT = ['import os', 'import sys', '', 'print("42")', ''] @@ -94,28 +88,53 @@ def test_format_edited_parts_isort_print_diff(): '', ] +A_PY_DIFF_BLACK_ISORT = [ + '--- /a.py', + '+++ /a.py', + '@@ -1,3 +1,4 @@', + '', + '+import os', + ' import sys', + '-import os', + "-print( '42')", + '+', + '+print("42")', + '', +] + @pytest.mark.parametrize( - 'srcs, isort, black_args, print_diff, expect_stdout, expect_a_py', + 'isort, black_args, print_diff, expect_stdout, expect_a_py', [ - (['a.py'], False, {}, True, A_PY_DIFF_BLACK, A_PY), - (['a.py'], True, {}, False, [''], A_PY_BLACK_ISORT), + (False, {}, True, A_PY_DIFF_BLACK, A_PY), + ( + True, + {}, + False, + ['ERROR: Imports are incorrectly sorted.', ''], + A_PY_BLACK_ISORT, + ), ( - ['a.py'], False, {'skip_string_normalization': True}, True, A_PY_DIFF_BLACK_NO_STR_NORMALIZE, A_PY, ), - (['a.py'], False, {}, False, [''], A_PY_BLACK), + (False, {}, False, [''], A_PY_BLACK), + ( + True, + {}, + True, + ['ERROR: Imports are incorrectly sorted.'] + A_PY_DIFF_BLACK_ISORT, + A_PY, + ), ], ) def test_format_edited_parts( git_repo, monkeypatch, capsys, - srcs, isort, black_args, print_diff, @@ -126,9 +145,7 @@ def test_format_edited_parts( paths = git_repo.add({'a.py': '\n', 'b.py': '\n'}, commit='Initial commit') paths['a.py'].write('\n'.join(A_PY)) paths['b.py'].write('print(42 )\n') - darker.__main__.format_edited_parts( - [Path(src) for src in srcs], isort, black_args, print_diff - ) + darker.__main__.format_edited_parts([Path('a.py')], isort, black_args, print_diff) stdout = capsys.readouterr().out.replace(str(git_repo.root), '') assert stdout.split('\n') == expect_stdout assert paths['a.py'].readlines(cr=False) == expect_a_py diff --git a/src/darker/tests/test_verification.py b/src/darker/tests/test_verification.py index 7a6081d46..ef3e9cf67 100644 --- a/src/darker/tests/test_verification.py +++ b/src/darker/tests/test_verification.py @@ -1,20 +1,20 @@ import pytest -from darker.verification import verify_ast_unchanged, NotEquivalentError +from darker.verification import NotEquivalentError, verify_ast_unchanged @pytest.mark.parametrize( - "src_lines, dst_content, expect", + "src_content, dst_content, expect", [ - (["if True: pass"], "if False: pass\n", AssertionError), - (["if True: pass"], "if True:\n pass\n", None), + ("if True: pass\n", "if False: pass\n", AssertionError), + ("if True: pass\n", "if True:\n pass\n", None), ], ) -def test_verify_ast_unchanged(src_lines, dst_content, expect): +def test_verify_ast_unchanged(src_content, dst_content, expect): black_chunks = [(1, ["black"], ["chunks"])] edited_linenums = [1, 2] try: - verify_ast_unchanged(src_lines, dst_content, black_chunks, edited_linenums) + verify_ast_unchanged(src_content, dst_content, black_chunks, edited_linenums) except NotEquivalentError: assert expect is AssertionError else: diff --git a/src/darker/verification.py b/src/darker/verification.py index 8399f1610..2bae94eff 100644 --- a/src/darker/verification.py +++ b/src/darker/verification.py @@ -12,13 +12,12 @@ class NotEquivalentError(Exception): def verify_ast_unchanged( - edited_to_file_lines: List[str], + edited_to_file_str: str, reformatted_str: str, black_chunks: List[Tuple[int, List[str], List[str]]], edited_linenums: List[int], ) -> None: """Verify that source code parses to the same AST before and after reformat""" - edited_to_file_str = joinlines(edited_to_file_lines) try: assert_equivalent(edited_to_file_str, reformatted_str) except AssertionError as exc_info: From f1bcf85d10298fdc722fba8780cd590a20fadb53 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Fri, 3 Jul 2020 18:48:23 +0300 Subject: [PATCH 18/18] Fix import sorting --- src/darker/diff.py | 2 +- src/darker/tests/test_black_diff.py | 2 +- src/darker/tests/test_diff.py | 2 +- src/darker/tests/test_git.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/darker/diff.py b/src/darker/diff.py index 493f91ccf..a6db970d3 100644 --- a/src/darker/diff.py +++ b/src/darker/diff.py @@ -67,7 +67,7 @@ import logging from difflib import SequenceMatcher -from typing import List, Generator, Tuple +from typing import Generator, List, Tuple logger = logging.getLogger(__name__) diff --git a/src/darker/tests/test_black_diff.py b/src/darker/tests/test_black_diff.py index f605fc1e1..82363da6d 100644 --- a/src/darker/tests/test_black_diff.py +++ b/src/darker/tests/test_black_diff.py @@ -2,7 +2,7 @@ import pytest -from darker.black_diff import read_black_config, run_black, run_black +from darker.black_diff import read_black_config, run_black @pytest.mark.parametrize( diff --git a/src/darker/tests/test_diff.py b/src/darker/tests/test_diff.py index 299fb1f3e..2bf7ca95b 100644 --- a/src/darker/tests/test_diff.py +++ b/src/darker/tests/test_diff.py @@ -1,6 +1,6 @@ from textwrap import dedent -from black import format_str, FileMode +from black import FileMode, format_str from darker.diff import ( diff_and_get_opcodes, diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py index 71eb14640..563f82198 100644 --- a/src/darker/tests/test_git.py +++ b/src/darker/tests/test_git.py @@ -3,9 +3,9 @@ import pytest from darker.git import ( + git_diff_name_only, git_get_unmodified_content, should_reformat_file, - git_diff_name_only, )