Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Black as a plugin #738

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Added
- Prevent Pylint from updating beyond version 3.2.7 due to dropped Python 3.8 support.
- The ``--formatter=black`` option (the default) has been added in preparation for
future formatters.
- Invoking Black is now implemented as a plugin. This allows for easier integration of
other formatters in the future. There's also a dummy ``none`` formatter plugin.
- ``--formatter=none`` now skips running Black. This is useful when you only want to run
Isort or Flynt.

Removed
-------
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ The following `command line arguments`_ can also be used to modify the defaults:
versions that should be supported by Black's output. [default: per-file auto-
detection]
--formatter FORMATTER
Formatter to use for reformatting code
[black\|none] Formatter to use for reformatting code. [default: black]

To change default values for these options for a given project,
add a ``[tool.darker]`` section to ``pyproject.toml`` in the project's root directory,
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ darker =
.pyi

[options.entry_points]
darker.formatter =
black = darker.formatters.black_formatter:BlackFormatter
none = darker.formatters.none_formatter:NoneFormatter
console_scripts =
darker = darker.__main__:main_with_error_handling

Expand Down
106 changes: 50 additions & 56 deletions src/darker/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Darker - apply black reformatting to only areas edited since the last commit"""
"""Darker - re-format only code areas edited since the last commit."""

import concurrent.futures
import logging
Expand All @@ -10,18 +10,16 @@
from pathlib import Path
from typing import Collection, Generator, List, Optional, Tuple

from darker.black_diff import (
BlackConfig,
filter_python_files,
read_black_config,
run_black,
)
from darker.chooser import choose_lines
from darker.command_line import parse_command_line
from darker.concurrency import get_executor
from darker.config import Exclusions, OutputMode, validate_config_output_mode
from darker.diff import diff_chunks
from darker.exceptions import DependencyError, MissingPackageError
from darker.files import filter_python_files
from darker.formatters import create_formatter
from darker.formatters.base_formatter import BaseFormatter
from darker.formatters.none_formatter import NoneFormatter
from darker.fstring import apply_flynt, flynt
from darker.git import (
EditedLinenumsDiffer,
Expand Down Expand Up @@ -60,12 +58,12 @@
ProcessedDocument = Tuple[Path, TextDocument, TextDocument]


def format_edited_parts( # pylint: disable=too-many-arguments
def format_edited_parts( # noqa: PLR0913 # pylint: disable=too-many-arguments
root: Path,
changed_files: Collection[Path], # pylint: disable=unsubscriptable-object
changed_files: Collection[Path],
exclude: Exclusions,
revrange: RevisionRange,
black_config: BlackConfig,
formatter: BaseFormatter,
report_unmodified: bool,
workers: int = 1,
) -> Generator[ProcessedDocument, None, None]:
Expand All @@ -82,7 +80,7 @@ def format_edited_parts( # pylint: disable=too-many-arguments
modified in the repository between the given Git revisions
:param exclude: Files to exclude when running Black,``isort`` or ``flynt``
:param revrange: The Git revisions to compare
:param black_config: Configuration to use for running Black
:param formatter: The code re-formatter to use
:param report_unmodified: ``True`` to yield also files which weren't modified
:param workers: number of cpu processes to use (0 - autodetect)
:return: A generator which yields details about changes for each file which should
Expand All @@ -101,7 +99,7 @@ def format_edited_parts( # pylint: disable=too-many-arguments
edited_linenums_differ,
exclude,
revrange,
black_config,
formatter,
)
futures.append(future)

Expand All @@ -115,21 +113,22 @@ def format_edited_parts( # pylint: disable=too-many-arguments
yield (absolute_path_in_rev2, rev2_content, content_after_reformatting)


def _modify_and_reformat_single_file( # pylint: disable=too-many-arguments
def _modify_and_reformat_single_file( # noqa: PLR0913
root: Path,
relative_path_in_rev2: Path,
edited_linenums_differ: EditedLinenumsDiffer,
exclude: Exclusions,
revrange: RevisionRange,
black_config: BlackConfig,
formatter: BaseFormatter,
) -> ProcessedDocument:
# pylint: disable=too-many-arguments
"""Black, isort and/or flynt formatting for modified chunks in a single file

:param root: Root directory for the relative path
:param relative_path_in_rev2: Relative path to a Python source code file
:param exclude: Files to exclude when running Black, ``isort`` or ``flynt``
:param revrange: The Git revisions to compare
:param black_config: Configuration to use for running Black
:param formatter: The code re-formatter to use
:return: Details about changes for the file

"""
Expand All @@ -148,13 +147,14 @@ def _modify_and_reformat_single_file( # pylint: disable=too-many-arguments
relative_path_in_rev2,
exclude.isort,
edited_linenums_differ,
black_config.get("config"),
black_config.get("line_length"),
formatter.get_config_path(),
formatter.get_line_length(),
)
has_isort_changes = rev2_isorted != rev2_content
# 2. run flynt (optional) on the isorted contents of each edited to-file
# 3. run black on the isorted and fstringified contents of each edited to-file
content_after_reformatting = _blacken_and_flynt_single_file(
# 3. run a re-formatter on the isorted and fstringified contents of each edited
# to-file
content_after_reformatting = _reformat_and_flynt_single_file(
root,
relative_path_in_rev2,
get_path_in_repo(relative_path_in_rev2),
Expand All @@ -163,13 +163,12 @@ def _modify_and_reformat_single_file( # pylint: disable=too-many-arguments
rev2_content,
rev2_isorted,
has_isort_changes,
black_config,
formatter,
)
return absolute_path_in_rev2, rev2_content, content_after_reformatting


def _blacken_and_flynt_single_file(
# pylint: disable=too-many-arguments,too-many-locals
def _reformat_and_flynt_single_file( # noqa: PLR0913
root: Path,
relative_path_in_rev2: Path,
relative_path_in_repo: Path,
Expand All @@ -178,8 +177,9 @@ def _blacken_and_flynt_single_file(
rev2_content: TextDocument,
rev2_isorted: TextDocument,
has_isort_changes: bool,
black_config: BlackConfig,
formatter: BaseFormatter,
) -> TextDocument:
# pylint: disable=too-many-arguments
"""In a Python file, reformat chunks with edits since the last commit using Black

:param root: The common root of all files to reformat
Expand All @@ -192,7 +192,7 @@ def _blacken_and_flynt_single_file(
:param rev2_content: Contents of the file at ``revrange.rev2``
:param rev2_isorted: Contents of the file after optional import sorting
:param has_isort_changes: ``True`` if ``isort`` was run and modified the file
:param black_config: Configuration to use for running Black
:param formatter: The code re-formatter to use
:return: Contents of the file after reformatting
:raise: NotEquivalentError

Expand All @@ -214,14 +214,18 @@ def _blacken_and_flynt_single_file(
len(fstringified.lines),
"some" if has_fstring_changes else "no",
)
# 3. run black on the isorted and fstringified contents of each edited to-file
formatted = _maybe_blacken_single_file(
relative_path_in_rev2, exclude.black, fstringified, black_config
# 3. run the code re-formatter on the isorted and fstringified contents of each
# edited to-file
formatted = _maybe_reformat_single_file(
relative_path_in_rev2, exclude.formatter, fstringified, formatter
)
logger.debug(
"Black reformat resulted in %s lines, with %s changes from reformatting",
len(formatted.lines),
"Running %r by %s.%s resulted in %s changed lines within a total of %s lines",
formatter.name,
formatter.__module__,
type(formatter).__name__,
"no" if formatted == fstringified else "some",
len(formatted.lines),
)

# 4. get a diff between the edited to-file and the processed content
Expand Down Expand Up @@ -270,26 +274,26 @@ def _maybe_flynt_single_file(
return apply_flynt(rev2_isorted, relpath_in_rev2, edited_linenums_differ)


def _maybe_blacken_single_file(
def _maybe_reformat_single_file(
relpath_in_rev2: Path,
exclude: Collection[str],
fstringified: TextDocument,
black_config: BlackConfig,
formatter: BaseFormatter,
) -> TextDocument:
"""Format Python source code with Black if the source code file path isn't excluded
"""Re-format Python source code if the source code file path isn't excluded.

:param relpath_in_rev2: Relative path to a Python source code file. Possibly a
VSCode ``.py.<HASH>.tmp`` file in the working tree.
:param exclude: Files to exclude when running Black
:param exclude: Files to exclude when running the re-formatter
:param fstringified: Contents of the file after optional import sorting and flynt
:param black_config: Configuration to use for running Black
:param formatter: The code re-formatter to use
:return: Python source code after reformatting

"""
if glob_any(relpath_in_rev2, exclude):
# File was excluded by Black configuration, don't reformat
return fstringified
return run_black(fstringified, black_config)
return formatter.run(fstringified)


def _drop_changes_on_unedited_lines(
Expand Down Expand Up @@ -459,7 +463,8 @@ def main( # noqa: C901,PLR0912,PLR0915

1. run isort on each edited file (optional)
2. run flynt (optional) on the isorted contents of each edited to-file
3. run black on the isorted and fstringified contents of each edited to-file
3. run a code re-formatter on the isorted and fstringified contents of each edited
to-file
4. get a diff between the edited to-file and the processed content
5. convert the diff into chunks, keeping original and reformatted content for each
chunk
Expand Down Expand Up @@ -511,19 +516,8 @@ def main( # noqa: C901,PLR0912,PLR0915
f"{get_extra_instruction('flynt')} to use the `--flynt` option."
)

black_config = read_black_config(tuple(args.src), args.config)
if args.config:
black_config["config"] = args.config
if args.line_length:
black_config["line_length"] = args.line_length
if args.target_version:
black_config["target_version"] = {args.target_version}
if args.skip_string_normalization is not None:
black_config["skip_string_normalization"] = args.skip_string_normalization
if args.skip_magic_trailing_comma is not None:
black_config["skip_magic_trailing_comma"] = args.skip_magic_trailing_comma
if args.preview:
black_config["preview"] = args.preview
formatter = create_formatter(args.formatter)
formatter.read_config(tuple(args.src), args)

paths, common_root = resolve_paths(args.stdin_filename, args.src)
# `common_root` is now the common root of given paths,
Expand Down Expand Up @@ -570,8 +564,8 @@ def main( # noqa: C901,PLR0912,PLR0915
else common_root
)
# These paths are relative to `common_root`:
files_to_process = filter_python_files(paths, common_root_, {})
files_to_blacken = filter_python_files(paths, common_root_, black_config)
files_to_process = filter_python_files(paths, common_root_, NoneFormatter())
files_to_reformat = filter_python_files(paths, common_root_, formatter)

# Now decide which files to reformat (Black & isort). Note that this doesn't apply
# to linting.
Expand All @@ -580,7 +574,7 @@ def main( # noqa: C901,PLR0912,PLR0915
# modified or not. Paths have previously been validated to contain exactly one
# existing file.
changed_files_to_reformat = files_to_process
black_exclude = set()
formatter_exclude = set()
else:
# In other modes, only reformat files which have been modified.
if git_is_repository(common_root):
Expand All @@ -595,10 +589,10 @@ def main( # noqa: C901,PLR0912,PLR0915

else:
changed_files_to_reformat = files_to_process
black_exclude = {
formatter_exclude = {
str(path)
for path in changed_files_to_reformat
if path not in files_to_blacken
if path not in files_to_reformat
}
use_color = should_use_color(config["color"])
formatting_failures_on_modified_lines = False
Expand All @@ -607,12 +601,12 @@ def main( # noqa: C901,PLR0912,PLR0915
common_root,
changed_files_to_reformat,
Exclusions(
black=black_exclude,
formatter=formatter_exclude,
isort=set() if args.isort else {"**/*"},
flynt=set() if args.flynt else {"**/*"},
),
revrange,
black_config,
formatter,
report_unmodified=output_mode == OutputMode.CONTENT,
workers=config["workers"],
),
Expand Down
Loading
Loading