diff --git a/extensions/fine_python_pyrefly/README.md b/extensions/fine_python_pyrefly/README.md new file mode 100644 index 0000000..492a5f3 --- /dev/null +++ b/extensions/fine_python_pyrefly/README.md @@ -0,0 +1,3 @@ +# fine_python_pyrefly + +FineCode extension for Pyrefly type checker integration. diff --git a/extensions/fine_python_pyrefly/fine_python_pyrefly/__init__.py b/extensions/fine_python_pyrefly/fine_python_pyrefly/__init__.py new file mode 100644 index 0000000..d9e6a9c --- /dev/null +++ b/extensions/fine_python_pyrefly/fine_python_pyrefly/__init__.py @@ -0,0 +1,6 @@ +from .lint_handler import PyreflyLintHandler, PyreflyLintHandlerConfig + +__all__ = [ + "PyreflyLintHandler", + "PyreflyLintHandlerConfig", +] \ No newline at end of file diff --git a/extensions/fine_python_pyrefly/fine_python_pyrefly/lint_handler.py b/extensions/fine_python_pyrefly/fine_python_pyrefly/lint_handler.py new file mode 100644 index 0000000..3e15608 --- /dev/null +++ b/extensions/fine_python_pyrefly/fine_python_pyrefly/lint_handler.py @@ -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, + ) diff --git a/extensions/fine_python_pyrefly/pyproject.toml b/extensions/fine_python_pyrefly/pyproject.toml new file mode 100644 index 0000000..2ac97b6 --- /dev/null +++ b/extensions/fine_python_pyrefly/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "fine_python_pyrefly" +version = "0.1.0" +description = "" +authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] +readme = "README.md" +requires-python = ">=3.11, < 3.14" +dependencies = ["finecode_extension_api==0.3.*", "pyrefly (>=0.30.0,<1.0.0)"] diff --git a/extensions/fine_python_pyrefly/setup.py b/extensions/fine_python_pyrefly/setup.py new file mode 100644 index 0000000..7809fb7 --- /dev/null +++ b/extensions/fine_python_pyrefly/setup.py @@ -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, + }, +) \ No newline at end of file diff --git a/extensions/fine_python_pyrefly/tests/__init__.py b/extensions/fine_python_pyrefly/tests/__init__.py new file mode 100644 index 0000000..39a7300 --- /dev/null +++ b/extensions/fine_python_pyrefly/tests/__init__.py @@ -0,0 +1 @@ +# Tests for fine_python_pyrefly extension \ No newline at end of file diff --git a/finecode_dev_common_preset/pyproject.toml b/finecode_dev_common_preset/pyproject.toml index ed7a1df..38fa669 100644 --- a/finecode_dev_common_preset/pyproject.toml +++ b/finecode_dev_common_preset/pyproject.toml @@ -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] diff --git a/presets/fine_python_lint/fine_python_lint/preset.toml b/presets/fine_python_lint/fine_python_lint/preset.toml index ddbb75e..dc81393 100644 --- a/presets/fine_python_lint/fine_python_lint/preset.toml +++ b/presets/fine_python_lint/fine_python_lint/preset.toml @@ -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.*", ] }, ] diff --git a/presets/fine_python_lint/pyproject.toml b/presets/fine_python_lint/pyproject.toml index 622b6d9..68f0de3 100644 --- a/presets/fine_python_lint/pyproject.toml +++ b/presets/fine_python_lint/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fine_python_lint" -version = "0.3.0" +version = "0.4.0" description = "" authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] readme = "README.md" diff --git a/presets/fine_python_recommended/pyproject.toml b/presets/fine_python_recommended/pyproject.toml index 6d963c4..48de1fc 100644 --- a/presets/fine_python_recommended/pyproject.toml +++ b/presets/fine_python_recommended/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "fine_python_recommended" -version = "0.3.0" +version = "0.4.0" description = "" authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] 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"]