Skip to content

Commit 7441aae

Browse files
authored
Merge pull request #19 from finecode-dev/feature/pyrefly-lint
Pyrefly lint handler
2 parents a1fbbf4 + 8140253 commit 7441aae

File tree

10 files changed

+225
-6
lines changed

10 files changed

+225
-6
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# fine_python_pyrefly
2+
3+
FineCode extension for Pyrefly type checker integration.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .lint_handler import PyreflyLintHandler, PyreflyLintHandlerConfig
2+
3+
__all__ = [
4+
"PyreflyLintHandler",
5+
"PyreflyLintHandlerConfig",
6+
]
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 PyreflyLintHandlerConfig(code_action.ActionHandlerConfig):
15+
...
16+
17+
18+
class PyreflyLintHandler(
19+
code_action.ActionHandler[lint_action.LintAction, PyreflyLintHandlerConfig]
20+
):
21+
"""
22+
NOTE: pyrefly currently can check only saved files, not file content provided by
23+
FineCode. In environments like IDE, messages from pyrefly will be updated only after
24+
save of a file.
25+
"""
26+
CACHE_KEY = "PyreflyLinter"
27+
28+
def __init__(
29+
self,
30+
config: PyreflyLintHandlerConfig,
31+
cache: icache.ICache,
32+
logger: ilogger.ILogger,
33+
file_manager: ifilemanager.IFileManager,
34+
command_runner: icommandrunner.ICommandRunner,
35+
) -> None:
36+
self.config = config
37+
self.cache = cache
38+
self.logger = logger
39+
self.file_manager = file_manager
40+
self.command_runner = command_runner
41+
42+
self.pyrefly_bin_path = Path(sys.executable).parent / "pyrefly"
43+
44+
async def run_on_single_file(
45+
self, file_path: Path
46+
) -> lint_action.LintRunResult:
47+
messages = {}
48+
try:
49+
cached_lint_messages = await self.cache.get_file_cache(
50+
file_path, self.CACHE_KEY
51+
)
52+
messages[str(file_path)] = cached_lint_messages
53+
return lint_action.LintRunResult(messages=messages)
54+
except icache.CacheMissException:
55+
pass
56+
57+
file_version = await self.file_manager.get_file_version(file_path)
58+
lint_messages = await self.run_pyrefly_lint_on_single_file(file_path)
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_pyrefly_lint_on_single_file(
80+
self,
81+
file_path: Path,
82+
) -> list[lint_action.LintMessage]:
83+
"""Run pyrefly type checking on a single file"""
84+
lint_messages: list[lint_action.LintMessage] = []
85+
86+
cmd = [
87+
str(self.pyrefly_bin_path),
88+
"check",
89+
"--output-format",
90+
"json",
91+
str(file_path),
92+
]
93+
94+
cmd_str = " ".join(cmd)
95+
pyrefly_process = await self.command_runner.run(cmd_str)
96+
97+
await pyrefly_process.wait_for_end()
98+
99+
output = pyrefly_process.get_output()
100+
try:
101+
pyrefly_results = json.loads(output)
102+
for error in pyrefly_results['errors']:
103+
lint_message = map_pyrefly_error_to_lint_message(error)
104+
lint_messages.append(lint_message)
105+
except json.JSONDecodeError:
106+
raise code_action.ActionFailedException(f'Output of pyrefly is not json: {output}')
107+
108+
return lint_messages
109+
110+
111+
def map_pyrefly_error_to_lint_message(error: dict) -> lint_action.LintMessage:
112+
"""Map a pyrefly error to a lint message"""
113+
# Extract line/column info (pyrefly uses 1-based indexing)
114+
start_line = error['line']
115+
start_column = error['column']
116+
end_line = error['stop_line']
117+
end_column = error['stop_column']
118+
119+
# Determine severity based on error type
120+
error_code = error.get('code', '')
121+
code_description = error.get("name", "")
122+
severity = lint_action.LintMessageSeverity.ERROR
123+
124+
return lint_action.LintMessage(
125+
range=lint_action.Range(
126+
start=lint_action.Position(line=start_line, character=start_column),
127+
end=lint_action.Position(line=end_line, character=end_column),
128+
),
129+
message=error.get("description", ""),
130+
code=error_code,
131+
code_description=code_description,
132+
source="pyrefly",
133+
severity=severity,
134+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[project]
2+
name = "fine_python_pyrefly"
3+
version = "0.1.0"
4+
description = ""
5+
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
6+
readme = "README.md"
7+
requires-python = ">=3.11, < 3.14"
8+
dependencies = ["finecode_extension_api==0.3.*", "pyrefly (>=0.30.0,<1.0.0)"]
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import atexit
2+
import shutil
3+
import sys
4+
import tempfile
5+
6+
from setuptools import setup
7+
from setuptools.command.build import build
8+
from setuptools.command.build_ext import build_ext
9+
from setuptools.command.build_py import build_py
10+
from setuptools.command.egg_info import egg_info
11+
12+
# Create a single temp directory for all build operations
13+
_TEMP_BUILD_DIR = None
14+
15+
16+
def get_temp_build_dir(pkg_name):
17+
global _TEMP_BUILD_DIR
18+
if _TEMP_BUILD_DIR is None:
19+
_TEMP_BUILD_DIR = tempfile.mkdtemp(prefix=f"{pkg_name}_build_")
20+
atexit.register(lambda: shutil.rmtree(_TEMP_BUILD_DIR, ignore_errors=True))
21+
return _TEMP_BUILD_DIR
22+
23+
24+
class TempDirBuildMixin:
25+
def initialize_options(self):
26+
super().initialize_options()
27+
temp_dir = get_temp_build_dir(self.distribution.get_name())
28+
self.build_base = temp_dir
29+
30+
31+
class TempDirEggInfoMixin:
32+
def initialize_options(self):
33+
super().initialize_options()
34+
temp_dir = get_temp_build_dir(self.distribution.get_name())
35+
self.egg_base = temp_dir
36+
37+
38+
class CustomBuild(TempDirBuildMixin, build):
39+
pass
40+
41+
42+
class CustomBuildPy(TempDirBuildMixin, build_py):
43+
pass
44+
45+
46+
class CustomBuildExt(TempDirBuildMixin, build_ext):
47+
pass
48+
49+
50+
class CustomEggInfo(TempDirEggInfoMixin, egg_info):
51+
def initialize_options(self):
52+
# Don't use temp dir for editable installs
53+
if "--editable" in sys.argv or "-e" in sys.argv:
54+
egg_info.initialize_options(self)
55+
else:
56+
super().initialize_options()
57+
58+
59+
setup(
60+
name="fine_python_pyrefly",
61+
cmdclass={
62+
"build": CustomBuild,
63+
"build_py": CustomBuildPy,
64+
"build_ext": CustomBuildExt,
65+
"egg_info": CustomEggInfo,
66+
},
67+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Tests for fine_python_pyrefly extension

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.3.*",
10+
"fine_python_recommended==0.4.*",
1111
]
1212

1313
[tool.setuptools.package-data]

presets/fine_python_lint/fine_python_lint/preset.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ handlers = [
77
{ name = "flake8", source = "fine_python_flake8.Flake8LintHandler", env = "dev_no_runtime", dependencies = [
88
"fine_python_flake8==0.2.*",
99
] },
10-
{ name = "mypy", source = "fine_python_mypy.MypyLintHandler", env = "dev_no_runtime", dependencies = [
11-
"fine_python_mypy==0.2.*",
10+
{ name = "pyrefly", source = "fine_python_pyrefly.PyreflyLintHandler", env = "dev_no_runtime", dependencies = [
11+
"fine_python_pyrefly==0.1.*",
1212
] },
1313
]
1414

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.3.0"
3+
version = "0.4.0"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
66
readme = "README.md"

presets/fine_python_recommended/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
[project]
22
name = "fine_python_recommended"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
description = ""
55
authors = [{ name = "Vladyslav Hnatiuk", email = "[email protected]" }]
66
readme = "README.md"
77
requires-python = ">=3.11, < 3.14"
8-
dependencies = ["fine_python_format==0.3.*", "fine_python_lint==0.3.*"]
8+
dependencies = ["fine_python_format==0.3.*", "fine_python_lint==0.4.*"]
99

1010
[build-system]
1111
requires = ["setuptools>=64"]

0 commit comments

Comments
 (0)