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
3 changes: 3 additions & 0 deletions extensions/fine_python_pyrefly/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# fine_python_pyrefly

FineCode extension for Pyrefly type checker integration.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .lint_handler import PyreflyLintHandler, PyreflyLintHandlerConfig

__all__ = [
"PyreflyLintHandler",
"PyreflyLintHandlerConfig",
]
134 changes: 134 additions & 0 deletions extensions/fine_python_pyrefly/fine_python_pyrefly/lint_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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 PyreflyLintHandlerConfig(code_action.ActionHandlerConfig):
...


class PyreflyLintHandler(
code_action.ActionHandler[lint_action.LintAction, PyreflyLintHandlerConfig]
):
"""
NOTE: pyrefly currently can check only saved files, not file content provided by
FineCode. In environments like IDE, messages from pyrefly will be updated only after
save of a file.
"""
CACHE_KEY = "PyreflyLinter"

def __init__(
self,
config: PyreflyLintHandlerConfig,
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.pyrefly_bin_path = Path(sys.executable).parent / "pyrefly"

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)
lint_messages = await self.run_pyrefly_lint_on_single_file(file_path)
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_pyrefly_lint_on_single_file(
self,
file_path: Path,
) -> list[lint_action.LintMessage]:
"""Run pyrefly type checking on a single file"""
lint_messages: list[lint_action.LintMessage] = []

cmd = [
str(self.pyrefly_bin_path),
"check",
"--output-format",
"json",
str(file_path),
]

cmd_str = " ".join(cmd)
pyrefly_process = await self.command_runner.run(cmd_str)

await pyrefly_process.wait_for_end()

output = pyrefly_process.get_output()
try:
pyrefly_results = json.loads(output)
for error in pyrefly_results['errors']:
lint_message = map_pyrefly_error_to_lint_message(error)
lint_messages.append(lint_message)
except json.JSONDecodeError:
raise code_action.ActionFailedException(f'Output of pyrefly is not json: {output}')

return lint_messages


def map_pyrefly_error_to_lint_message(error: dict) -> lint_action.LintMessage:
"""Map a pyrefly error to a lint message"""
# Extract line/column info (pyrefly uses 1-based indexing)
start_line = error['line']
start_column = error['column']
end_line = error['stop_line']
end_column = error['stop_column']

# Determine severity based on error type
error_code = error.get('code', '')
code_description = error.get("name", "")
severity = lint_action.LintMessageSeverity.ERROR

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=error.get("description", ""),
code=error_code,
code_description=code_description,
source="pyrefly",
severity=severity,
)
8 changes: 8 additions & 0 deletions extensions/fine_python_pyrefly/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
name = "fine_python_pyrefly"
version = "0.1.0"
description = ""
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
readme = "README.md"
requires-python = ">=3.11, < 3.14"
dependencies = ["finecode_extension_api==0.3.*", "pyrefly (>=0.30.0,<1.0.0)"]
67 changes: 67 additions & 0 deletions extensions/fine_python_pyrefly/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import atexit
import shutil
import sys
import tempfile

from setuptools import setup
from setuptools.command.build import build
from setuptools.command.build_ext import build_ext
from setuptools.command.build_py import build_py
from setuptools.command.egg_info import egg_info

# Create a single temp directory for all build operations
_TEMP_BUILD_DIR = None


def get_temp_build_dir(pkg_name):
global _TEMP_BUILD_DIR
if _TEMP_BUILD_DIR is None:
_TEMP_BUILD_DIR = tempfile.mkdtemp(prefix=f"{pkg_name}_build_")
atexit.register(lambda: shutil.rmtree(_TEMP_BUILD_DIR, ignore_errors=True))
return _TEMP_BUILD_DIR


class TempDirBuildMixin:
def initialize_options(self):
super().initialize_options()
temp_dir = get_temp_build_dir(self.distribution.get_name())
self.build_base = temp_dir


class TempDirEggInfoMixin:
def initialize_options(self):
super().initialize_options()
temp_dir = get_temp_build_dir(self.distribution.get_name())
self.egg_base = temp_dir


class CustomBuild(TempDirBuildMixin, build):
pass


class CustomBuildPy(TempDirBuildMixin, build_py):
pass


class CustomBuildExt(TempDirBuildMixin, build_ext):
pass


class CustomEggInfo(TempDirEggInfoMixin, egg_info):
def initialize_options(self):
# Don't use temp dir for editable installs
if "--editable" in sys.argv or "-e" in sys.argv:
egg_info.initialize_options(self)
else:
super().initialize_options()


setup(
name="fine_python_pyrefly",
cmdclass={
"build": CustomBuild,
"build_py": CustomBuildPy,
"build_ext": CustomBuildExt,
"egg_info": CustomEggInfo,
},
)
1 change: 1 addition & 0 deletions extensions/fine_python_pyrefly/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Tests for fine_python_pyrefly extension
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.3.*",
"fine_python_recommended==0.4.*",
]

[tool.setuptools.package-data]
Expand Down
4 changes: 2 additions & 2 deletions presets/fine_python_lint/fine_python_lint/preset.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ handlers = [
{ name = "flake8", source = "fine_python_flake8.Flake8LintHandler", env = "dev_no_runtime", dependencies = [
"fine_python_flake8==0.2.*",
] },
{ name = "mypy", source = "fine_python_mypy.MypyLintHandler", env = "dev_no_runtime", dependencies = [
"fine_python_mypy==0.2.*",
{ name = "pyrefly", source = "fine_python_pyrefly.PyreflyLintHandler", env = "dev_no_runtime", dependencies = [
"fine_python_pyrefly==0.1.*",
] },
]

Expand Down
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.3.0"
version = "0.4.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.3.0"
version = "0.4.0"
description = ""
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
readme = "README.md"
requires-python = ">=3.11, < 3.14"
dependencies = ["fine_python_format==0.3.*", "fine_python_lint==0.3.*"]
dependencies = ["fine_python_format==0.3.*", "fine_python_lint==0.4.*"]

[build-system]
requires = ["setuptools>=64"]
Expand Down
Loading