Skip to content

Commit ecc0f76

Browse files
committed
pyrefly lint handler. Use it in lint preset instead of mypy. fine_python_lint v0.4.0, fine_python_recommended v0.4.0
1 parent a1fbbf4 commit ecc0f76

File tree

10 files changed

+226
-6
lines changed

10 files changed

+226
-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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
CACHE_KEY = "PyreflyLinter"
22+
23+
def __init__(
24+
self,
25+
config: PyreflyLintHandlerConfig,
26+
cache: icache.ICache,
27+
logger: ilogger.ILogger,
28+
file_manager: ifilemanager.IFileManager,
29+
command_runner: icommandrunner.ICommandRunner,
30+
) -> None:
31+
self.config = config
32+
self.cache = cache
33+
self.logger = logger
34+
self.file_manager = file_manager
35+
self.command_runner = command_runner
36+
37+
self.pyrefly_bin_path = Path(sys.executable).parent / "pyrefly"
38+
39+
async def run_on_single_file(
40+
self, file_path: Path
41+
) -> lint_action.LintRunResult:
42+
messages = {}
43+
try:
44+
cached_lint_messages = await self.cache.get_file_cache(
45+
file_path, self.CACHE_KEY
46+
)
47+
messages[str(file_path)] = cached_lint_messages
48+
return lint_action.LintRunResult(messages=messages)
49+
except icache.CacheMissException:
50+
pass
51+
52+
file_version = await self.file_manager.get_file_version(file_path)
53+
file_content = await self.file_manager.get_content(file_path)
54+
lint_messages = await self.run_pyrefly_lint_on_single_file(file_path, file_content)
55+
messages[str(file_path)] = lint_messages
56+
await self.cache.save_file_cache(
57+
file_path, file_version, self.CACHE_KEY, lint_messages
58+
)
59+
60+
return lint_action.LintRunResult(messages=messages)
61+
62+
async def run(
63+
self,
64+
payload: lint_action.LintRunPayload,
65+
run_context: code_action.RunActionWithPartialResultsContext,
66+
) -> None:
67+
file_paths = [file_path async for file_path in payload]
68+
69+
for file_path in file_paths:
70+
run_context.partial_result_scheduler.schedule(
71+
file_path,
72+
self.run_on_single_file(file_path),
73+
)
74+
75+
async def run_pyrefly_lint_on_single_file(
76+
self,
77+
file_path: Path,
78+
file_content: str,
79+
) -> list[lint_action.LintMessage]:
80+
"""Run pyrefly type checking on a single file"""
81+
lint_messages: list[lint_action.LintMessage] = []
82+
83+
cmd = [
84+
str(self.pyrefly_bin_path),
85+
"check",
86+
"--output-format",
87+
"json",
88+
str(file_path),
89+
]
90+
91+
cmd_str = " ".join(cmd)
92+
pyrefly_process = await self.command_runner.run(cmd_str)
93+
94+
pyrefly_process.write_to_stdin(file_content)
95+
pyrefly_process.close_stdin() # Signal EOF
96+
97+
await pyrefly_process.wait_for_end()
98+
99+
output = pyrefly_process.get_output()
100+
try:
101+
pyrefly_results = json.loads(output)
102+
self.logger.info(pyrefly_results)
103+
for error in pyrefly_results['errors']:
104+
lint_message = map_pyrefly_error_to_lint_message(error)
105+
lint_messages.append(lint_message)
106+
except json.JSONDecodeError:
107+
raise code_action.ActionFailedException(f'Output of pyrefly is not json: {output}')
108+
109+
return lint_messages
110+
111+
112+
def map_pyrefly_error_to_lint_message(error: dict) -> lint_action.LintMessage:
113+
"""Map a pyrefly error to a lint message"""
114+
# Extract line/column info (pyrefly uses 1-based indexing)
115+
start_line = error['line']
116+
start_column = error['column']
117+
end_line = error['stop_line']
118+
end_column = error['stop_column']
119+
120+
# Determine severity based on error type
121+
error_code = error.get('code', '')
122+
code_description = error.get("name", "")
123+
severity = lint_action.LintMessageSeverity.ERROR
124+
125+
return lint_action.LintMessage(
126+
range=lint_action.Range(
127+
start=lint_action.Position(line=start_line, character=start_column),
128+
end=lint_action.Position(line=end_line, character=end_column),
129+
),
130+
message=error.get("description", ""),
131+
code=error_code,
132+
code_description=code_description,
133+
source="pyrefly",
134+
severity=severity,
135+
)
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)