Skip to content

Commit b3fcc57

Browse files
authored
Merge pull request #16 from finecode-dev/feature/ruff-lint
Ruff lint handler
2 parents 81daee4 + a8cf7b0 commit b3fcc57

File tree

11 files changed

+191
-17
lines changed

11 files changed

+191
-17
lines changed

extensions/fine_python_flake8/fine_python_flake8/action.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def run_flake8_on_single_file(
5858
max_line_length=config.max_line_length,
5959
extend_select=config.extend_select,
6060
extend_ignore=config.extend_ignore,
61+
select=config.select
6162
)
6263
decider = style_guide.DecisionEngine(guide.options)
6364

@@ -110,6 +111,7 @@ def run_flake8_on_single_file(
110111
@dataclasses.dataclass
111112
class Flake8LintHandlerConfig(code_action.ActionHandlerConfig):
112113
max_line_length: int = 79
114+
select: list[str] | None = None
113115
extend_select: list[str] | None = None
114116
extend_ignore: list[str] | None = None
115117

extensions/fine_python_flake8/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fine_python_flake8"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
66
readme = "README.md"
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from .format_handler import RuffFormatHandler, RuffFormatHandlerConfig
2+
from .lint_handler import RuffLintHandler, RuffLintHandlerConfig
23

34
__all__ = [
45
"RuffFormatHandler",
5-
"RuffFormatHandlerConfig"
6+
"RuffFormatHandlerConfig",
7+
"RuffLintHandler",
8+
"RuffLintHandlerConfig",
69
]
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
import json
5+
import sys
6+
from pathlib import Path
7+
8+
from finecode_extension_api import code_action
9+
from finecode_extension_api.actions import lint as lint_action
10+
from finecode_extension_api.interfaces import icache, icommandrunner, ilogger, ifilemanager
11+
12+
13+
@dataclasses.dataclass
14+
class RuffLintHandlerConfig(code_action.ActionHandlerConfig):
15+
line_length: int = 88
16+
target_version: str = "py38"
17+
select: list[str] | None = None # Rules to enable
18+
ignore: list[str] | None = None # Rules to disable
19+
preview: bool = False
20+
21+
22+
class RuffLintHandler(
23+
code_action.ActionHandler[lint_action.LintAction, RuffLintHandlerConfig]
24+
):
25+
CACHE_KEY = "RuffLinter"
26+
27+
def __init__(
28+
self,
29+
config: RuffLintHandlerConfig,
30+
cache: icache.ICache,
31+
logger: ilogger.ILogger,
32+
file_manager: ifilemanager.IFileManager,
33+
command_runner: icommandrunner.ICommandRunner,
34+
) -> None:
35+
self.config = config
36+
self.cache = cache
37+
self.logger = logger
38+
self.file_manager = file_manager
39+
self.command_runner = command_runner
40+
41+
self.ruff_bin_path = Path(sys.executable).parent / "ruff"
42+
43+
async def run_on_single_file(
44+
self, file_path: Path
45+
) -> lint_action.LintRunResult:
46+
messages = {}
47+
try:
48+
cached_lint_messages = await self.cache.get_file_cache(
49+
file_path, self.CACHE_KEY
50+
)
51+
messages[str(file_path)] = cached_lint_messages
52+
return lint_action.LintRunResult(messages=messages)
53+
except icache.CacheMissException:
54+
pass
55+
56+
file_version = await self.file_manager.get_file_version(file_path)
57+
file_content = await self.file_manager.get_content(file_path)
58+
lint_messages = await self.run_ruff_lint_on_single_file(file_path, file_content)
59+
messages[str(file_path)] = lint_messages
60+
await self.cache.save_file_cache(
61+
file_path, file_version, self.CACHE_KEY, lint_messages
62+
)
63+
64+
return lint_action.LintRunResult(messages=messages)
65+
66+
async def run(
67+
self,
68+
payload: lint_action.LintRunPayload,
69+
run_context: code_action.RunActionWithPartialResultsContext,
70+
) -> None:
71+
file_paths = [file_path async for file_path in payload]
72+
73+
for file_path in file_paths:
74+
run_context.partial_result_scheduler.schedule(
75+
file_path,
76+
self.run_on_single_file(file_path),
77+
)
78+
79+
async def run_ruff_lint_on_single_file(
80+
self,
81+
file_path: Path,
82+
file_content: str,
83+
) -> list[lint_action.LintMessage]:
84+
"""Run ruff linting on a single file"""
85+
lint_messages: list[lint_action.LintMessage] = []
86+
87+
# Build ruff check command
88+
cmd = [
89+
str(self.ruff_bin_path),
90+
"check",
91+
"--output-format",
92+
"json",
93+
"--line-length",
94+
str(self.config.line_length),
95+
"--target-version",
96+
self.config.target_version,
97+
"--stdin-filename",
98+
str(file_path),
99+
]
100+
101+
if self.config.select:
102+
cmd.extend(["--select", ",".join(self.config.select)])
103+
if self.config.ignore:
104+
cmd.extend(["--ignore", ",".join(self.config.ignore)])
105+
if self.config.preview:
106+
cmd.append("--preview")
107+
108+
cmd_str = " ".join(cmd)
109+
ruff_process = await self.command_runner.run(
110+
cmd_str,
111+
)
112+
113+
ruff_process.write_to_stdin(file_content)
114+
ruff_process.close_stdin() # Signal EOF
115+
116+
await ruff_process.wait_for_end()
117+
118+
output = ruff_process.get_output()
119+
try:
120+
ruff_results = json.loads(output)
121+
for violation in ruff_results:
122+
lint_message = map_ruff_violation_to_lint_message(violation)
123+
lint_messages.append(lint_message)
124+
except json.JSONDecodeError:
125+
raise code_action.ActionFailedException(f'Output of ruff is not json: {output}')
126+
127+
return lint_messages
128+
129+
130+
def map_ruff_violation_to_lint_message(violation: dict) -> lint_action.LintMessage:
131+
"""Map a ruff violation to a lint message"""
132+
location = violation.get("location", {})
133+
end_location = violation.get("end_location", {})
134+
135+
# Extract line/column info (ruff uses 1-based indexing)
136+
start_line = max(1, location.get("row", 1))
137+
start_column = max(0, location.get("column", 0))
138+
end_line = max(1, end_location.get("row", start_line + 1)) - 1 # Convert to 0-based
139+
end_column = max(0, end_location.get("column", start_column))
140+
141+
# Determine severity based on rule code
142+
code = violation.get("code", "")
143+
code_description = violation.get("url", "")
144+
if code.startswith(("E", "F")): # Error codes
145+
severity = lint_action.LintMessageSeverity.ERROR
146+
elif code.startswith("W"): # Warning codes
147+
severity = lint_action.LintMessageSeverity.WARNING
148+
else:
149+
severity = lint_action.LintMessageSeverity.INFO
150+
151+
return lint_action.LintMessage(
152+
range=lint_action.Range(
153+
start=lint_action.Position(line=start_line, character=start_column),
154+
end=lint_action.Position(line=end_line, character=end_column),
155+
),
156+
message=violation.get("message", ""),
157+
code=code,
158+
code_description=code_description,
159+
source="ruff",
160+
severity=severity,
161+
)

finecode_dev_common_preset/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ readme = "README.md"
77
requires-python = ">=3.11, < 3.14"
88
dependencies = [
99
"fine_python_aksem @ git+https://github.com/Aksem/fine_python_aksem.git",
10-
"fine_python_recommended==0.2.*",
10+
"fine_python_recommended==0.3.*",
1111
]
1212

1313
[tool.setuptools.package-data]

finecode_dev_common_preset/src/finecode_dev_common_preset/preset.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ presets = [
44
{ source = "fine_python_aksem" },
55
]
66

7-
[[tool.finecode.action_handler]]
8-
source = "fine_python_black.BlackFormatHandler"
9-
config.preview = true
10-
117
# in development finecode can only be started with local version of finecode_extension_runner,
128
# otherwise version conflict occurs, because versions of finecode and
139
# finecode_extension_runner must match

finecode_extension_runner/src/finecode_extension_runner/_services/run_action.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -544,11 +544,16 @@ async def run_subresult_coros_concurrently(
544544
coro_task = tg.create_task(coro)
545545
coros_tasks.append(coro_task)
546546
except ExceptionGroup as eg:
547-
logger.error(f"R{run_id} | {eg}")
547+
errors_str = ""
548548
for exc in eg.exceptions:
549-
logger.exception(exc)
549+
if isinstance(exc, code_action.ActionFailedException):
550+
errors_str += exc.message + '.'
551+
else:
552+
logger.error("Unhandled exception:")
553+
logger.exception(exc)
554+
errors_str += str(exc) + '.'
550555
raise ActionFailedException(
551-
f"Concurrent running action handlers of '{action_name}' failed(Run {run_id}). See logs for more details"
556+
f"Concurrent running action handlers of '{action_name}' failed(Run {run_id}): {errors_str}"
552557
)
553558

554559
action_subresult: code_action.RunActionResult | None = None

presets/fine_python_format/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fine_python_format"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
66
readme = "README.md"
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
[tool.finecode.action.lint]
22
source = "finecode_extension_api.actions.lint.LintAction"
33
handlers = [
4+
{ name = "ruff", source = "fine_python_ruff.RuffLintHandler", env = "dev_no_runtime", dependencies = [
5+
"fine_python_ruff==0.1.*",
6+
] },
47
{ name = "flake8", source = "fine_python_flake8.Flake8LintHandler", env = "dev_no_runtime", dependencies = [
58
"fine_python_flake8==0.2.*",
6-
"flake8-bugbear (>=24.12.12,<25.0.0)",
79
] },
810
{ name = "mypy", source = "fine_python_mypy.MypyLintHandler", env = "dev_no_runtime", dependencies = [
911
"fine_python_mypy==0.2.*",
1012
] },
1113
]
1214

15+
# flake8 is used only for custom rules, all standard rules are checked by ruff, but
16+
# keep flake8 configuration if someone activates some rules or uses flake8 config
17+
# parameters in their own rules
1318
[[tool.finecode.action_handler]]
1419
source = "fine_python_flake8.Flake8LintHandler"
1520
config.max_line_length = 80
16-
config.extend_select = ["B950"]
17-
# W391 is not compatible with black, because black adds an empty line to the end of the file
21+
# W391 is not compatible with black(and ruff formatter, which is compatible with black),
22+
# because black adds an empty line to the end of the file
1823
# TODO: move in recommended config once config merging is implemented
1924
config.extend_ignore = ["E203", "E501", "E701", "W391"]
25+
# disable all standard rules
26+
config.select = []

presets/fine_python_lint/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fine_python_lint"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
66
readme = "README.md"

0 commit comments

Comments
 (0)