Skip to content
Merged
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: 2 additions & 0 deletions extensions/fine_python_flake8/fine_python_flake8/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion extensions/fine_python_flake8/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fine_python_flake8"
version = "0.2.0"
version = "0.2.1"
description = ""
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
readme = "README.md"
Expand Down
5 changes: 4 additions & 1 deletion extensions/fine_python_ruff/fine_python_ruff/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from .format_handler import RuffFormatHandler, RuffFormatHandlerConfig
from .lint_handler import RuffLintHandler, RuffLintHandlerConfig

__all__ = [
"RuffFormatHandler",
"RuffFormatHandlerConfig"
"RuffFormatHandlerConfig",
"RuffLintHandler",
"RuffLintHandlerConfig",
]
161 changes: 161 additions & 0 deletions extensions/fine_python_ruff/fine_python_ruff/lint_handler.py
Original file line number Diff line number Diff line change
@@ -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,
)
2 changes: 1 addition & 1 deletion finecode_dev_common_preset/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion presets/fine_python_format/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fine_python_format"
version = "0.2.0"
version = "0.3.0"
description = ""
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
readme = "README.md"
Expand Down
13 changes: 10 additions & 3 deletions presets/fine_python_lint/fine_python_lint/preset.toml
Original file line number Diff line number Diff line change
@@ -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 = []
2 changes: 1 addition & 1 deletion presets/fine_python_lint/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fine_python_lint"
version = "0.2.0"
version = "0.3.0"
description = ""
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
readme = "README.md"
Expand Down
4 changes: 2 additions & 2 deletions presets/fine_python_recommended/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[project]
name = "fine_python_recommended"
version = "0.2.0"
version = "0.3.0"
description = ""
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
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"]
Expand Down
Loading