Skip to content

Commit

Permalink
refactor: Black as a plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
akaihola committed Sep 25, 2024
1 parent 332ecd5 commit 6e8ff91
Show file tree
Hide file tree
Showing 17 changed files with 352 additions and 158 deletions.
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
94 changes: 44 additions & 50 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 @@ -17,8 +17,9 @@
from darker.diff import diff_chunks
from darker.exceptions import DependencyError, MissingPackageError
from darker.files import filter_python_files
from darker.formatters.black_formatter import read_black_config, run_black
from darker.formatters.formatter_config import BlackConfig
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 @@ -56,12 +57,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 @@ -78,7 +79,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 @@ -97,7 +98,7 @@ def format_edited_parts( # pylint: disable=too-many-arguments
edited_linenums_differ,
exclude,
revrange,
black_config,
formatter,
)
futures.append(future)

Expand All @@ -111,21 +112,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 @@ -144,13 +146,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 @@ -159,13 +162,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 @@ -174,8 +176,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 @@ -188,7 +191,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 @@ -210,9 +213,10 @@ 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",
Expand Down Expand Up @@ -266,26 +270,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 @@ -455,7 +459,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 @@ -507,19 +512,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)

Check failure on line 516 in src/darker/__main__.py

View workflow job for this annotation

GitHub Actions / Mypy

src/darker/__main__.py#L516

"BaseFormatter" has no attribute "read_config" [attr-defined]

paths, common_root = resolve_paths(args.stdin_filename, args.src)
# `common_root` is now the common root of given paths,
Expand Down Expand Up @@ -566,8 +560,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 @@ -576,7 +570,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 @@ -591,10 +585,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 @@ -603,12 +597,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
5 changes: 3 additions & 2 deletions src/darker/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
DarkerConfig,
OutputMode,
)
from darker.formatters import get_formatter_names
from darker.version import __version__
from darkgraylib.command_line import add_parser_argument
from darkgraylib.config import ConfigurationError
Expand Down Expand Up @@ -84,10 +85,10 @@ def make_argument_parser(require_src: bool) -> ArgumentParser:
choices=[v.name.lower() for v in TargetVersion],
)
add_arg(
"Formatter to use for reformatting code",
hlp.FORMATTER,
"--formatter",
default="black",
choices=["black"],
choices=get_formatter_names(),
metavar="FORMATTER",
)
return parser
Expand Down
2 changes: 1 addition & 1 deletion src/darker/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,6 @@ class Exclusions:
"""

black: set[str] = field(default_factory=set)
formatter: set[str] = field(default_factory=set)
isort: set[str] = field(default_factory=set)
flynt: set[str] = field(default_factory=set)
20 changes: 10 additions & 10 deletions src/darker/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import inspect
from pathlib import Path
from typing import Collection, Optional, Set, Tuple
from typing import Collection, Optional, Tuple

from black import (
DEFAULT_EXCLUDES,
Expand All @@ -14,7 +14,7 @@
re_compile_maybe_verbose,
)

from darker.formatters.formatter_config import BlackConfig
from darker.formatters.base_formatter import BaseFormatter
from darkgraylib.files import find_project_root


Expand All @@ -41,14 +41,14 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
def filter_python_files(
paths: Collection[Path], # pylint: disable=unsubscriptable-object
root: Path,
black_config: BlackConfig,
) -> Set[Path]:
"""Get Python files and explicitly listed files not excluded by Black's config
formatter: BaseFormatter,
) -> set[Path]:
"""Get Python files and explicitly listed files not excluded by Black's config.
:param paths: Relative file/directory paths from CWD to Python sources
:param root: A common root directory for all ``paths``
:param black_config: Black configuration which contains the exclude options read
from Black's configuration files
:param formatter: The code re-formatter which provides the configuration containing
the exclude options
:return: Paths of files which should be reformatted according to
``black_config``, relative to ``root``.
Expand All @@ -70,9 +70,9 @@ def filter_python_files(
directories,
root,
include=DEFAULT_INCLUDE_RE,
exclude=black_config.get("exclude", DEFAULT_EXCLUDE_RE),
extend_exclude=black_config.get("extend_exclude"),
force_exclude=black_config.get("force_exclude"),
exclude=formatter.get_exclude(DEFAULT_EXCLUDE_RE),
extend_exclude=formatter.get_extend_exclude(),
force_exclude=formatter.get_force_exclude(),
report=Report(),
**kwargs, # type: ignore[arg-type]
)
Expand Down
37 changes: 37 additions & 0 deletions src/darker/formatters/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
"""Built-in code re-formatter plugins."""

from __future__ import annotations

import sys
from importlib.metadata import EntryPoint, entry_points
from typing import cast

from darker.formatters.base_formatter import BaseFormatter

ENTRY_POINT_GROUP = "darker.formatter"


def get_formatter_entry_points(name: str | None = None) -> tuple[EntryPoint, ...]:
"""Get the entry points of all built-in code re-formatter plugins."""
if sys.version_info < (3, 10):
return tuple(
ep
for ep in entry_points()[ENTRY_POINT_GROUP]
if not name or ep.name == name
)
if name:
result = entry_points(group=ENTRY_POINT_GROUP, name=name)
else:
result = entry_points(group=ENTRY_POINT_GROUP)
return cast(tuple[EntryPoint, ...], result)


def get_formatter_names() -> list[str]:
"""Get the names of all built-in code re-formatter plugins."""
return [ep.name for ep in get_formatter_entry_points()]


def create_formatter(name: str) -> BaseFormatter:
"""Create a code re-formatter plugin instance by name."""
matching_entry_points = get_formatter_entry_points(name)
formatter_class = next(iter(matching_entry_points)).load()
return cast(BaseFormatter, formatter_class())
Loading

0 comments on commit 6e8ff91

Please sign in to comment.