Skip to content

Commit

Permalink
feat: add ruff code re-formatter plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
akaihola committed Sep 25, 2024
1 parent 960b585 commit 8a281eb
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 41 deletions.
11 changes: 8 additions & 3 deletions src/darker/files.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Helper functions for working with files and directories."""

from __future__ import annotations

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

from black import (
DEFAULT_EXCLUDES,
Expand All @@ -14,9 +15,13 @@
re_compile_maybe_verbose,
)

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

if TYPE_CHECKING:
from pathlib import Path

from darker.formatters.base_formatter import BaseFormatter


def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
"""Find the absolute filepath to a pyproject.toml if it exists"""
Expand Down
35 changes: 31 additions & 4 deletions src/darker/formatters/base_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Pattern
from typing import TYPE_CHECKING, Generic, Pattern, TypeVar

from darker.files import find_pyproject_toml
from darker.formatters.formatter_config import FormatterConfig

if TYPE_CHECKING:
from argparse import Namespace

from darker.formatters.formatter_config import FormatterConfig
from darkgraylib.utils import TextDocument


class BaseFormatter:
T = TypeVar("T", bound=FormatterConfig)


class HasConfig(Generic[T]): # pylint: disable=too-few-public-methods
"""Base class for code re-formatters."""

def __init__(self) -> None:
"""Initialize the code re-formatter plugin base class."""
self.config: FormatterConfig = {}
self.config = {} # type: ignore[var-annotated]


class BaseFormatter(HasConfig[FormatterConfig]):
"""Base class for code re-formatters."""

def read_config(self, src: tuple[str, ...], args: Namespace) -> None:
"""Read the formatter configuration from a configuration file
Expand All @@ -33,6 +42,21 @@ def run(self, content: TextDocument) -> TextDocument:
"""Reformat the content."""
raise NotImplementedError

def read_config(self, src: tuple[str, ...], args: Namespace) -> None:
"""Read code re-formatter configuration from a configuration file.
:param src: The source code files and directories to be processed by Darker
:param args: Command line arguments
"""
config_path = args.config or find_pyproject_toml(src)
if config_path:
self._read_config_file(config_path)
self._read_cli_args(args)

def _read_cli_args(self, args: Namespace) -> None:
pass

def get_config_path(self) -> str | None:
"""Get the path of the configuration file."""
raise NotImplementedError
Expand All @@ -58,3 +82,6 @@ def __eq__(self, other: object) -> bool:
if not isinstance(other, BaseFormatter):
return NotImplemented
return type(self) is type(other) and self.config == other.config

def _read_config_file(self, config_path: str) -> None:
pass
41 changes: 13 additions & 28 deletions src/darker/formatters/black_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
... ]
... )
First, :func:`run_black` uses Black to reformat the contents of a given file.
First, `BlackFormatter.run` uses Black to reformat the contents of a given file.
Reformatted lines are returned e.g.::
>>> from darker.formatters.black_formatter import BlackFormatter
Expand Down Expand Up @@ -48,16 +48,19 @@
)

from darker.files import find_pyproject_toml
from darker.formatters.base_formatter import BaseFormatter
from darker.formatters.base_formatter import BaseFormatter, HasConfig
from darker.formatters.formatter_config import (
BlackCompatibleConfig,
read_black_compatible_cli_args,
validate_target_versions,
)
from darkgraylib.config import ConfigurationError
from darkgraylib.utils import TextDocument

if TYPE_CHECKING:
from argparse import Namespace
from typing import Pattern

from darker.formatters.formatter_config import BlackConfig

__all__ = ["Mode"]

logger = logging.getLogger(__name__)
Expand All @@ -74,12 +77,10 @@ class BlackModeAttributes(TypedDict, total=False):
preview: bool


class BlackFormatter(BaseFormatter):
class BlackFormatter(BaseFormatter, HasConfig[BlackCompatibleConfig]):
"""Black code formatter plugin interface."""

def __init__(self) -> None: # pylint: disable=super-init-not-called
"""Initialize the Black code re-formatter plugin."""
self.config: BlackConfig = {}
config: BlackCompatibleConfig # type: ignore[assignment]

def read_config(self, src: tuple[str, ...], args: Namespace) -> None:
"""Read Black configuration from ``pyproject.toml``.
Expand Down Expand Up @@ -131,18 +132,7 @@ def _read_config_file(self, config_path: str) -> None: # noqa: C901
)

def _read_cli_args(self, args: Namespace) -> None:
if args.config:
self.config["config"] = args.config
if getattr(args, "line_length", None):
self.config["line_length"] = args.line_length
if getattr(args, "target_version", None):
self.config["target_version"] = {args.target_version}
if getattr(args, "skip_string_normalization", None) is not None:
self.config["skip_string_normalization"] = args.skip_string_normalization
if getattr(args, "skip_magic_trailing_comma", None) is not None:
self.config["skip_magic_trailing_comma"] = args.skip_magic_trailing_comma
if getattr(args, "preview", None):
self.config["preview"] = args.preview
return read_black_compatible_cli_args(args, self.config)

def run(self, content: TextDocument) -> TextDocument:
"""Run the Black code re-formatter for the Python source code given as a string.
Expand All @@ -158,15 +148,10 @@ def run(self, content: TextDocument) -> TextDocument:
if "line_length" in self.config:
mode["line_length"] = self.config["line_length"]
if "target_version" in self.config:
if isinstance(self.config["target_version"], set):
target_versions_in = self.config["target_version"]
else:
target_versions_in = {self.config["target_version"]}
all_target_versions = {tgt_v.name.lower(): tgt_v for tgt_v in TargetVersion}
bad_target_versions = target_versions_in - set(all_target_versions)
if bad_target_versions:
message = f"Invalid target version(s) {bad_target_versions}"
raise ConfigurationError(message)
target_versions_in = validate_target_versions(
self.config["target_version"], all_target_versions
)
mode["target_versions"] = {
all_target_versions[n] for n in target_versions_in
}
Expand Down
60 changes: 56 additions & 4 deletions src/darker/formatters/formatter_config.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,74 @@
"""Code re-formatter plugin configuration type definitions."""

from __future__ import annotations

import re
from decimal import Decimal
from re import Pattern
from typing import Set, TypedDict, Union
from typing import TYPE_CHECKING, Iterable, TypedDict

from darkgraylib.config import ConfigurationError

if TYPE_CHECKING:
from argparse import Namespace


class FormatterConfig(TypedDict):
"""Base class for code re-formatter configuration."""


class BlackConfig(TypedDict, total=False):
"""Type definition for Black configuration dictionaries"""
def validate_target_versions(
value: str | set[str], valid_target_versions: Iterable[str]
) -> set[str]:
"""Validate the target-version configuration option value."""
target_versions_in = {value} if isinstance(value, str) else value
if not isinstance(value, (str, set)):
message = f"Invalid target version(s) {value!r}" # type: ignore[unreachable]
raise ConfigurationError(message)
bad_target_versions = target_versions_in - set(valid_target_versions)
if bad_target_versions:
message = f"Invalid target version(s) {bad_target_versions}"
raise ConfigurationError(message)
return target_versions_in


get_version_num = re.compile(r"\d+").search


def get_minimum_target_version(target_versions: set[str]) -> str:
"""Get the minimum target version from a set of target versions."""
nums_and_tgts = ((get_version_num(tgt), tgt) for tgt in target_versions)
matches = ((Decimal(match.group()), tgt) for match, tgt in nums_and_tgts if match)
return min(matches)[1]


class BlackCompatibleConfig(FormatterConfig, total=False):
"""Type definition for configuration dictionaries of Black compatible formatters."""

config: str
exclude: Pattern[str]
extend_exclude: Pattern[str]
force_exclude: Pattern[str]
target_version: Union[str, Set[str]]
target_version: str | set[str]
line_length: int
skip_string_normalization: bool
skip_magic_trailing_comma: bool
preview: bool


def read_black_compatible_cli_args(
args: Namespace, config: BlackCompatibleConfig
) -> None:
"""Read Black-compatible configuration from command line arguments."""
if args.config:
config["config"] = args.config
if getattr(args, "line_length", None):
config["line_length"] = args.line_length
if getattr(args, "target_version", None):
config["target_version"] = {args.target_version}
if getattr(args, "skip_string_normalization", None) is not None:
config["skip_string_normalization"] = args.skip_string_normalization
if getattr(args, "skip_magic_trailing_comma", None) is not None:
config["skip_magic_trailing_comma"] = args.skip_magic_trailing_comma
if getattr(args, "preview", None):
config["preview"] = args.preview
Loading

0 comments on commit 8a281eb

Please sign in to comment.