Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/pysen/error_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def parse_error_diffs(
logger: Optional[logging.Logger] = None,
) -> Iterable[Diagnostic]:
"""
Compatible with isort, black
Compatible with isort, black, ruff
"""

def _is_changed(line: unidiff.patch.Line) -> bool:
Expand Down
79 changes: 79 additions & 0 deletions src/pysen/ext/ruff_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import copy
import dataclasses
import enum
import functools
import pathlib
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple

from pysen import process_utils
from pysen.error_lines import parse_error_diffs
from pysen.path import change_dir
from pysen.reporter import Reporter


def _run_command_and_report(
command: List[str], reporter: Reporter, base_dir: pathlib.Path
) -> int:
def _parse_file_path(file_path: str) -> pathlib.Path:
return base_dir / pathlib.Path(file_path.split(" ")[0])

with change_dir(base_dir):
ret, stdout, _ = process_utils.run(
process_utils.add_python_executable(*command), reporter
)

diagnostics = parse_error_diffs(stdout, _parse_file_path, logger=reporter.logger)
reporter.report_diagnostics(list(diagnostics))

return ret


def _create_ruff_command(
setting_path: pathlib.Path,
subcommand: str,
targets: List[str],
flags: List[str],
) -> List[str]:
return ["ruff", "--config", str(setting_path), subcommand] + targets + flags


def run(
reporter: Reporter,
base_dir: pathlib.Path,
setting_path: pathlib.Path,
sources: Iterable[pathlib.Path],
inplace_edit: bool,
) -> int:
targets = [str(d) for d in sources]
if len(targets) == 0:
return 0

if inplace_edit:
# - `ruff check --fix`
# - `ruff format`
# this is horrible CLI design since `pysen run format` would actually "fix" violations reported in `pysen run lint`
lint_ret = _run_command_and_report(
_create_ruff_command(setting_path, "check", targets, ["--fix"]),
reporter,
base_dir,
)
format_ret = _run_command_and_report(
_create_ruff_command(setting_path, "format", targets, []),
reporter,
base_dir,
)
return max(lint_ret, format_ret)
else:
# - `ruff check --diff` to lint code
# - `ruff format --check` to show formatting diffs
lint_ret = _run_command_and_report(
_create_ruff_command(setting_path, "check", targets, ["--diff"]),
reporter,
base_dir,
)
format_ret = _run_command_and_report(
_create_ruff_command(setting_path, "format", targets, ["--diff"]),
reporter,
base_dir,
)
return max(lint_ret, format_ret)
15 changes: 15 additions & 0 deletions src/pysen/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
MypyTarget,
)
from .py_version import PythonVersion
from .ruff import Ruff, RuffSetting
from .source import Source


Expand Down Expand Up @@ -43,6 +44,7 @@ def get_setting(self) -> MypySetting:

@dataclasses.dataclass
class ConfigureLintOptions:
enable_ruff: Optional[bool] = None
enable_black: Optional[bool] = None
enable_flake8: Optional[bool] = None
enable_isort: Optional[bool] = None
Expand Down Expand Up @@ -72,6 +74,19 @@ def configure_lint(options: ConfigureLintOptions) -> List[ComponentBase]:

line_length = options.line_length or 88

if options.enable_ruff:
ruff_setting = RuffSetting.default()
ruff_setting.line_length = line_length
flake8_setting = Flake8Setting.default().to_black_compatible()
ruff_setting.select = flake8_setting.select
ruff_setting.ignore = flake8_setting.ignore
if options.isort_known_first_party is not None:
ruff_setting.known_first_party = list(options.isort_known_first_party)
if options.isort_known_third_party is not None:
ruff_setting.known_third_party = list(options.isort_known_third_party)
ruff = Ruff(setting=ruff_setting, source=options.source)
components.append(ruff)

# NOTE: `isort` may format code in a way that violates `black` rules
# Apply `isort` after `black` to avoid such violation
if options.enable_isort:
Expand Down
136 changes: 136 additions & 0 deletions src/pysen/ruff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import pathlib
from dataclasses import dataclass
from typing import Any, DefaultDict, Dict, List, Optional, Sequence, Tuple

from pysen.command import CommandBase
from pysen.component import LintComponentBase
from pysen.ext import ruff_wrapper
from pysen.lint_command import LintCommandBase
from pysen.path import resolve_path
from pysen.reporter import Reporter
from pysen.source import PythonFileFilter, Source

from .flake8 import Flake8Setting
from .isort import IsortSetting
from .runner_options import PathContext, RunOptions
from .setting import SettingBase, SettingFile

_SettingFileName = "pyproject.toml"


@dataclass
class RuffSetting(SettingBase):
line_length: int = 88
select: Optional[List[str]] = None
ignore: Optional[List[str]] = None
known_first_party: Optional[List[str]] = None
known_third_party: Optional[List[str]] = None

@staticmethod
def default() -> "RuffSetting":
return RuffSetting()

def export_top_level(self) -> Tuple[List[str], Dict[str, Any]]:
section_name = ["tool", "ruff"]
entries: Dict[str, Any] = {
"line-length": self.line_length,
}
if self.select is not None:
entries["select"] = self.select
if self.ignore is not None:
entries["ignore"] = self.ignore
return section_name, entries

def export_isort(self) -> Tuple[List[str], Dict[str, Any]]:
section_name = ["tool", "ruff", "isort"]
entries: Dict[str, Any] = {}
if self.known_first_party is not None:
entries["known-first-party"] = self.known_first_party
if self.known_third_party is not None:
entries["known-third-party"] = self.known_third_party
return section_name, entries


class RuffCommand(LintCommandBase):
def __init__(
self,
name: str,
paths: PathContext,
source: Source,
inplace_edit: bool,
) -> None:
super().__init__(paths.base_dir, source)
self._name = name
self._setting_path = resolve_path(paths.settings_dir, _SettingFileName)
self._inplace_edit = inplace_edit

@property
def name(self) -> str:
return self._name

@property
def has_side_effects(self) -> bool:
return self._inplace_edit

def __call__(self, reporter: Reporter) -> int:
sources = self._get_sources(reporter, PythonFileFilter)
reporter.logger.info(f"Checking {len(sources)} files")
return ruff_wrapper.run(
reporter, self.base_dir, self._setting_path, sources, self._inplace_edit
)

def run_files(self, reporter: Reporter, files: Sequence[pathlib.Path]) -> int:
covered_files = self._get_covered_files(reporter, files, PythonFileFilter)

if len(covered_files) == 0:
return 0

return ruff_wrapper.run(
reporter,
self.base_dir,
self._setting_path,
files,
self._inplace_edit,
)


class Ruff(LintComponentBase):
def __init__(
self,
name: str = "ruff",
setting: Optional[RuffSetting] = None,
source: Optional[Source] = None,
) -> None:
super().__init__(name, source)
self._setting = setting or RuffSetting.default()

@property
def setting(self) -> RuffSetting:
return self._setting

@setting.setter
def setting(self, value: RuffSetting) -> None:
self._setting = value

def export_settings(
self,
paths: PathContext,
files: DefaultDict[str, SettingFile],
) -> None:
setting_file = files[_SettingFileName]
setting_file.set_section(*self._setting.export_top_level())
setting_file.set_section(*self._setting.export_isort())

@property
def targets(self) -> Sequence[str]:
return ["lint", "format"]

def create_command(
self, target: str, paths: PathContext, options: RunOptions
) -> CommandBase:
if target == "lint":
return RuffCommand(self.name, paths, self.source, False)
elif target == "format":
return RuffCommand(self.name, paths, self.source, True)

raise AssertionError(f"unknown {target}")
Loading