diff --git a/extensions/fine_python_flake8/fine_python_flake8/action.py b/extensions/fine_python_flake8/fine_python_flake8/action.py index 8e54c4c..9d9f9d3 100644 --- a/extensions/fine_python_flake8/fine_python_flake8/action.py +++ b/extensions/fine_python_flake8/fine_python_flake8/action.py @@ -58,6 +58,7 @@ def run_flake8_on_single_file( max_line_length=config.max_line_length, extend_select=config.extend_select, extend_ignore=config.extend_ignore, + select=config.select ) decider = style_guide.DecisionEngine(guide.options) @@ -110,6 +111,7 @@ def run_flake8_on_single_file( @dataclasses.dataclass class Flake8LintHandlerConfig(code_action.ActionHandlerConfig): max_line_length: int = 79 + select: list[str] | None = None extend_select: list[str] | None = None extend_ignore: list[str] | None = None diff --git a/extensions/fine_python_flake8/pyproject.toml b/extensions/fine_python_flake8/pyproject.toml index 50e52c7..a8c9371 100644 --- a/extensions/fine_python_flake8/pyproject.toml +++ b/extensions/fine_python_flake8/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fine_python_flake8" -version = "0.2.0" +version = "0.2.1" description = "" authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] readme = "README.md" diff --git a/extensions/fine_python_ruff/fine_python_ruff/__init__.py b/extensions/fine_python_ruff/fine_python_ruff/__init__.py index dccf650..3e77465 100644 --- a/extensions/fine_python_ruff/fine_python_ruff/__init__.py +++ b/extensions/fine_python_ruff/fine_python_ruff/__init__.py @@ -1,6 +1,9 @@ from .format_handler import RuffFormatHandler, RuffFormatHandlerConfig +from .lint_handler import RuffLintHandler, RuffLintHandlerConfig __all__ = [ "RuffFormatHandler", - "RuffFormatHandlerConfig" + "RuffFormatHandlerConfig", + "RuffLintHandler", + "RuffLintHandlerConfig", ] diff --git a/extensions/fine_python_ruff/fine_python_ruff/lint_handler.py b/extensions/fine_python_ruff/fine_python_ruff/lint_handler.py new file mode 100644 index 0000000..1b138c0 --- /dev/null +++ b/extensions/fine_python_ruff/fine_python_ruff/lint_handler.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +import dataclasses +import json +import sys +from pathlib import Path + +from finecode_extension_api import code_action +from finecode_extension_api.actions import lint as lint_action +from finecode_extension_api.interfaces import icache, icommandrunner, ilogger, ifilemanager + + +@dataclasses.dataclass +class RuffLintHandlerConfig(code_action.ActionHandlerConfig): + line_length: int = 88 + target_version: str = "py38" + select: list[str] | None = None # Rules to enable + ignore: list[str] | None = None # Rules to disable + preview: bool = False + + +class RuffLintHandler( + code_action.ActionHandler[lint_action.LintAction, RuffLintHandlerConfig] +): + CACHE_KEY = "RuffLinter" + + def __init__( + self, + config: RuffLintHandlerConfig, + cache: icache.ICache, + logger: ilogger.ILogger, + file_manager: ifilemanager.IFileManager, + command_runner: icommandrunner.ICommandRunner, + ) -> None: + self.config = config + self.cache = cache + self.logger = logger + self.file_manager = file_manager + self.command_runner = command_runner + + self.ruff_bin_path = Path(sys.executable).parent / "ruff" + + async def run_on_single_file( + self, file_path: Path + ) -> lint_action.LintRunResult: + messages = {} + try: + cached_lint_messages = await self.cache.get_file_cache( + file_path, self.CACHE_KEY + ) + messages[str(file_path)] = cached_lint_messages + return lint_action.LintRunResult(messages=messages) + except icache.CacheMissException: + pass + + file_version = await self.file_manager.get_file_version(file_path) + file_content = await self.file_manager.get_content(file_path) + lint_messages = await self.run_ruff_lint_on_single_file(file_path, file_content) + messages[str(file_path)] = lint_messages + await self.cache.save_file_cache( + file_path, file_version, self.CACHE_KEY, lint_messages + ) + + return lint_action.LintRunResult(messages=messages) + + async def run( + self, + payload: lint_action.LintRunPayload, + run_context: code_action.RunActionWithPartialResultsContext, + ) -> None: + file_paths = [file_path async for file_path in payload] + + for file_path in file_paths: + run_context.partial_result_scheduler.schedule( + file_path, + self.run_on_single_file(file_path), + ) + + async def run_ruff_lint_on_single_file( + self, + file_path: Path, + file_content: str, + ) -> list[lint_action.LintMessage]: + """Run ruff linting on a single file""" + lint_messages: list[lint_action.LintMessage] = [] + + # Build ruff check command + cmd = [ + str(self.ruff_bin_path), + "check", + "--output-format", + "json", + "--line-length", + str(self.config.line_length), + "--target-version", + self.config.target_version, + "--stdin-filename", + str(file_path), + ] + + if self.config.select: + cmd.extend(["--select", ",".join(self.config.select)]) + if self.config.ignore: + cmd.extend(["--ignore", ",".join(self.config.ignore)]) + if self.config.preview: + cmd.append("--preview") + + cmd_str = " ".join(cmd) + ruff_process = await self.command_runner.run( + cmd_str, + ) + + ruff_process.write_to_stdin(file_content) + ruff_process.close_stdin() # Signal EOF + + await ruff_process.wait_for_end() + + output = ruff_process.get_output() + try: + ruff_results = json.loads(output) + for violation in ruff_results: + lint_message = map_ruff_violation_to_lint_message(violation) + lint_messages.append(lint_message) + except json.JSONDecodeError: + raise code_action.ActionFailedException(f'Output of ruff is not json: {output}') + + return lint_messages + + +def map_ruff_violation_to_lint_message(violation: dict) -> lint_action.LintMessage: + """Map a ruff violation to a lint message""" + location = violation.get("location", {}) + end_location = violation.get("end_location", {}) + + # Extract line/column info (ruff uses 1-based indexing) + start_line = max(1, location.get("row", 1)) + start_column = max(0, location.get("column", 0)) + end_line = max(1, end_location.get("row", start_line + 1)) - 1 # Convert to 0-based + end_column = max(0, end_location.get("column", start_column)) + + # Determine severity based on rule code + code = violation.get("code", "") + code_description = violation.get("url", "") + if code.startswith(("E", "F")): # Error codes + severity = lint_action.LintMessageSeverity.ERROR + elif code.startswith("W"): # Warning codes + severity = lint_action.LintMessageSeverity.WARNING + else: + severity = lint_action.LintMessageSeverity.INFO + + return lint_action.LintMessage( + range=lint_action.Range( + start=lint_action.Position(line=start_line, character=start_column), + end=lint_action.Position(line=end_line, character=end_column), + ), + message=violation.get("message", ""), + code=code, + code_description=code_description, + source="ruff", + severity=severity, + ) diff --git a/finecode_dev_common_preset/pyproject.toml b/finecode_dev_common_preset/pyproject.toml index 74bdb9f..ed7a1df 100644 --- a/finecode_dev_common_preset/pyproject.toml +++ b/finecode_dev_common_preset/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" requires-python = ">=3.11, < 3.14" dependencies = [ "fine_python_aksem @ git+https://github.com/Aksem/fine_python_aksem.git", - "fine_python_recommended==0.2.*", + "fine_python_recommended==0.3.*", ] [tool.setuptools.package-data] diff --git a/finecode_dev_common_preset/src/finecode_dev_common_preset/preset.toml b/finecode_dev_common_preset/src/finecode_dev_common_preset/preset.toml index 189e726..0914817 100644 --- a/finecode_dev_common_preset/src/finecode_dev_common_preset/preset.toml +++ b/finecode_dev_common_preset/src/finecode_dev_common_preset/preset.toml @@ -4,10 +4,6 @@ presets = [ { source = "fine_python_aksem" }, ] -[[tool.finecode.action_handler]] -source = "fine_python_black.BlackFormatHandler" -config.preview = true - # in development finecode can only be started with local version of finecode_extension_runner, # otherwise version conflict occurs, because versions of finecode and # finecode_extension_runner must match diff --git a/finecode_extension_runner/src/finecode_extension_runner/_services/run_action.py b/finecode_extension_runner/src/finecode_extension_runner/_services/run_action.py index ddd3bbc..8d30970 100644 --- a/finecode_extension_runner/src/finecode_extension_runner/_services/run_action.py +++ b/finecode_extension_runner/src/finecode_extension_runner/_services/run_action.py @@ -544,11 +544,16 @@ async def run_subresult_coros_concurrently( coro_task = tg.create_task(coro) coros_tasks.append(coro_task) except ExceptionGroup as eg: - logger.error(f"R{run_id} | {eg}") + errors_str = "" for exc in eg.exceptions: - logger.exception(exc) + if isinstance(exc, code_action.ActionFailedException): + errors_str += exc.message + '.' + else: + logger.error("Unhandled exception:") + logger.exception(exc) + errors_str += str(exc) + '.' raise ActionFailedException( - f"Concurrent running action handlers of '{action_name}' failed(Run {run_id}). See logs for more details" + f"Concurrent running action handlers of '{action_name}' failed(Run {run_id}): {errors_str}" ) action_subresult: code_action.RunActionResult | None = None diff --git a/presets/fine_python_format/pyproject.toml b/presets/fine_python_format/pyproject.toml index 631796d..89eab10 100644 --- a/presets/fine_python_format/pyproject.toml +++ b/presets/fine_python_format/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fine_python_format" -version = "0.2.0" +version = "0.3.0" description = "" authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] readme = "README.md" diff --git a/presets/fine_python_lint/fine_python_lint/preset.toml b/presets/fine_python_lint/fine_python_lint/preset.toml index 0c97b72..ddbb75e 100644 --- a/presets/fine_python_lint/fine_python_lint/preset.toml +++ b/presets/fine_python_lint/fine_python_lint/preset.toml @@ -1,19 +1,26 @@ [tool.finecode.action.lint] source = "finecode_extension_api.actions.lint.LintAction" handlers = [ + { name = "ruff", source = "fine_python_ruff.RuffLintHandler", env = "dev_no_runtime", dependencies = [ + "fine_python_ruff==0.1.*", + ] }, { name = "flake8", source = "fine_python_flake8.Flake8LintHandler", env = "dev_no_runtime", dependencies = [ "fine_python_flake8==0.2.*", - "flake8-bugbear (>=24.12.12,<25.0.0)", ] }, { name = "mypy", source = "fine_python_mypy.MypyLintHandler", env = "dev_no_runtime", dependencies = [ "fine_python_mypy==0.2.*", ] }, ] +# flake8 is used only for custom rules, all standard rules are checked by ruff, but +# keep flake8 configuration if someone activates some rules or uses flake8 config +# parameters in their own rules [[tool.finecode.action_handler]] source = "fine_python_flake8.Flake8LintHandler" config.max_line_length = 80 -config.extend_select = ["B950"] -# W391 is not compatible with black, because black adds an empty line to the end of the file +# W391 is not compatible with black(and ruff formatter, which is compatible with black), +# because black adds an empty line to the end of the file # TODO: move in recommended config once config merging is implemented config.extend_ignore = ["E203", "E501", "E701", "W391"] +# disable all standard rules +config.select = [] diff --git a/presets/fine_python_lint/pyproject.toml b/presets/fine_python_lint/pyproject.toml index fbdf476..622b6d9 100644 --- a/presets/fine_python_lint/pyproject.toml +++ b/presets/fine_python_lint/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fine_python_lint" -version = "0.2.0" +version = "0.3.0" description = "" authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] readme = "README.md" diff --git a/presets/fine_python_recommended/pyproject.toml b/presets/fine_python_recommended/pyproject.toml index e653876..6d963c4 100644 --- a/presets/fine_python_recommended/pyproject.toml +++ b/presets/fine_python_recommended/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "fine_python_recommended" -version = "0.2.0" +version = "0.3.0" description = "" authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] readme = "README.md" requires-python = ">=3.11, < 3.14" -dependencies = ["fine_python_format==0.2.*", "fine_python_lint==0.2.*"] +dependencies = ["fine_python_format==0.3.*", "fine_python_lint==0.3.*"] [build-system] requires = ["setuptools>=64"]