Esempio n. 1
0
def test_opcodes_to_edit_linenums(context_lines, expect):
    edit_linenums = list(opcodes_to_edit_linenums(OPCODES, context_lines))
    expect_ranges = [[n, n] if isinstance(n, int) else n for n in expect]
    expect_linenums = list(
        chain(*(range(n[0], n[1] + 1) for n in expect_ranges)))

    assert edit_linenums == expect_linenums
Esempio n. 2
0
    def revision_vs_lines(self, path_in_repo: Path, lines: List[str],
                          context_lines: int) -> List[int]:
        """For file `path_in_repo`, return changed line numbers from given revision

        :param path_in_repo: Path of the file to compare, relative to repository root
        :param lines: The contents to compare to, e.g. from current working tree
        :return: Line numbers of lines changed between the revision and given content

        """
        revision_lines = git_get_unmodified_content(path_in_repo,
                                                    self.revision,
                                                    self.git_root)
        edited_opcodes = diff_and_get_opcodes(revision_lines, lines)
        return list(opcodes_to_edit_linenums(edited_opcodes, context_lines))
Esempio n. 3
0
File: git.py Progetto: wnoise/darker
    def revision_vs_lines(self, path_in_repo: Path, content: TextDocument,
                          context_lines: int) -> List[int]:
        """For file `path_in_repo`, return changed line numbers from given revision

        :param path_in_repo: Path of the file to compare, relative to repository root
        :param content: The contents to compare to, e.g. from current working tree
        :param context_lines: The number of lines to include before and after a change
        :return: Line numbers of lines changed between the revision and given content

        """
        old = git_get_content_at_revision(path_in_repo, self.revrange.rev1,
                                          self.git_root)
        edited_opcodes = diff_and_get_opcodes(old, content)
        return list(opcodes_to_edit_linenums(edited_opcodes, context_lines))
Esempio n. 4
0
def test_opcodes_to_edit_linenums_empty_opcodes():
    result = list(opcodes_to_edit_linenums([], context_lines=0))

    assert result == []
Esempio n. 5
0
def format_edited_parts(
    srcs: Iterable[Path],
    isort: bool,
    black_args: Dict[str, Union[bool, int]],
    print_diff: bool,
) -> None:
    """Black (and optional isort) formatting for chunks with edits since the last commit

    1. run isort on each edited file
    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
    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. concatenate all chosen chunks
    9. verify that the resulting reformatted source code parses to an identical AST as
       the original edited to-file
    10. write the reformatted source back to the original file

    :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

    """
    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:
        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
            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
            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))

            # 5. get the diff between each edited and reformatted file
            opcodes = diff_and_get_opcodes(edited, formatted)

            # 6. convert the diff into chunks
            black_chunks = list(opcodes_to_chunks(opcodes, edited, formatted))

            # 7. choose reformatted content
            chosen_lines: List[str] = list(choose_lines(black_chunks, edited_linenums))

            # 8. concatenate chosen chunks
            result_str = joinlines(chosen_lines)

            # 9. verify
            logger.debug(
                "Verifying that the %s original edited lines and %s reformatted lines "
                "parse into an identical abstract syntax tree",
                len(edited),
                len(chosen_lines),
            )
            try:
                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.
                # Try again with a larger `-U<context_lines>` option for `git diff`,
                # or give up if `context_lines` is already very large.
                if context_lines == MAX_CONTEXT_LINES:
                    raise
                logger.debug(
                    "AST verification failed. "
                    "Trying again with %s lines of context for `git diff -U`",
                    context_lines + 1,
                )
                continue
            else:
                # 10. A re-formatted Python file which produces an identical AST was
                #     created successfully - write an updated file
                #     or print the diff
                if print_diff:
                    difflines = list(
                        unified_diff(
                            worktree_srcs[src_relative].splitlines(),
                            chosen_lines,
                            src.as_posix(),
                            src.as_posix(),
                        )
                    )
                    if len(difflines) > 2:
                        h1, h2, *rest = difflines
                        print(h1, end="")
                        print(h2, end="")
                        print("\n".join(rest))
                else:
                    logger.info("Writing %s bytes into %s", len(result_str), src)
                    src.write_text(result_str)
                break
Esempio n. 6
0
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]