Esempio n. 1
0
def test_format_edited_parts_lint(git_repo):
    """Unit test for ``format_edited_parts`` with linters"""
    paths = git_repo.add({"a.py": "pass\n"}, commit="Initial commit")
    paths["a.py"].write_bytes(b'"properly"\n"formatted"\n')
    with patch.object(darker.__main__, "run_linter") as run_linter:

        _ = list(
            darker.__main__.format_edited_parts(
                [Path("a.py")],
                RevisionRange("HEAD"),
                False,
                ["linter1", "linter2 command line"],
                {},
            ))

        assert run_linter.call_args_list == [
            call("linter1", git_repo.root, {Path("a.py")},
                 RevisionRange("HEAD")),
            call(
                "linter2 command line",
                git_repo.root,
                {Path("a.py")},
                RevisionRange("HEAD"),
            ),
        ]
Esempio n. 2
0
def test_git_get_modified_files(git_repo, modify_paths, paths, expect):
    """Tests for `darker.git.git_get_modified_files()`"""
    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',
        },
        commit="Initial commit",
    )
    for path, content in modify_paths.items():
        absolute_path = git_repo.root / path
        if content is None:
            absolute_path.unlink()
        else:
            absolute_path.parent.mkdir(parents=True, exist_ok=True)
            absolute_path.write_bytes(content.encode("ascii"))

    result = git_get_modified_files({root / p
                                     for p in paths},
                                    RevisionRange("HEAD"),
                                    cwd=root)

    assert result == {Path(p) for p in expect}
Esempio n. 3
0
def test_format_edited_parts_ast_changed(git_repo, caplog):
    """``darker.__main__.format_edited_parts()`` when reformatting changes the AST"""
    caplog.set_level(logging.DEBUG, logger="darker.__main__")
    paths = git_repo.add({"a.py": "1\n2\n3\n4\n5\n6\n7\n8\n"},
                         commit="Initial commit")
    paths["a.py"].write_bytes(b"8\n7\n6\n5\n4\n3\n2\n1\n")
    with patch.object(
            darker.__main__,
            "verify_ast_unchanged") as verify_ast_unchanged, pytest.raises(
                NotEquivalentError):
        verify_ast_unchanged.side_effect = NotEquivalentError

        _ = list(
            darker.__main__.format_edited_parts(
                git_repo.root,
                [Path("a.py")],
                RevisionRange("HEAD"),
                enable_isort=False,
                black_args={},
            ))
    a_py = str(paths["a.py"])
    main = "darker.__main__:__main__.py"
    log = [
        line for line in re.sub(r":\d+", "", caplog.text).splitlines()
        if " lines of context " in line
    ]
    assert log == [
        f"DEBUG    {main} AST verification of {a_py} with 0 lines of context failed",
        f"DEBUG    {main} Trying with 5 lines of context for `git diff -U {a_py}`",
        f"DEBUG    {main} AST verification of {a_py} with 5 lines of context failed",
        f"DEBUG    {main} Trying with 7 lines of context for `git diff -U {a_py}`",
        f"DEBUG    {main} AST verification of {a_py} with 7 lines of context failed",
        f"DEBUG    {main} Trying with 8 lines of context for `git diff -U {a_py}`",
        f"DEBUG    {main} AST verification of {a_py} with 8 lines of context failed",
    ]
Esempio n. 4
0
def test_git_get_modified_files(git_repo, modify_paths, paths, expect):
    """Tests for `darker.git.git_get_modified_files()`"""
    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',
        },
        commit="Initial commit",
    )
    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_get_modified_files({root / p
                                     for p in paths},
                                    RevisionRange("HEAD"),
                                    cwd=root)

    assert {str(p) for p in result} == set(expect)
Esempio n. 5
0
def test_run_linter(git_repo, monkeypatch, capsys, _descr, paths, location,
                    expect):
    """Linter gets correct paths on command line and outputs just changed lines

    We use ``echo`` as our "linter". It just adds the paths of each file to lint as an
    "error" on a line of ``test.py``. What this test does is the equivalent of e.g.::

    - creating a ``test.py`` such that the first line is modified after the last commit
    - creating and committing ``one.py`` and ``two.py``
    - running::

          $ darker -L 'echo test.py:1:' one.py two.py
          test.py:1: git-repo-root/one.py git-repo-root/two.py

    """
    src_paths = git_repo.add({"test.py": "1\n2\n"}, commit="Initial commit")
    src_paths["test.py"].write_bytes(b"one\n2\n")
    monkeypatch.chdir(git_repo.root)
    cmdline = f"echo {location}"

    linting.run_linter(cmdline, Path(git_repo.root), {Path(p)
                                                      for p in paths},
                       RevisionRange("HEAD"))

    # We can now verify that the linter received the correct paths on its command line
    # by checking standard output from the our `echo` "linter".
    # The test cases also verify that only linter reports on modified lines are output.
    result = capsys.readouterr().out.splitlines()
    # Use evil `eval()` so we get Windows compatible expected paths:
    assert result == [
        eval(f'f"{line}"', {"git_repo": git_repo}) for line in expect
    ]
Esempio n. 6
0
def test_format_edited_parts_empty():
    with pytest.raises(ValueError):

        list(
            darker.__main__.format_edited_parts(
                [], RevisionRange("HEAD"), False, [], {}
            )
        )
Esempio n. 7
0
def test_run_linter_return_value(git_repo, location, expect):
    """``run_linter()`` returns the number of linter errors on modified lines"""
    src_paths = git_repo.add({"test.py": "1\n2\n"}, commit="Initial commit")
    src_paths["test.py"].write_bytes(b"one\n2\n")
    cmdline = f"echo {location}"

    result = linting.run_linter(cmdline, Path(git_repo.root),
                                {Path("test.py")}, RevisionRange("HEAD"))

    assert result == expect
Esempio n. 8
0
def test_revisionrange_parse_pre_commit(environ, expect_rev1, expect_rev2,
                                        expect_use_common_ancestor):
    """RevisionRange.parse(':PRE-COMMIT:') gets the range from environment variables"""
    with patch.dict(os.environ, environ):

        result = RevisionRange.parse(":PRE-COMMIT:")

        assert result.rev1 == expect_rev1
        assert result.rev2 == expect_rev2
        assert result.use_common_ancestor == expect_use_common_ancestor
Esempio n. 9
0
def test_run_linter_non_worktree():
    """``run_linter()`` doesn't support linting commits, only the worktree"""
    with pytest.raises(NotImplementedError):

        linting.run_linter(
            "dummy-linter",
            Path("/dummy"),
            {Path("dummy.py")},
            RevisionRange.parse("..HEAD"),
        )
Esempio n. 10
0
def test_git_get_modified_files_revision_range(_description, branched_repo,
                                               revrange, expect):
    """Test for :func:`darker.git.git_get_modified_files` with a revision range"""
    result = git_get_modified_files(
        [Path(branched_repo.root)],
        RevisionRange.parse(revrange),
        Path(branched_repo.root),
    )

    assert {path.name for path in result} == expect
Esempio n. 11
0
def test_edited_linenums_differ_revision_vs_lines(git_repo, context_lines,
                                                  expect):
    """Tests for EditedLinenumsDiffer.revision_vs_lines()"""
    git_repo.add({'a.py': '1\n2\n3\n4\n5\n6\n7\n8\n'}, commit='Initial commit')
    content = TextDocument.from_lines(
        ["1", "2", "three", "4", "5", "6", "seven", "8"])
    differ = EditedLinenumsDiffer(git_repo.root, RevisionRange("HEAD"))

    result = differ.revision_vs_lines(Path("a.py"), content, context_lines)

    assert result == expect
Esempio n. 12
0
def test_edited_linenums_differ_revision_vs_worktree(git_repo, context_lines,
                                                     expect):
    """Tests for EditedLinenumsDiffer.revision_vs_worktree()"""
    paths = git_repo.add({"a.py": "1\n2\n3\n4\n5\n6\n7\n8\n"},
                         commit="Initial commit")
    paths["a.py"].write("1\n2\nthree\n4\n5\n6\nseven\n8\n")
    differ = EditedLinenumsDiffer(Path(git_repo.root), RevisionRange("HEAD"))

    result = differ.compare_revisions(Path("a.py"), context_lines)

    assert result == expect
Esempio n. 13
0
def test_run_linters(linter_cmdlines, linters_return, expect_result):
    """Unit test for ``run_linters()``"""
    with patch.object(linting, "run_linter") as run_linter:
        run_linter.side_effect = linters_return

        result = linting.run_linters(
            linter_cmdlines,
            Path("dummy git_root"),
            {Path("dummy paths")},
            RevisionRange("dummy revrange"),
        )

        expect_calls = [
            call(
                linter_cmdline,
                Path("dummy git_root"),
                {Path("dummy paths")},
                RevisionRange("dummy revrange"),
            ) for linter_cmdline in linter_cmdlines
        ]
        assert run_linter.call_args_list == expect_calls
        assert result == expect_result
Esempio n. 14
0
def test_format_edited_parts_all_unchanged(git_repo, monkeypatch):
    """``format_edited_parts()`` yields nothing if no reformatting was needed"""
    monkeypatch.chdir(git_repo.root)
    paths = git_repo.add({'a.py': 'pass\n', 'b.py': 'pass\n'}, commit='Initial commit')
    paths['a.py'].write('"properly"\n"formatted"\n')
    paths['b.py'].write('"not"\n"checked"\n')

    result = list(
        darker.__main__.format_edited_parts(
            [Path("a.py")], RevisionRange("HEAD"), True, [], {}
        )
    )

    assert result == []
Esempio n. 15
0
def main(argv: List[str] = None) -> int:
    """Parse the command line and apply black formatting for each source file

    :param argv: The command line arguments to the ``darker`` command
    :return: 1 if the ``--check`` argument was provided and at least one file was (or
             should be) reformatted; 0 otherwise.

    """
    if argv is None:
        argv = sys.argv[1:]
    args, config, config_nondefault = parse_command_line(argv)
    logging.basicConfig(level=args.log_level)
    if args.log_level == logging.INFO:
        formatter = logging.Formatter("%(levelname)s: %(message)s")
        logging.getLogger().handlers[0].setFormatter(formatter)

    # Make sure we don't get excessive debug log output from Black
    logging.getLogger("blib2to3.pgen2.driver").setLevel(logging.WARNING)

    if args.log_level <= logging.DEBUG:
        print("\n# Effective configuration:\n")
        print(dump_config(config))
        print("\n# Configuration options which differ from defaults:\n")
        print(dump_config(config_nondefault))
        print("\n")

    if args.isort and not isort:
        logger.error(f"{ISORT_INSTRUCTION} to use the `--isort` option.")
        exit(1)

    black_args = BlackArgs()
    if args.config:
        black_args["config"] = args.config
    if args.line_length:
        black_args["line_length"] = args.line_length
    if args.skip_string_normalization is not None:
        black_args["skip_string_normalization"] = args.skip_string_normalization

    paths = {Path(p) for p in args.src}
    some_files_changed = False
    revrange = RevisionRange.parse(args.revision)
    for path, old, new in format_edited_parts(
        paths, revrange, args.isort, args.lint, black_args
    ):
        some_files_changed = True
        if args.diff:
            print_diff(path, old, new)
        if not args.check and not args.diff:
            modify_file(path, new)
    return 1 if args.check and some_files_changed else 0
Esempio n. 16
0
def test_format_edited_parts(git_repo, monkeypatch, enable_isort, black_args, expect):
    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')

    changes = [
        (path, worktree_content.string, chosen.string, chosen.lines)
        for path, worktree_content, chosen in darker.__main__.format_edited_parts(
            [Path("a.py")], RevisionRange("HEAD"), enable_isort, [], black_args
        )
    ]

    expect_changes = [
        (paths["a.py"], "\n".join(A_PY), "\n".join(expect), tuple(expect[:-1]))
    ]
    assert changes == expect_changes
Esempio n. 17
0
def test_format_edited_parts(git_repo, enable_isort, black_args, newline,
                             expect):
    """Correct reformatting and import sorting changes are produced"""
    paths = git_repo.add({
        "a.py": newline,
        "b.py": newline
    },
                         commit="Initial commit")
    paths["a.py"].write_bytes(newline.join(A_PY).encode("ascii"))
    paths["b.py"].write_bytes("print(42 ){newline}".encode("ascii"))

    result = darker.__main__.format_edited_parts([Path("a.py")],
                                                 RevisionRange("HEAD"),
                                                 enable_isort, [], black_args)

    changes = [(path, worktree_content.string, chosen.string, chosen.lines)
               for path, worktree_content, chosen in result]
    expect_changes = [(paths["a.py"], newline.join(A_PY), newline.join(expect),
                       tuple(expect[:-1]))]
    assert changes == expect_changes
Esempio n. 18
0
def test_format_edited_parts_all_unchanged(git_repo, monkeypatch):
    """``format_edited_parts()`` yields nothing if no reformatting was needed"""
    monkeypatch.chdir(git_repo.root)
    paths = git_repo.add({
        "a.py": "pass\n",
        "b.py": "pass\n"
    },
                         commit="Initial commit")
    paths["a.py"].write_bytes(b'"properly"\n"formatted"\n')
    paths["b.py"].write_bytes(b'"not"\n"checked"\n')

    result = list(
        darker.__main__.format_edited_parts(
            Path(git_repo.root),
            {Path("a.py"), Path("b.py")},
            RevisionRange("HEAD"),
            True,
            {},
        ))

    assert result == []
Esempio n. 19
0
    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):

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

    assert mode_class_mock.call_args_list == [expect]


@pytest.mark.parametrize(
    'options, expect',
    [
        (["a.py"], ({Path("a.py")}, RevisionRange("HEAD"), False, [], {})),
        (["--isort", "a.py"],
         ({Path("a.py")}, RevisionRange("HEAD"), True, [], {})),
        (
            ["--config", "my.cfg", "a.py"],
            ({Path("a.py")}, RevisionRange("HEAD"), False, [], {
                "config": "my.cfg"
            }),
        ),
        (
            ["--line-length", "90", "a.py"],
            ({Path("a.py")}, RevisionRange("HEAD"), False, [], {
                "line_length": 90
            }),
        ),
        (
Esempio n. 20
0
def test_revisionrange_parse(revision_range, expect):
    """Test for :meth:`RevisionRange.parse`"""
    revrange = RevisionRange.parse(revision_range)
    assert (revrange.rev1, revrange.rev2,
            revrange.use_common_ancestor) == expect
Esempio n. 21
0
def main(argv: List[str] = None) -> int:
    """Parse the command line and reformat and optionally lint each source file

    1. run isort on each edited file (optional)
    2. diff the given revision and worktree (optionally with isort modifications) 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
    6. convert the diff into chunks, keeping original and reformatted content for each
       chunk
    7. choose reformatted content for each chunk if there were any changed lines inside
       the chunk in the edited to-file, or choose the chunk's original contents if no
       edits were done in that chunk
    8. verify that the resulting reformatted source code parses to an identical AST as
       the original edited to-file
    9. write the reformatted source back to the original file
    10. run linter subprocesses for all edited files (10.-13. optional)
    11. diff the given revision and worktree (after isort and Black reformatting) for
        each file reported by a linter
    12. extract line numbers in each file reported by a linter for changed lines
    13. print only linter error lines which fall on changed lines

    :param argv: The command line arguments to the ``darker`` command
    :return: 1 if the ``--check`` argument was provided and at least one file was (or
             should be) reformatted; 0 otherwise.

    """
    if argv is None:
        argv = sys.argv[1:]
    args, config, config_nondefault = parse_command_line(argv)
    logging.basicConfig(level=args.log_level)
    if args.log_level == logging.INFO:
        formatter = logging.Formatter("%(levelname)s: %(message)s")
        logging.getLogger().handlers[0].setFormatter(formatter)

    # Make sure we don't get excessive debug log output from Black
    logging.getLogger("blib2to3.pgen2.driver").setLevel(logging.WARNING)

    if args.log_level <= logging.DEBUG:
        print("\n# Effective configuration:\n")
        print(dump_config(config))
        print("\n# Configuration options which differ from defaults:\n")
        print(dump_config(config_nondefault))
        print("\n")

    if args.isort and not isort:
        logger.error(f"{ISORT_INSTRUCTION} to use the `--isort` option.")
        exit(1)

    black_args = BlackArgs()
    if args.config:
        black_args["config"] = args.config
    if args.line_length:
        black_args["line_length"] = args.line_length
    if args.skip_string_normalization is not None:
        black_args[
            "skip_string_normalization"] = args.skip_string_normalization

    paths = {Path(p) for p in args.src}
    git_root = get_common_root(paths)
    failures_on_modified_lines = False
    revrange = RevisionRange.parse(args.revision)
    changed_files = git_get_modified_files(paths, revrange, git_root)
    for path, old, new in format_edited_parts(git_root, changed_files,
                                              revrange, args.isort,
                                              black_args):
        failures_on_modified_lines = True
        if args.diff:
            print_diff(path, old, new)
        if not args.check and not args.diff:
            modify_file(path, new)
    if run_linters(args.lint, git_root, changed_files, revrange):
        failures_on_modified_lines = True
    return 1 if args.check and failures_on_modified_lines else 0
Esempio n. 22
0
    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):

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

    assert mode_class_mock.call_args_list == [expect]


@pytest.mark.kwparametrize(
    dict(
        options=["a.py"],
        # Expected arguments to the `format_edited_parts()` call.
        # `Path("git_root")` will be replaced with the temporary Git repository root:
        expect=(Path("git_root"), {Path("a.py")}, RevisionRange("HEAD"), False, {}),
    ),
    dict(
        options=["--isort", "a.py"],
        expect=(Path("git_root"), {Path("a.py")}, RevisionRange("HEAD"), True, {}),
    ),
    dict(
        options=["--config", "my.cfg", "a.py"],
        expect=(
            Path("git_root"),
            {Path("a.py")},
            RevisionRange("HEAD"),
            False,
            {"config": "my.cfg"},
        ),
    ),