diff --git a/extensions/fine_python_flake8/fine_python_flake8/action.py b/extensions/fine_python_flake8/fine_python_flake8/action.py index 630ad32..8e54c4c 100644 --- a/extensions/fine_python_flake8/fine_python_flake8/action.py +++ b/extensions/fine_python_flake8/fine_python_flake8/action.py @@ -2,6 +2,7 @@ import argparse import ast +import dataclasses import operator from pathlib import Path @@ -106,6 +107,7 @@ def run_flake8_on_single_file( return lint_messages +@dataclasses.dataclass class Flake8LintHandlerConfig(code_action.ActionHandlerConfig): max_line_length: int = 79 extend_select: list[str] | None = None diff --git a/extensions/fine_python_mypy/fine_python_mypy/action.py b/extensions/fine_python_mypy/fine_python_mypy/action.py index 51e4efa..4a7265b 100644 --- a/extensions/fine_python_mypy/fine_python_mypy/action.py +++ b/extensions/fine_python_mypy/fine_python_mypy/action.py @@ -1,6 +1,7 @@ # TODO: what to do with file manager? Mypy would need ability to check module text, # not only module file import asyncio +import dataclasses import hashlib import sys from pathlib import Path @@ -20,6 +21,7 @@ class DmypyFailedError(Exception): ... +@dataclasses.dataclass class MypyManyCodeActionConfig(code_action.ActionHandlerConfig): ... @@ -239,7 +241,7 @@ async def _run_dmypy(self, file_paths: list[Path], cwd: Path) -> str: self.logger.debug(f"run dmypy in {cwd}") status_file_path = self._get_status_file_path(dmypy_cwd=cwd) runner_python_executable = sys.executable - file_paths_str = " ".join([f"'{str(file_path)}'" for file_path in file_paths]) + file_paths_strs = [str(file_path) for file_path in file_paths] cmd_parts = [ f"{runner_python_executable}", "-m", @@ -248,7 +250,7 @@ async def _run_dmypy(self, file_paths: list[Path], cwd: Path) -> str: "run", "--", *self.DMYPY_ARGS, - f"{file_paths_str}", + *file_paths_strs ] cmd = " ".join(cmd_parts) dmypy_run_process = await self.command_runner.run( diff --git a/finecode.sh b/finecode.sh deleted file mode 100644 index 176acc2..0000000 --- a/finecode.sh +++ /dev/null @@ -1 +0,0 @@ -poetry run python \ No newline at end of file diff --git a/finecode_dev_common_preset/poetry.lock b/finecode_dev_common_preset/poetry.lock index 7c0956b..0fd78ba 100644 --- a/finecode_dev_common_preset/poetry.lock +++ b/finecode_dev_common_preset/poetry.lock @@ -79,14 +79,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "click" -version = "8.1.8" +version = "8.2.1" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] [package.dependencies] @@ -129,129 +129,99 @@ name = "fine-python-ast" version = "0.1.0" description = "" optional = false -python-versions = ">=3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "fine_python_ast-0.1.0-py3-none-any.whl", hash = "sha256:3dcdcdc40ed89e0b90686f2ad5d358d8edba622a5de4c1f52f2f0787dcc4e07d"}, + {file = "fine_python_ast-0.1.0.tar.gz", hash = "sha256:ebc68aef7d0379f8771f610e436f6075b7c299a8895f1b9b7769fd038f95397e"}, +] [package.dependencies] -finecode_extension_api = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "finecode_extension_api"} - -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "extensions/fine_python_ast" +finecode_extension_api = "0.1.0" [[package]] name = "fine-python-black" version = "0.1.0" description = "" optional = false -python-versions = ">=3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "fine_python_black-0.1.0-py3-none-any.whl", hash = "sha256:27a012123783e4217222546e37280660e5a4948cdd3aa12d7fae7a3ce21a875d"}, + {file = "fine_python_black-0.1.0.tar.gz", hash = "sha256:ce300e897b4d819abe4754177cd5b81d29b2d4d04233942135edc1a8ed234bf7"}, +] [package.dependencies] black = ">=25.1.0,<26.0.0" -finecode_extension_api = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "finecode_extension_api"} - -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "extensions/fine_python_black" +finecode_extension_api = "0.1.0" [[package]] name = "fine-python-flake8" version = "0.1.0" description = "" optional = false -python-versions = ">=3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "fine_python_flake8-0.1.0-py3-none-any.whl", hash = "sha256:7ed7641505936f334396631f779953a13ae18c8f207a8258d722e50fa452d792"}, + {file = "fine_python_flake8-0.1.0.tar.gz", hash = "sha256:ccba9a1ec41f5f3aa756efa63bd64650237ad63d3098aedf673e9c254e502914"}, +] [package.dependencies] -fine_python_ast = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "extensions/fine_python_ast"} -finecode_extension_api = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "finecode_extension_api"} +fine_python_ast = "0.1.0" +finecode_extension_api = "0.1.0" flake8 = ">=7.1.2,<8.0.0" types-flake8 = ">=7.1.0.20241020,<8.0.0.0" -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "extensions/fine_python_flake8" - [[package]] name = "fine-python-format" version = "0.1.0" description = "" optional = false -python-versions = ">=3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "fine_python_format-0.1.0-py3-none-any.whl", hash = "sha256:26315bc8ae5a4830efd9ee41ab3e220beead52c0702cc6f82df720e59dbfbfc2"}, + {file = "fine_python_format-0.1.0.tar.gz", hash = "sha256:8d8031316abed4761d096c2a0c100c8d7cc6d391ea386149ef625964ab80d0d2"}, +] [package.dependencies] -fine_python_black = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "extensions/fine_python_black"} -fine_python_isort = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "extensions/fine_python_isort"} - -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "presets/fine_python_format" +fine_python_black = "0.1.0" +fine_python_isort = "0.1.0" [[package]] name = "fine-python-isort" version = "0.1.0" description = "" optional = false -python-versions = ">= 3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "fine_python_isort-0.1.0-py3-none-any.whl", hash = "sha256:cbd6cd5502d65122e9f6461758f78db8d9e5628ab97c41a67ee6ef85a3526c8e"}, + {file = "fine_python_isort-0.1.0.tar.gz", hash = "sha256:64468a96b49663226b422885b25de7c22c24f6dafef6c25c1d02ea6e49662b53"}, +] [package.dependencies] -finecode_extension_api = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "finecode_extension_api"} +finecode_extension_api = "0.1.0" isort = ">=5.13,<6" -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "extensions/fine_python_isort" - [[package]] name = "fine-python-lint" version = "0.1.0" description = "" optional = false -python-versions = ">=3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "fine_python_lint-0.1.0-py3-none-any.whl", hash = "sha256:180517a54a38ab4942bf51611d03a9afd3668e8d11f59e289444c2e1756434de"}, + {file = "fine_python_lint-0.1.0.tar.gz", hash = "sha256:597660e54fe4fa4024b54f0050cdc378c48d0a03fd2938d1c0ae60204a8160d8"}, +] [package.dependencies] -fine_python_flake8 = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "extensions/fine_python_flake8"} -fine_python_mypy = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "extensions/fine_python_mypy"} +fine_python_flake8 = "0.1.0" +fine_python_mypy = "0.1.0" flake8-bugbear = ">=24.12.12,<25.0.0" -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "presets/fine_python_lint" - [[package]] name = "fine-python-module-exports" version = "0.1.0" @@ -263,14 +233,14 @@ files = [] develop = false [package.dependencies] -fine_python_ast = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "extensions/fine_python_ast"} -finecode_extension_api = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "finecode_extension_api"} +fine_python_ast = "0.1.0" +finecode_extension_api = "0.1.0" [package.source] type = "git" url = "https://github.com/finecode-dev/finecode.git" reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" +resolved_reference = "fa081a6f836fb6ca46611fc1d9561ba74ad16686" subdirectory = "extensions/fine_python_module_exports" [[package]] @@ -284,58 +254,45 @@ files = [] develop = false [package.dependencies] -finecode_extension_api = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "finecode_extension_api"} +finecode_extension_api = "0.1.0" mypy = ">=1.15,<2.0" [package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "extensions/fine_python_mypy" +type = "directory" +url = "../extensions/fine_python_mypy" [[package]] name = "fine-python-recommended" version = "0.1.0" description = "" optional = false -python-versions = ">=3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "fine_python_recommended-0.1.0-py3-none-any.whl", hash = "sha256:3416df1eb4af8ce80206684c87c55dc55331999ef8b9ffb60a874daf0ddfbb98"}, + {file = "fine_python_recommended-0.1.0.tar.gz", hash = "sha256:bc721e5eb381b02a6049f171e042c29cbbd9dac5ecec8c22a88806a6811d3b9d"}, +] [package.dependencies] -fine_python_format = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "presets/fine_python_format"} -fine_python_lint = {git = "https://github.com/finecode-dev/finecode.git", subdirectory = "presets/fine_python_lint"} - -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "presets/fine_python_recommended" +fine_python_format = "0.1.0" +fine_python_lint = "0.1.0" [[package]] name = "finecode-extension-api" version = "0.1.0" description = "" optional = false -python-versions = ">=3.11, < 3.14" +python-versions = "<3.14,>=3.11" groups = ["main"] -files = [] -develop = false +files = [ + {file = "finecode_extension_api-0.1.0-py3-none-any.whl", hash = "sha256:55d4448a1a2f7224c3fdf05879fa9782246c0a85dc1dfb17543374fd808c2b5b"}, + {file = "finecode_extension_api-0.1.0.tar.gz", hash = "sha256:dee16c180e4cd318f71bf7a94b6ae425667153f8fb62630ab60db9b034f45595"}, +] [package.dependencies] pydantic = ">=2.10.6,<3.0.0" typing-extensions = ">=4.12.2,<5.0.0" -[package.source] -type = "git" -url = "https://github.com/finecode-dev/finecode.git" -reference = "HEAD" -resolved_reference = "946844b2f151a7091664c407f7e24ce01442bee5" -subdirectory = "finecode_extension_api" - [[package]] name = "flake8" version = "7.2.0" @@ -490,14 +447,14 @@ files = [ [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, - {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [package.extras] @@ -519,14 +476,14 @@ files = [ [[package]] name = "pydantic" -version = "2.11.4" +version = "2.11.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, - {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, + {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, + {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, ] [package.dependencies] @@ -680,14 +637,14 @@ types-pyflakes = "*" [[package]] name = "types-pyflakes" -version = "3.3.2.20250429" +version = "3.3.2.20250511" description = "Typing stubs for pyflakes" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "types_pyflakes-3.3.2.20250429-py3-none-any.whl", hash = "sha256:f9ccc1968ddd1a18232c1e66cfcce8a9e8f4b2b85fbbf682bf87148a2b2d58a0"}, - {file = "types_pyflakes-3.3.2.20250429.tar.gz", hash = "sha256:a81b0ee91e34d143f655d366bd4002730f0e342a5aa338779d2f995515ce1c5c"}, + {file = "types_pyflakes-3.3.2.20250511-py3-none-any.whl", hash = "sha256:85802fdd0b64d3553ef12ac0ba02d85c4bbd38747579c544e6bb005ec455becf"}, + {file = "types_pyflakes-3.3.2.20250511.tar.gz", hash = "sha256:d0ef58f9ec15eab2a9e427814f48587be4eb2752a8ae7dec201d65086f50ace2"}, ] [[package]] @@ -704,14 +661,14 @@ files = [ [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, - {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, ] [package.dependencies] @@ -720,4 +677,4 @@ typing-extensions = ">=4.12.0" [metadata] lock-version = "2.1" python-versions = ">=3.11, < 3.14" -content-hash = "21456805d44cb3d018f182a4d345d7e8165871f4ee1f7c41b70e755cbd0a3a7f" +content-hash = "986f7e2322de03c2f24845d27748b9a3cb4bab269efe8e91ba627714b92e8dfc" diff --git a/finecode_dev_common_preset/pyproject.toml b/finecode_dev_common_preset/pyproject.toml index cd59846..ec45326 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 @ git+https://github.com/finecode-dev/finecode.git#subdirectory=presets/fine_python_recommended", + "fine_python_recommended==0.1.0", ] [tool.poetry] diff --git a/finecode_extension_api/finecode_extension_api/actions/format.py b/finecode_extension_api/finecode_extension_api/actions/format.py index c5da7cc..e227980 100644 --- a/finecode_extension_api/finecode_extension_api/actions/format.py +++ b/finecode_extension_api/finecode_extension_api/actions/format.py @@ -1,3 +1,4 @@ +import dataclasses import sys from pathlib import Path from typing import NamedTuple @@ -12,6 +13,7 @@ from finecode_extension_api import code_action, textstyler +@dataclasses.dataclass class FormatRunPayload(code_action.RunActionPayload): file_paths: list[Path] save: bool @@ -22,6 +24,7 @@ class FileInfo(NamedTuple): file_version: str + class FormatRunContext(code_action.RunActionContext): def __init__( self, @@ -42,12 +45,14 @@ async def init(self, initial_payload: FormatRunPayload) -> None: ) -class FormatRunFileResult(code_action.RunActionResult): +@dataclasses.dataclass +class FormatRunFileResult: changed: bool # changed code or empty string if code was not changed code: str +@dataclasses.dataclass class FormatRunResult(code_action.RunActionResult): result_by_file_path: dict[Path, FormatRunFileResult] @@ -79,11 +84,13 @@ def to_text(self) -> str | textstyler.StyledText: return text -type FormatAction = code_action.Action[ - FormatRunPayload, FormatRunContext, FormatRunResult -] +class FormatAction(code_action.Action): + PAYLOAD_TYPE = FormatRunPayload + RUN_CONTEXT_TYPE = FormatRunContext + RESULT_TYPE = FormatRunResult +@dataclasses.dataclass class SaveFormatHandlerConfig(code_action.ActionHandlerConfig): ... diff --git a/finecode_extension_api/finecode_extension_api/actions/ide/text_document_code_action.py b/finecode_extension_api/finecode_extension_api/actions/ide/text_document_code_action.py index 84bf6cb..f57b52c 100644 --- a/finecode_extension_api/finecode_extension_api/actions/ide/text_document_code_action.py +++ b/finecode_extension_api/finecode_extension_api/actions/ide/text_document_code_action.py @@ -1,8 +1,10 @@ +import dataclasses import enum from finecode_extension_api import code_action, common_types +@dataclasses.dataclass class CodeActionPayload(code_action.RunActionPayload): text_document: common_types.TextDocumentIdentifier range: common_types.Range @@ -27,10 +29,11 @@ class CodeActionTriggerKind(enum.IntEnum): AUTOMATIC = 2 -class Diagnostic(code_action.BaseModel): ... +@dataclasses.dataclass +class Diagnostic: ... -class CodeActionContext(code_action.BaseModel): +class CodeActionContext: diagnostics: list[Diagnostic] only: CodeActionKind | None trigger_kind: CodeActionTriggerKind diff --git a/finecode_extension_api/finecode_extension_api/actions/ide/text_document_inlay_hint.py b/finecode_extension_api/finecode_extension_api/actions/ide/text_document_inlay_hint.py index b2523d7..44fb9f5 100644 --- a/finecode_extension_api/finecode_extension_api/actions/ide/text_document_inlay_hint.py +++ b/finecode_extension_api/finecode_extension_api/actions/ide/text_document_inlay_hint.py @@ -1,8 +1,10 @@ +import dataclasses import enum from finecode_extension_api import code_action, common_types +@dataclasses.dataclass class InlayHintPayload(code_action.RunActionPayload): text_document: common_types.TextDocumentIdentifier range: common_types.Range @@ -13,7 +15,8 @@ class InlayHintKind(enum.IntEnum): PARAM = 2 -class InlayHint(code_action.BaseModel): +@dataclasses.dataclass +class InlayHint: position: common_types.Position label: str kind: InlayHintKind @@ -21,10 +24,11 @@ class InlayHint(code_action.BaseModel): padding_right: bool = False +@dataclasses.dataclass class InlayHintResult(code_action.RunActionResult): hints: list[InlayHint] | None -type TextDocumentInlayHintAction = code_action.Action[ - InlayHintPayload, code_action.RunActionContext, InlayHintResult -] +class TextDocumentInlayHintAction(code_action.Action): + PAYLOAD_TYPE = InlayHintPayload + RESULT_TYPE = InlayHintResult diff --git a/finecode_extension_api/finecode_extension_api/actions/lint.py b/finecode_extension_api/finecode_extension_api/actions/lint.py index 34046d0..39f5510 100644 --- a/finecode_extension_api/finecode_extension_api/actions/lint.py +++ b/finecode_extension_api/finecode_extension_api/actions/lint.py @@ -1,16 +1,19 @@ import collections.abc +import dataclasses import enum from pathlib import Path from finecode_extension_api import code_action, textstyler -class Position(code_action.BaseModel): +@dataclasses.dataclass +class Position: line: int character: int -class Range(code_action.BaseModel): +@dataclasses.dataclass +class Range: start: Position end: Position @@ -23,7 +26,8 @@ class LintMessageSeverity(enum.IntEnum): HINT = 4 -class LintMessage(code_action.BaseModel): +@dataclasses.dataclass +class LintMessage: range: Range message: str code: str | None = None @@ -32,6 +36,7 @@ class LintMessage(code_action.BaseModel): severity: LintMessageSeverity | None = None +@dataclasses.dataclass class LintRunPayload(code_action.RunActionPayload, collections.abc.AsyncIterable): file_paths: list[Path] @@ -39,6 +44,7 @@ def __aiter__(self) -> collections.abc.AsyncIterator[Path]: return LintRunPayloadIterator(self) +@dataclasses.dataclass class LintRunPayloadIterator(collections.abc.AsyncIterator): def __init__(self, lint_run_payload: LintRunPayload): self.lint_run_payload = lint_run_payload @@ -54,6 +60,7 @@ async def __anext__(self) -> Path: return self.lint_run_payload.file_paths[self.current_file_path_index - 1] +@dataclasses.dataclass class LintRunResult(code_action.RunActionResult): # messages is a dict to support messages for multiple files because it could be the # case that linter checks given file and its dependencies. @@ -100,6 +107,7 @@ def return_code(self) -> code_action.RunReturnCode: return code_action.RunReturnCode.SUCCESS -type LintAction = code_action.Action[ - LintRunPayload, code_action.RunActionWithPartialResultsContext, LintRunResult -] +class LintAction(code_action.Action): + PAYLOAD_TYPE = LintRunPayload + RUN_CONTEXT_TYPE = code_action.RunActionWithPartialResultsContext + RESULT_TYPE = LintRunResult diff --git a/finecode_extension_api/finecode_extension_api/code_action.py b/finecode_extension_api/finecode_extension_api/code_action.py index 6d6efc4..25e4261 100644 --- a/finecode_extension_api/finecode_extension_api/code_action.py +++ b/finecode_extension_api/finecode_extension_api/code_action.py @@ -2,19 +2,22 @@ import asyncio import collections.abc +import dataclasses import enum from pathlib import Path from typing import Generic, Protocol, TypeVar +import typing -from pydantic import BaseModel -from finecode_extension_api import partialresultscheduler +from finecode_extension_api import partialresultscheduler, textstyler -class ActionHandlerConfig(BaseModel): ... +@dataclasses.dataclass +class ActionHandlerConfig: ... -class RunActionPayload(BaseModel): ... +@dataclasses.dataclass +class RunActionPayload: ... class RunReturnCode(enum.IntEnum): @@ -22,11 +25,12 @@ class RunReturnCode(enum.IntEnum): ERROR = 1 -class RunActionResult(BaseModel): +@dataclasses.dataclass +class RunActionResult: def update(self, other: RunActionResult) -> None: raise NotImplementedError() - def to_text(self) -> str: + def to_text(self) -> str | textstyler.StyledText: return str(self) @property @@ -77,7 +81,16 @@ def __init__(self, project_dir: Path, cache_dir: Path) -> None: self.cache_dir = cache_dir -class Action(Generic[RunPayloadType, RunContextType, RunResultType]): ... +@dataclasses.dataclass +class ActionConfig: + run_handlers_concurrently: bool = False + + +class Action(Generic[RunPayloadType, RunContextType, RunResultType]): + PAYLOAD_TYPE: typing.Type[RunActionPayload] = RunActionPayload + RUN_CONTEXT_TYPE: typing.Type[RunActionContext] = RunActionContext + RESULT_TYPE: typing.Type[RunActionResult] = RunActionResult + CONFIG: typing.Type[ActionConfig] = ActionConfig InitializeCallable = collections.abc.Callable[[], None] diff --git a/finecode_extension_api/finecode_extension_api/common_types.py b/finecode_extension_api/finecode_extension_api/common_types.py index 4fd0923..5a21f86 100644 --- a/finecode_extension_api/finecode_extension_api/common_types.py +++ b/finecode_extension_api/finecode_extension_api/common_types.py @@ -1,21 +1,25 @@ -from finecode_extension_api.code_action import BaseModel +import dataclasses -class Position(BaseModel): +@dataclasses.dataclass +class Position: line: int character: int -class Range(BaseModel): +@dataclasses.dataclass +class Range: start: Position end: Position -class TextDocumentIdentifier(BaseModel): +@dataclasses.dataclass +class TextDocumentIdentifier: uri: str -class TextDocumentItem(BaseModel): +@dataclasses.dataclass +class TextDocumentItem: uri: str language_id: str version: int diff --git a/presets/fine_python_format/fine_python_format/preset.toml b/presets/fine_python_format/fine_python_format/preset.toml index 06451ed..ec2babd 100644 --- a/presets/fine_python_format/fine_python_format/preset.toml +++ b/presets/fine_python_format/fine_python_format/preset.toml @@ -1,9 +1,15 @@ [tool.finecode.action.format] source = "finecode_extension_api.actions.format.FormatAction" handlers = [ - { name = "isort", source = "fine_python_isort.IsortFormatHandler" }, - { name = "black", source = "fine_python_black.BlackFormatHandler" }, - { name = "save", source = "finecode_extension_api.actions.format.SaveFormatHandler" }, + { name = "isort", source = "fine_python_isort.IsortFormatHandler", env = "dev_no_runtime", dependencies = [ + "fine_python_black==0.1.0", + ] }, + { name = "black", source = "fine_python_black.BlackFormatHandler", env = "dev_no_runtime", dependencies = [ + "fine_python_isort==0.1.0", + ] }, + { name = "save", source = "finecode_extension_api.actions.format.SaveFormatHandler", env = "dev_no_runtime", dependencies = [ + "finecode_extension_api==0.1.0", + ] }, ] [tool.finecode.action_handler.isort.config] diff --git a/presets/fine_python_format/pyproject.toml b/presets/fine_python_format/pyproject.toml index 5173275..b74b89f 100644 --- a/presets/fine_python_format/pyproject.toml +++ b/presets/fine_python_format/pyproject.toml @@ -5,7 +5,7 @@ description = "" authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }] readme = "README.md" requires-python = ">=3.11, < 3.14" -dependencies = ["fine_python_black==0.1.0", "fine_python_isort==0.1.0"] +dependencies = ["fine_extension_api==0.1.*"] [tool.poetry.group.dev.dependencies] finecode = { version = "0.2.0" } diff --git a/presets/fine_python_lint/fine_python_lint/preset.toml b/presets/fine_python_lint/fine_python_lint/preset.toml index eae1276..978178e 100644 --- a/presets/fine_python_lint/fine_python_lint/preset.toml +++ b/presets/fine_python_lint/fine_python_lint/preset.toml @@ -1,8 +1,13 @@ [tool.finecode.action.lint] source = "finecode_extension_api.actions.lint.LintAction" handlers = [ - { name = "flake8", source = "fine_python_flake8.Flake8LintHandler" }, - { name = "mypy", source = "fine_python_mypy.MypyLintHandler" }, + { name = "flake8", source = "fine_python_flake8.Flake8LintHandler", env = "dev_no_runtime", dependencies = [ + "fine_python_flake8==0.1.0", + "flake8-bugbear (>=24.12.12,<25.0.0)", + ] }, + { name = "mypy", source = "fine_python_mypy.MypyLintHandler", env = "dev_no_runtime", dependencies = [ + "fine_python_mypy==0.1.0", + ] }, ] [tool.finecode.action_handler.flake8.config] diff --git a/src/finecode/extension_runner/cli.py b/src/finecode/extension_runner/cli.py index a24790d..d6e6674 100644 --- a/src/finecode/extension_runner/cli.py +++ b/src/finecode/extension_runner/cli.py @@ -18,7 +18,8 @@ type=click.Path(exists=True, file_okay=False, resolve_path=True, path_type=Path), required=True, ) -def main(trace: bool, debug: bool, debug_port: int, project_path: Path): +@click.option("--env-name", "env_name", type=str, default="unknown") +def main(trace: bool, debug: bool, debug_port: int, project_path: Path, env_name: str): if debug is True: import debugpy @@ -36,7 +37,7 @@ def main(trace: bool, debug: bool, debug_port: int, project_path: Path): # extension runner doesn't stop with async start after closing LS client(WM). Use # sync start until this problem is solved - runner_start.start_runner_sync() + runner_start.start_runner_sync(env_name) if __name__ == "__main__": diff --git a/src/finecode/extension_runner/lsp_server.py b/src/finecode/extension_runner/lsp_server.py index 9b48ca0..4f89f6d 100644 --- a/src/finecode/extension_runner/lsp_server.py +++ b/src/finecode/extension_runner/lsp_server.py @@ -6,6 +6,7 @@ from __future__ import annotations import atexit +import dataclasses import json import time @@ -113,8 +114,10 @@ def send_partial_result( token: int | str, partial_result: code_action.RunActionResult ) -> None: logger.debug(f"Send partial result for {token}") + partial_result_dict = dataclasses.asdict(partial_result) + partial_result_json = json.dumps(partial_result_dict) server.progress( - types.ProgressParams(token=token, value=partial_result.model_dump_json()) + types.ProgressParams(token=token, value=partial_result_json) ) services.document_requester = document_requester diff --git a/src/finecode/extension_runner/services.py b/src/finecode/extension_runner/services.py index a611deb..4160947 100644 --- a/src/finecode/extension_runner/services.py +++ b/src/finecode/extension_runner/services.py @@ -9,6 +9,7 @@ from pathlib import Path from loguru import logger +from pydantic.dataclasses import dataclass as pydantic_dataclass from finecode.extension_runner import bootstrap, context, domain, global_state from finecode.extension_runner import ( @@ -120,42 +121,12 @@ def create_action_exec_info(action: domain.Action) -> domain.ActionExecInfo: logger.error(f"Error importing action type: {e}") raise e - # typing.TypeAliasType is available in Python 3.12+ - if hasattr(typing, "TypeAliasType") and not isinstance( - action_type_def, typing.TypeAliasType - ): - raise Exception("Action definition expected to be a type") - - action_type_alias = action_type_def.__value__ - - if not isinstance(action_type_alias, typing._GenericAlias): - raise Exception( - "Action definition expected to be an instantiation of" - " finecode_extension_api.code_action.Action type" - ) - - try: - unpack_with_action = next(iter(action_type_alias)) - except StopIteration: - raise Exception("Action type definition is invalid: no action type alias?") - - # typing.Unpack cannot used in isinstance: - # TypeError: typing.Unpack cannot be used with isinstance() - # if not isinstance(unpack_with_action,typing.Unpack): - # raise Exception("Action type definition is invalid: type alias is not unpack") - - if len(unpack_with_action.__args__) != 1: - raise Exception("Action type definition is invalid: expected 1 Action instance") - - action_generic_alias = unpack_with_action.__args__[0] - action_args = action_generic_alias.__args__ - - if len(action_args) != 3: - raise Exception( - "Action type definition is invalid: Action type expects 3 arguments" - ) + if not issubclass(action_type_def, code_action.Action): + raise Exception("Action class expected to be a subclass of finecode_extension_api.code_action.Action") - payload_type, run_context_type, _ = action_args + payload_type = action_type_def.PAYLOAD_TYPE + run_context_type = action_type_def.RUN_CONTEXT_TYPE + # TODO: validate that classes and correct subclasses? @@ -288,7 +259,8 @@ async def run_action( # TODO: catch validation errors payload: code_action.RunActionPayload | None = None if action_exec_info.payload_type is not None: - payload = action_exec_info.payload_type(**request.params) + payload_type_with_validation = pydantic_dataclass(action_exec_info.payload_type) + payload = payload_type_with_validation(**request.params) run_context: code_action.RunActionContext | None = None if action_exec_info.run_context_type is not None: @@ -297,6 +269,7 @@ async def run_action( known_args={"run_id": lambda _: run_id}, params_to_ignore=["self"], ) + run_context = action_exec_info.run_context_type(**constructor_args) # TODO: handler errors await run_context.init(initial_payload=payload) diff --git a/src/finecode/extension_runner/start.py b/src/finecode/extension_runner/start.py index 6a7e967..866009f 100644 --- a/src/finecode/extension_runner/start.py +++ b/src/finecode/extension_runner/start.py @@ -54,7 +54,7 @@ # await pygls_server_utils.start_io_async(server) -def start_runner_sync(): +def start_runner_sync(env_name: str) -> None: project_log_dir_path = project_dirs.get_project_dir(global_state.project_dir_path) logger.remove() # disable logging raw messages @@ -63,7 +63,7 @@ def start_runner_sync(): # ~~extension runner communicates with workspace manager with tcp, we can print logs # to stdout as well~~. See README.md logs.save_logs_to_file( - file_path=project_log_dir_path / "execution.log", + file_path=project_log_dir_path / f"execution_{env_name}.log", log_level=global_state.log_level, stdout=False, ) diff --git a/src/finecode/workspace_manager/cli_app/run.py b/src/finecode/workspace_manager/cli_app/run.py index e52c461..657d1bc 100644 --- a/src/finecode/workspace_manager/cli_app/run.py +++ b/src/finecode/workspace_manager/cli_app/run.py @@ -215,9 +215,9 @@ async def run_actions_in_running_project( ) run_tasks.append(run_task) except ExceptionGroup as eg: - for error in eg: + for error in eg.exceptions: logger.exception(error) - raise RunFailed(f"Running of action {action_name} failed") + raise RunFailed(f"Running of actions {actions} failed") for idx, run_task in enumerate(run_tasks): run_result = run_task.result() diff --git a/src/finecode/workspace_manager/config/collect_actions.py b/src/finecode/workspace_manager/config/collect_actions.py index 7ee0543..9f2435e 100644 --- a/src/finecode/workspace_manager/config/collect_actions.py +++ b/src/finecode/workspace_manager/config/collect_actions.py @@ -52,6 +52,8 @@ def _collect_actions_in_config( .get("action_handler", {}) .get(handler.name, {}) .get("config", {}), + env=handler.env, + dependencies=handler.dependencies ) for handler in action_def.handlers ], diff --git a/src/finecode/workspace_manager/config/config_models.py b/src/finecode/workspace_manager/config/config_models.py index 96b8cbf..7cb9597 100644 --- a/src/finecode/workspace_manager/config/config_models.py +++ b/src/finecode/workspace_manager/config/config_models.py @@ -32,6 +32,8 @@ class PresetDefinition(BaseModel): class ActionHandlerDefinition(BaseModel): name: str source: str + env: str + dependencies: list[str] class ActionDefinition(BaseModel): diff --git a/src/finecode/workspace_manager/config/read_configs.py b/src/finecode/workspace_manager/config/read_configs.py index faaecc9..97b6432 100644 --- a/src/finecode/workspace_manager/config/read_configs.py +++ b/src/finecode/workspace_manager/config/read_configs.py @@ -31,7 +31,7 @@ async def read_projects_in_dir( logger.debug(f"Skip '{def_file}' because it is config dump, not real project config") continue - status = domain.ProjectStatus.READY + status = domain.ProjectStatus.CONFIG_VALID actions: list[domain.Action] | None = None with open(def_file, "rb") as pyproject_file: @@ -40,12 +40,6 @@ async def read_projects_in_dir( if project_def.get("tool", {}).get("finecode", None) is None: status = domain.ProjectStatus.NO_FINECODE actions = [] - else: - # finecode config exists, check also finecode.sh - finecode_sh_path = def_file.parent / "finecode.sh" - - if not finecode_sh_path.exists(): - status = domain.ProjectStatus.NO_FINECODE_SH new_project = domain.Project( name=def_file.parent.name, @@ -73,13 +67,26 @@ async def read_project_config( finecode_raw_config = project_def.get("tool", {}).get("finecode", None) if finecode_raw_config: finecode_config = config_models.FinecodeConfig(**finecode_raw_config) + # all presets expected to be in `dev_no_runtime` environment + project_runners = ws_context.ws_projects_extension_runners[project.dir_path] + # TODO: can it be the case that there is no such runner? + dev_no_runtime_runner = project_runners['dev_no_runtime'] new_config = await collect_config_from_py_presets( presets_sources=[preset.source for preset in finecode_config.presets], def_path=project.def_path, - runner=ws_context.ws_projects_extension_runners[project.dir_path], + runner=dev_no_runtime_runner, ) _merge_projects_configs(project_def, new_config) + # add builtins if they are not overwritten + prepare_envs_action = domain.Action(name='prepare_envs', source='finecode_extension_api.actions.prepare_envs.PrepareEnvsAction', handlers=[domain.ActionHandler(name='prepare_venvs', source='fine_python_virtualenv.VirtualenvPrepareEnvHandler', config={}, env='dev_workspace', dependencies=['fine_python_venv==0.1.*'])], config={}) + add_action_to_config_if_new(project_def, prepare_envs_action) + + # add runtime dependency group if it's not explicitly declared + add_runtime_dependency_group_if_new(project_def) + + merge_handlers_dependencies_into_groups(project_def) + ws_context.ws_projects_raw_configs[project.dir_path] = project_def else: logger.info( @@ -294,3 +301,78 @@ def _merge_preset_configs(config1: dict[str, Any], config2: dict[str, Any]) -> N del config2["tool"]["finecode"]["action_handler"] del config2["tool"]["finecode"] + + +def add_action_to_config_if_new(raw_config: dict[str, Any], action: domain.Action) -> None: + # adds action to raw config if it is not defined yet. Existing action will be not + # overwritten + tool_config = add_or_get_dict_key_value(raw_config, 'tool', {}) + finecode_config = add_or_get_dict_key_value(tool_config, 'finecode', {}) + action_config = add_or_get_dict_key_value(finecode_config, 'action', {}) + if action.name not in action_config: + action_raw_dict = { + "source": action.source, + "handlers": [handler_to_dict(handler) for handler in action.handlers] + } + action_config[action.name] = action_raw_dict + + # example of action definition: + # [tool.finecode.action.text_document_inlay_hint] + # source = "finecode_extension_api.actions.ide.text_document_inlay_hint.TextDocumentInlayHintAction" + # handlers = [ + # { name = 'module_exports_inlay_hint', source = 'fine_python_module_exports.extension.get_document_inlay_hints', env = "dev_no_runtime", dependencies = [ + # "fine_python_module_exports @ git+https://github.com/finecode-dev/finecode.git#subdirectory=extensions/fine_python_module_exports", + # ] }, + # ] + + +def add_or_get_dict_key_value(dict_obj: dict[str, Any], key: str, default_value: Any) -> Any: + if key not in dict_obj: + value = default_value + dict_obj[key] = value + else: + value = dict_obj[key] + + return value + + +def handler_to_dict(handler: domain.ActionHandler) -> dict[str, str | list[str]]: + return { + "name": handler.name, + "source": handler.source, + "env": handler.env, + "dependencies": handler.dependencies + } + + +def add_runtime_dependency_group_if_new(project_config: dict[str, Any]) -> None: + runtime_dependencies = project_config.get('project', {}).get('dependencies', []) + + deps_groups = add_or_get_dict_key_value(project_config, 'dependency-groups', {}) + if 'runtime' not in deps_groups: + deps_groups['runtime'] = runtime_dependencies + + +def merge_handlers_dependencies_into_groups(project_config: dict[str, Any]) -> None: + # tool.finecode.action..handlers[x].dependencies + actions_dict = project_config.get('tool', {}).get('finecode', {}).get('action', {}) + if 'dependency-groups' not in project_config: + project_config['dependency-groups'] = {} + deps_groups = project_config['dependency-groups'] + + for action_info in actions_dict.values(): + action_handlers = action_info.get('handlers', []) + + for handler in action_handlers: + handler_env = handler.get('env', None) + if handler_env is None: + logger.warning(f'Handler {handler} has no env, skip it') + continue + deps = handler.get('dependencies', []) + + if handler_env not in deps_groups: + deps_groups[handler_env] = [] + + env_deps = deps_groups[handler_env] + # should we remove duplicates here? + env_deps += deps diff --git a/src/finecode/workspace_manager/context.py b/src/finecode/workspace_manager/context.py index 2cef2e7..f790b61 100644 --- a/src/finecode/workspace_manager/context.py +++ b/src/finecode/workspace_manager/context.py @@ -19,7 +19,8 @@ class WorkspaceContext: ws_projects: dict[Path, domain.Project] = field(default_factory=dict) # ws_projects_raw_configs: dict[Path, dict[str, Any]] = field(default_factory=dict) - ws_projects_extension_runners: dict[Path, ExtensionRunnerInfo] = field( + # > + ws_projects_extension_runners: dict[Path, dict[str, ExtensionRunnerInfo]] = field( default_factory=dict ) ignore_watch_paths: set[Path] = field(default_factory=set) diff --git a/src/finecode/workspace_manager/domain.py b/src/finecode/workspace_manager/domain.py index 0f98307..14e7ef9 100644 --- a/src/finecode/workspace_manager/domain.py +++ b/src/finecode/workspace_manager/domain.py @@ -4,6 +4,8 @@ from enum import Enum, auto from pathlib import Path +import ordered_set + class Preset: def __init__(self, source: str) -> None: @@ -11,10 +13,12 @@ def __init__(self, source: str) -> None: class ActionHandler: - def __init__(self, name: str, source: str, config: dict[str, typing.Any]): + def __init__(self, name: str, source: str, config: dict[str, typing.Any], env: str, dependencies: list[str]): self.name: str = name self.source: str = source self.config: dict[str, typing.Any] = config + self.env: str = env + self.dependencies: list[str] = dependencies class Action: @@ -55,15 +59,26 @@ def __str__(self) -> str: def __repr__(self) -> str: return str(self) + + @property + def envs(self) -> list[str]: + if self.actions is None: + raise ValueError("Actions are not collected yet") + + all_envs_set = ordered_set.OrderedSet([]) + for action in self.actions: + action_envs = [handler.env for handler in action.handlers] + all_envs_set |= ordered_set.OrderedSet(action_envs) + + return list(all_envs_set) class ProjectStatus(Enum): - READY = auto() + CONFIG_INVALID = auto() + # config valid, but no finecode in project NO_FINECODE = auto() - NO_FINECODE_SH = auto() - RUNNER_FAILED = auto() - RUNNING = auto() - EXITED = auto() + # config valid and finecode is used in project + CONFIG_VALID = auto() RootActions = list[str] diff --git a/src/finecode/workspace_manager/finecode_cmd.py b/src/finecode/workspace_manager/finecode_cmd.py index 5cd2114..92ff65f 100644 --- a/src/finecode/workspace_manager/finecode_cmd.py +++ b/src/finecode/workspace_manager/finecode_cmd.py @@ -1,13 +1,10 @@ from pathlib import Path -def get_finecode_cmd(project_path: Path) -> str: - sh_path = project_path / "finecode.sh" +def get_python_cmd(project_path: Path, env_name: str) -> str: + venv_python_path = project_path / ".venvs" / env_name / "bin" / "python" - if not sh_path.exists(): - raise ValueError(f"finecode.sh not found in project {project_path}") + if not venv_python_path.exists(): + raise ValueError(f"{env_name} venv not found in project {project_path}") - with open(sh_path, "r") as sh_file: - sh_cmd = sh_file.readline() - - return sh_cmd + return venv_python_path.as_posix() diff --git a/src/finecode/workspace_manager/lsp_server/endpoints/action_tree.py b/src/finecode/workspace_manager/lsp_server/endpoints/action_tree.py index 955184a..b3b3784 100644 --- a/src/finecode/workspace_manager/lsp_server/endpoints/action_tree.py +++ b/src/finecode/workspace_manager/lsp_server/endpoints/action_tree.py @@ -2,6 +2,7 @@ from pathlib import Path from loguru import logger +import ordered_set from pygls.lsp.server import LanguageServer from finecode.workspace_manager import context, domain @@ -37,7 +38,7 @@ def get_project_action_tree( project: domain.Project, ws_context: context.WorkspaceContext ) -> list[schemas.ActionTreeNode]: actions_nodes: list[schemas.ActionTreeNode] = [] - if project.status == domain.ProjectStatus.RUNNING: + if project.status == domain.ProjectStatus.CONFIG_VALID: assert project.actions is not None for action in project.actions: node_id = f"{project.dir_path.as_posix()}::{action.name}" @@ -67,7 +68,7 @@ def get_project_action_tree( ) else: logger.info( - f"Project is not running: {project.dir_path}, no actions will be shown" + f"Project has no valid config and finecode: {project.dir_path}, no actions will be shown" ) return actions_nodes @@ -175,10 +176,11 @@ async def __list_actions( # list ws dirs and first level # wait for start of all runners, this is required to be able to resolve presets - all_started_coros = [ - runner.initialized_event.wait() - for runner in ws_context.ws_projects_extension_runners.values() - ] + all_started_coros = [] + for envs in ws_context.ws_projects_extension_runners.values(): + # all presets are expected to be in `dev_no_runtime` env + dev_no_runtime_runner = envs['dev_no_runtime'] + all_started_coros.append(dev_no_runtime_runner.initialized_event.wait()) await asyncio.gather(*all_started_coros) nodes: list[schemas.ActionTreeNode] = create_node_list_for_ws(ws_context) @@ -279,19 +281,22 @@ async def __reload_action(action_node_id: str) -> None: action_name = splitted_action_id[1] try: - next(action for action in project.actions if action.name == action_name) + action = next(action for action in project.actions if action.name == action_name) except StopIteration as error: logger.error(f"Unexpected error, project or action not found: {error}") raise InternalError() - runner = global_state.ws_context.ws_projects_extension_runners[project_path] + all_handlers_envs = ordered_set.OrderedSet([handler.env for handler in action.handlers]) + for env in all_handlers_envs: + # parallel to speed up? + runner = global_state.ws_context.ws_projects_extension_runners[project_path][env] - try: - await runner_client.reload_action(runner, action_name) - except runner_client.BaseRunnerRequestException as error: - await user_messages.error( - f"Action {action_name} reload failed: {error.message}" - ) + try: + await runner_client.reload_action(runner, action_name) + except runner_client.BaseRunnerRequestException as error: + await user_messages.error( + f"Action {action_name} reload failed: {error.message}" + ) async def run_action( @@ -312,13 +317,19 @@ async def run_action( action_name = splitted_action_id[1] - response = await wm_services.run_action( - action_name=action_name, - params=request.params, - project_def=project_def, - ws_context=global_state.ws_context, - ) - return schemas.RunActionResponse(result=response.result) + try: + response = await wm_services.run_action( + action_name=action_name, + params=request.params, + project_def=project_def, + ws_context=global_state.ws_context, + ) + result = response.result + except wm_services.ActionRunFailed as exception: + logger.error(exception.message) + result = {} + + return schemas.RunActionResponse(result=result) async def notify_changed_action_node( diff --git a/src/finecode/workspace_manager/lsp_server/endpoints/diagnostics.py b/src/finecode/workspace_manager/lsp_server/endpoints/diagnostics.py index 689b4c6..3dad26b 100644 --- a/src/finecode/workspace_manager/lsp_server/endpoints/diagnostics.py +++ b/src/finecode/workspace_manager/lsp_server/endpoints/diagnostics.py @@ -10,16 +10,13 @@ from lsprotocol import types from finecode import pygls_types_utils -from finecode.workspace_manager import domain, project_analyzer, proxy_utils +from finecode.workspace_manager import domain, project_analyzer, proxy_utils, services, context from finecode.workspace_manager.lsp_server import global_state -from finecode.workspace_manager.runner import runner_client from finecode_extension_api.actions import lint as lint_action if TYPE_CHECKING: from pygls.lsp.server import LanguageServer - from finecode.workspace_manager.runner import runner_info - def map_lint_message_to_diagnostic( lint_message: lint_action.LintMessage, @@ -223,7 +220,7 @@ async def document_diagnostic( @dataclass class LintActionExecInfo: - runner: runner_info.ExtensionRunnerInfo + project_dir_path: Path action_name: str request_data: dict[str, str | list[str]] = field(default_factory=dict) @@ -238,7 +235,8 @@ async def run_workspace_diagnostic_with_partial_results( action_name="lint", params=exec_info.request_data, partial_result_token=partial_result_token, - runner=exec_info.runner, + project_dir_path=exec_info.project_dir_path, + ws_context=global_state.ws_context ) as response: async for partial_response in response: lint_subresult = lint_action.LintRunResult(**partial_response) @@ -283,17 +281,14 @@ async def workspace_diagnostic_with_partial_results( return types.WorkspaceDiagnosticReport(items=[]) -async def workspace_diagnostic_with_full_result(exec_infos: list[LintActionExecInfo]): +async def workspace_diagnostic_with_full_result(exec_infos: list[LintActionExecInfo], ws_context: context.WorkspaceContext): send_tasks: list[asyncio.Task] = [] try: async with asyncio.TaskGroup() as tg: for exec_info in exec_infos: + project = ws_context.ws_projects[exec_info.project_dir_path] task = tg.create_task( - runner_client.run_action( - runner=exec_info.runner, - action_name=exec_info.action_name, - params=exec_info.request_data, - ) + services.run_action(action_name=exec_info.action_name, params=exec_info.request_data, project_def=project, ws_context=ws_context, preprocess_payload=False) ) send_tasks.append(task) except ExceptionGroup as eg: @@ -332,9 +327,9 @@ async def _workspace_diagnostic( exec_info_by_project_dir_path: dict[Path, LintActionExecInfo] = {} for project_dir_path in relevant_projects_paths: - runner = global_state.ws_context.ws_projects_extension_runners[project_dir_path] + project = global_state.ws_context.ws_projects[project_dir_path] exec_info_by_project_dir_path[project_dir_path] = LintActionExecInfo( - runner=runner, action_name="lint" + project_dir_path=project_dir_path, action_name="lint" ) # find which runner is responsible for which files @@ -351,9 +346,9 @@ async def _workspace_diagnostic( for project_dir_path, files_for_runner in files_by_projects.items(): project = global_state.ws_context.ws_projects[project_dir_path] - if project.status != domain.ProjectStatus.RUNNING: + if project.status != domain.ProjectStatus.CONFIG_VALID: logger.warning( - f"Runner of project {project_dir_path} is not running," + f"Project {project_dir_path} has not valid configuration and finecode," " lint in it will not be executed" ) continue @@ -373,7 +368,7 @@ async def _workspace_diagnostic( exec_infos=exec_infos, partial_result_token=params.partial_result_token ) else: - return await workspace_diagnostic_with_full_result(exec_infos=exec_infos) + return await workspace_diagnostic_with_full_result(exec_infos=exec_infos, ws_context=global_state.ws_context) async def workspace_diagnostic( diff --git a/src/finecode/workspace_manager/lsp_server/endpoints/document_sync.py b/src/finecode/workspace_manager/lsp_server/endpoints/document_sync.py index 3c58020..5116830 100644 --- a/src/finecode/workspace_manager/lsp_server/endpoints/document_sync.py +++ b/src/finecode/workspace_manager/lsp_server/endpoints/document_sync.py @@ -7,7 +7,7 @@ from finecode.workspace_manager import domain from finecode.workspace_manager.lsp_server import global_state -from finecode.workspace_manager.runner import runner_client +from finecode.workspace_manager.runner import runner_client, runner_info async def document_did_open( @@ -34,14 +34,15 @@ async def document_did_open( try: async with asyncio.TaskGroup() as tg: for project_path in projects_paths: - runner = global_state.ws_context.ws_projects_extension_runners[ + runners_by_env = global_state.ws_context.ws_projects_extension_runners[ project_path ] - tg.create_task( - runner_client.notify_document_did_open( - runner=runner, document_info=document_info + for runner in runners_by_env.values(): + tg.create_task( + runner_client.notify_document_did_open( + runner=runner, document_info=document_info + ) ) - ) except ExceptionGroup as e: logger.error(f"Error while sending opened document: {e}") @@ -62,21 +63,26 @@ async def document_did_close( projects_paths = [ project_path for project_path, project in global_state.ws_context.ws_projects.items() - if project.status == domain.ProjectStatus.RUNNING + if project.status == domain.ProjectStatus.CONFIG_VALID and file_path.is_relative_to(project_path) ] try: async with asyncio.TaskGroup() as tg: for project_path in projects_paths: - runner = global_state.ws_context.ws_projects_extension_runners[ + runners_by_env = global_state.ws_context.ws_projects_extension_runners[ project_path ] - tg.create_task( - runner_client.notify_document_did_close( - runner=runner, document_uri=params.text_document.uri + for runner in runners_by_env.values(): + if runner.status != runner_info.RunnerStatus.RUNNING: + logger.trace(f"Runner {runner.working_dir_path} is not running, skip it") + continue + + tg.create_task( + runner_client.notify_document_did_close( + runner=runner, document_uri=params.text_document.uri + ) ) - ) except ExceptionGroup as e: logger.error(f"Error while sending closed document: {e}") diff --git a/src/finecode/workspace_manager/lsp_server/lsp_server.py b/src/finecode/workspace_manager/lsp_server/lsp_server.py index 8894727..ce25ea1 100644 --- a/src/finecode/workspace_manager/lsp_server/lsp_server.py +++ b/src/finecode/workspace_manager/lsp_server/lsp_server.py @@ -263,14 +263,14 @@ async def reset(ls: LanguageServer, params): async def restart_extension_runner(ls: LanguageServer, params): - logger.info(f"restart extension runner {params}") + logger.info(f"restart extension runners {params}") await global_state.server_initialized.wait() params_dict = params[0] runner_working_dir_str = params_dict["projectPath"] runner_working_dir_path = Path(runner_working_dir_str) - await wm_services.restart_extension_runner( + await wm_services.restart_extension_runners( runner_working_dir_path, global_state.ws_context ) diff --git a/src/finecode/workspace_manager/proxy_utils.py b/src/finecode/workspace_manager/proxy_utils.py index ca0c206..6f7e2c6 100644 --- a/src/finecode/workspace_manager/proxy_utils.py +++ b/src/finecode/workspace_manager/proxy_utils.py @@ -1,22 +1,21 @@ import asyncio import collections.abc import contextlib -from pathlib import Path +import pathlib from typing import Any from loguru import logger +import ordered_set -from finecode.workspace_manager import context, domain, find_project +from finecode.workspace_manager import context, domain, find_project, services +from finecode.workspace_manager.services import ActionRunFailed from finecode.workspace_manager.runner import manager as runner_manager from finecode.workspace_manager.runner import runner_client, runner_info -class ActionRunFailed(Exception): ... - - -def find_action_project_runner( - file_path: Path, action_name: str, ws_context: context.WorkspaceContext -) -> runner_info.ExtensionRunnerInfo: +def find_action_project( + file_path: pathlib.Path, action_name: str, ws_context: context.WorkspaceContext +) -> pathlib.Path: try: project_path = find_project.find_project_with_action_for_file( file_path=file_path, @@ -32,36 +31,34 @@ def find_action_project_runner( raise ActionRunFailed(error) project_status = ws_context.ws_projects[project_path].status - if project_status != domain.ProjectStatus.RUNNING: + if project_status != domain.ProjectStatus.CONFIG_VALID: logger.info( - f"Extension runner {project_path} is not running, " + f"Extension runner {project_path} has no valid config with finecode, " f"status: {project_status.name}" ) raise ActionRunFailed( - f"Extension runner {project_path} is not running, " + f"Project {project_path} has no valid config with finecode," f"status: {project_status.name}" ) - runner = ws_context.ws_projects_extension_runners[project_path] - return runner + return project_path async def find_action_project_and_run( - file_path: Path, + file_path: pathlib.Path, action_name: str, params: dict[str, Any], ws_context: context.WorkspaceContext, ) -> runner_client.RunActionResponse: - runner = find_action_project_runner( + project_path = find_action_project( file_path=file_path, action_name=action_name, ws_context=ws_context ) + project = ws_context.ws_projects[project_path] + try: - response = await runner_client.run_action( - runner=runner, action_name=action_name, params=params - ) - except runner_client.BaseRunnerRequestException as error: - logger.error(f"Error on running action {action_name} on {file_path}: {error.message}") - raise ActionRunFailed(error.message) + response = await services.run_action(action_name=action_name, params=params, project_def=project, ws_context=ws_context, preprocess_payload=False) + except services.ActionRunFailed as exception: + raise exception return response @@ -133,7 +130,7 @@ async def run_action_and_notify( runner: runner_info.ExtensionRunnerInfo, result_list: AsyncList, partial_results_task: asyncio.Task, -) -> None: +) -> runner_client.RunActionResponse: try: return await run_action_in_runner( action_name=action_name, @@ -163,11 +160,12 @@ async def run_with_partial_results( action_name: str, params: dict[str, Any], partial_result_token: int | str, - runner: runner_info.ExtensionRunnerInfo, + project_dir_path: pathlib.Path, + ws_context: context.WorkspaceContext ) -> collections.abc.AsyncIterator[ collections.abc.AsyncIterable[domain.PartialResultRawValue] ]: - logger.trace(f"Run {action_name} in runner {runner.working_dir_path}") + logger.trace(f"Run {action_name} in project {project_dir_path}") result: AsyncList[domain.PartialResultRawValue] = AsyncList() try: @@ -177,16 +175,22 @@ async def run_with_partial_results( result_list=result, partial_result_token=partial_result_token ) ) - tg.create_task( - run_action_and_notify( - action_name=action_name, - params=params, - partial_result_token=partial_result_token, - runner=runner, - result_list=result, - partial_results_task=partial_results_task, + project = ws_context.ws_projects[project_dir_path] + action = next(action for action in project.actions if action.name == 'lint') + action_envs = ordered_set.OrderedSet([handler.env for handler in action.handlers]) + runners_by_env = ws_context.ws_projects_extension_runners[project_dir_path] + for env in action_envs: + runner = runners_by_env[env] + tg.create_task( + run_action_and_notify( + action_name=action_name, + params=params, + partial_result_token=partial_result_token, + runner=runner, + result_list=result, + partial_results_task=partial_results_task, + ) ) - ) yield result except ExceptionGroup as eg: @@ -197,39 +201,39 @@ async def run_with_partial_results( @contextlib.asynccontextmanager async def find_action_project_and_run_with_partial_results( - file_path: Path, + file_path: pathlib.Path, action_name: str, params: dict[str, Any], partial_result_token: int | str, ws_context: context.WorkspaceContext, ) -> collections.abc.AsyncIterator[runner_client.RunActionRawResult]: logger.trace(f"Run {action_name} on {file_path}") - runner = find_action_project_runner( + project_path = find_action_project( file_path=file_path, action_name=action_name, ws_context=ws_context ) - return run_with_partial_results( action_name=action_name, params=params, partial_result_token=partial_result_token, - runner=runner, + project_dir_path=project_path, + ws_context=ws_context ) def find_all_projects_with_action( action_name: str, ws_context: context.WorkspaceContext -) -> list[Path]: +) -> list[pathlib.Path]: projects = ws_context.ws_projects - relevant_projects: dict[Path, domain.Project] = { + relevant_projects: dict[pathlib.Path, domain.Project] = { path: project for path, project in projects.items() if project.status != domain.ProjectStatus.NO_FINECODE } - # exclude not running projects and projects without requested action + # exclude projects without valid config and projects without requested action for project_dir_path, project_def in relevant_projects.copy().items(): - if project_def.status != domain.ProjectStatus.RUNNING: - # projects that are not running, have no actions. Files of those projects + if project_def.status != domain.ProjectStatus.CONFIG_VALID: + # projects without valid config have no actions. Files of those projects # will be not processed because we don't know whether it has one of expected # actions continue @@ -243,7 +247,7 @@ def find_all_projects_with_action( del relevant_projects[project_dir_path] continue - relevant_projects_paths: list[Path] = list(relevant_projects.keys()) + relevant_projects_paths: list[pathlib.Path] = list(relevant_projects.keys()) return relevant_projects_paths @@ -251,4 +255,6 @@ def find_all_projects_with_action( "find_action_project_and_run", "find_action_project_and_run_with_partial_results", "run_with_partial_results", + # reexport for easier use of proxy helpers + "ActionRunFailed" ] diff --git a/src/finecode/workspace_manager/runner/manager.py b/src/finecode/workspace_manager/runner/manager.py index a5b0fb8..dce6d7b 100644 --- a/src/finecode/workspace_manager/runner/manager.py +++ b/src/finecode/workspace_manager/runner/manager.py @@ -65,15 +65,17 @@ def map_change_object(change): async def start_extension_runner( - runner_dir: Path, ws_context: context.WorkspaceContext + runner_dir: Path, env_name: str, ws_context: context.WorkspaceContext ) -> runner_info.ExtensionRunnerInfo | None: + runner_info_instance = runner_info.ExtensionRunnerInfo( + working_dir_path=runner_dir, env_name=env_name, status=runner_info.RunnerStatus.READY_TO_START, initialized_event=asyncio.Event(), client=None + ) + try: - _finecode_cmd = finecode_cmd.get_finecode_cmd(runner_dir) + python_cmd = finecode_cmd.get_python_cmd(runner_dir, env_name) except ValueError: try: - ws_context.ws_projects[runner_dir].status = ( - domain.ProjectStatus.NO_FINECODE_SH - ) + runner_info_instance.status = runner_info.RunnerStatus.NO_VENV await notify_project_changed(ws_context.ws_projects[runner_dir]) except KeyError: ... @@ -82,6 +84,7 @@ async def start_extension_runner( process_args: list[str] = [ "--trace", f"--project-path={runner_dir.as_posix()}", + f"--env-name={env_name}" ] # TODO: config parameter for debug and debug port # if runner_dir == Path("/home/user/Development/FineCode/finecode"): @@ -91,17 +94,15 @@ async def start_extension_runner( process_args_str: str = " ".join(process_args) client = await create_lsp_client_io( runner_info.CustomJsonRpcClient, - f"{_finecode_cmd} -m finecode.extension_runner.cli {process_args_str}", + f"{python_cmd} -m finecode.extension_runner.cli {process_args_str}", runner_dir, ) - runner_info_instance = runner_info.ExtensionRunnerInfo( - working_dir_path=runner_dir, initialized_event=asyncio.Event(), client=client - ) + runner_info_instance.client = client async def on_exit(): logger.debug(f"Extension Runner {runner_info_instance.working_dir_path} exited") - ws_context.ws_projects[runner_dir].status = domain.ProjectStatus.EXITED - await notify_project_changed(ws_context.ws_projects[runner_dir]) + runner_info_instance.status = runner_info.RunnerStatus.EXITED + await notify_project_changed(ws_context.ws_projects[runner_dir]) # TODO: fix # TODO: restart if WM is not stopping runner_info_instance.client.server_exit_callback = on_exit @@ -172,9 +173,10 @@ def stop_extension_runner_sync(runner: runner_info.ExtensionRunnerInfo) -> None: async def kill_extension_runner(runner: runner_info.ExtensionRunnerInfo) -> None: - if runner.client._server is not None: - runner.client._server.terminate() - await runner.client.stop() + if runner.client is not None: + if runner.client._server is not None: + runner.client._server.terminate() + await runner.client.stop() async def update_runners(ws_context: context.WorkspaceContext) -> None: @@ -186,34 +188,49 @@ async def update_runners(ws_context: context.WorkspaceContext) -> None: # # this function should handle all possible statuses of projects and they either # start of fail to start, only projects without finecode are ignored - extension_runners = list(ws_context.ws_projects_extension_runners.values()) + extension_runners_paths = list(ws_context.ws_projects_extension_runners.keys()) new_dirs, deleted_dirs = dirs_utils.find_changed_dirs( [*ws_context.ws_projects.keys()], - [runner.working_dir_path for runner in extension_runners], + extension_runners_paths, ) for deleted_dir in deleted_dirs: - try: - runner_to_delete = next( - runner - for runner in extension_runners - if runner.working_dir_path == deleted_dir - ) - except StopIteration: - continue - await stop_extension_runner(runner_to_delete) - extension_runners.remove(runner_to_delete) + runners_by_env = ws_context.ws_projects_extension_runners[deleted_dir] + for runner in runners_by_env.values(): + await stop_extension_runner(runner) + del ws_context.ws_projects_extension_runners[deleted_dir] new_runners_tasks: list[asyncio.Task] = [] try: + # first start runner in 'dev_no_runtime' env to be able to resolve presets for + # other envs async with asyncio.TaskGroup() as tg: for new_dir in new_dirs: project = ws_context.ws_projects[new_dir] project_status = project.status - if project_status == domain.ProjectStatus.READY: - runner_task = tg.create_task(start_extension_runner(runner_dir=new_dir, ws_context=ws_context)) - new_runners_tasks.append(runner_task) + if project_status == domain.ProjectStatus.CONFIG_VALID: + task = tg.create_task(_start_dev_no_runtime_runner(project_def=project, ws_context=ws_context)) + new_runners_tasks.append(task) elif project_status != domain.ProjectStatus.NO_FINECODE: - raise RunnerFailedToStart(f"Runner for project '{project.name}' failed to start, status: {project_status.name}") + raise RunnerFailedToStart(f"Project '{project.name}' has invalid configuration, status: {project_status.name}") + + save_runners_from_tasks_in_context(tasks=new_runners_tasks, ws_context=ws_context) + + # only then start runners for all other envs + new_runners_tasks = [] + async with asyncio.TaskGroup() as tg: + for new_dir in new_dirs: + project = ws_context.ws_projects[new_dir] + project_status = project.status + if ws_context.ws_projects_extension_runners.get(new_dir, {}).get('dev_no_runtime', None) is None: + # start only if dev_no_runtime started successfully + for env in project.envs: + if env == 'dev_no_runtime': + # this env has already started above + continue + + runner_task = tg.create_task(start_extension_runner(runner_dir=new_dir, env_name=env, ws_context=ws_context)) + new_runners_tasks.append(runner_task) + except ExceptionGroup as eg: for exception in eg.exceptions: if isinstance(exception, runner_client.BaseRunnerRequestException) or isinstance(exception, RunnerFailedToStart): @@ -222,14 +239,11 @@ async def update_runners(ws_context: context.WorkspaceContext) -> None: logger.exception(exception) raise RunnerFailedToStart("Failed to start runner") - extension_runners += [ + save_runners_from_tasks_in_context(tasks=new_runners_tasks, ws_context=ws_context) + extension_runners: list[runner_info.ExtensionRunnerInfo] = [ runner.result() for runner in new_runners_tasks if runner is not None ] - ws_context.ws_projects_extension_runners = { - runner.working_dir_path: runner for runner in extension_runners - } - try: async with asyncio.TaskGroup() as tg: for runner in extension_runners: @@ -249,6 +263,31 @@ async def update_runners(ws_context: context.WorkspaceContext) -> None: raise RunnerFailedToStart("Failed to initialize runner") +async def _start_dev_no_runtime_runner(project_def: domain.Project, ws_context: context.WorkspaceContext) -> runner_info.ExtensionRunnerInfo: + runner = await start_extension_runner(runner_dir=project_def.dir_path, env_name='dev_no_runtime', ws_context=ws_context) + + if runner is None: + raise Exception("Runner failed to start") + + save_runner_in_context(runner=runner, ws_context=ws_context) + + # we cannot reuse '_init_runner' here because we need to start lsp client first, + # read config(=also resolve presets) and only then we can update runner config, + # because this requires resolved project config with presets + await _init_lsp_client(runner=runner, project=project_def) + + + await read_configs.read_project_config(project=project_def, ws_context=ws_context) + collect_actions.collect_actions( + project_path=project_def.dir_path, ws_context=ws_context + ) + + await _update_runner_config(runner=runner, project=project_def) + await _finish_runner_init(runner=runner, project=project_def, ws_context=ws_context) + + return runner + + async def _init_runner( runner: runner_info.ExtensionRunnerInfo, project: domain.Project, @@ -256,6 +295,15 @@ async def _init_runner( ) -> None: # initialization is required to be able to perform other requests logger.trace(f"Init runner {runner.working_dir_path}") + assert project.actions is not None + + await _init_lsp_client(runner=runner, project=project) + + await _update_runner_config(runner=runner, project=project) + await _finish_runner_init(runner=runner, project=project, ws_context=ws_context) + + +async def _init_lsp_client(runner: runner_info.ExtensionRunnerInfo, project: domain.Project) -> None: try: await runner_client.initialize( runner, @@ -264,7 +312,7 @@ async def _init_runner( client_version="0.1.0", ) except runner_client.BaseRunnerRequestException as error: - project.status = domain.ProjectStatus.RUNNER_FAILED + runner.status = runner_info.RunnerStatus.FAILED await notify_project_changed(project) runner.initialized_event.set() raise RunnerFailedToStart(f"Runner failed to initialize: {error.message}") @@ -273,7 +321,7 @@ async def _init_runner( await runner_client.notify_initialized(runner) except Exception as error: logger.error(f"Failed to notify runner about initialization: {error}") - project.status = domain.ProjectStatus.RUNNER_FAILED + runner.status = runner_info.RunnerStatus.FAILED await notify_project_changed(project) runner.initialized_event.set() logger.exception(error) @@ -281,21 +329,15 @@ async def _init_runner( f"Runner failed to notify about initialization: {error}" ) - logger.debug("LSP Server initialized") - - await read_configs.read_project_config(project=project, ws_context=ws_context) - collect_actions.collect_actions( - project_path=project.dir_path, ws_context=ws_context - ) + logger.debug("LSP Client initialized") - assert ( - project.actions is not None - ), f"Actions of project {project.dir_path} are not read yet" +async def _update_runner_config(runner: runner_info.ExtensionRunnerInfo, project: domain.Project) -> None: + assert project.actions is not None try: await runner_client.update_config(runner, project.actions) except runner_client.BaseRunnerRequestException as exception: - project.status = domain.ProjectStatus.RUNNER_FAILED + runner.status = runner_info.RunnerStatus.FAILED await notify_project_changed(project) runner.initialized_event.set() raise RunnerFailedToStart(f"Runner failed to update config: {exception.message}") @@ -304,7 +346,10 @@ async def _init_runner( f"Updated config of runner {runner.working_dir_path}," f" process id {runner.process_id}" ) - project.status = domain.ProjectStatus.RUNNING + + +async def _finish_runner_init(runner: runner_info.ExtensionRunnerInfo, project: domain.Project, ws_context: context.WorkspaceContext) -> None: + runner.status = runner_info.RunnerStatus.RUNNING await notify_project_changed(project) await send_opened_files( @@ -314,6 +359,22 @@ async def _init_runner( runner.initialized_event.set() +def save_runners_from_tasks_in_context(tasks: list[asyncio.Task], ws_context: context.WorkspaceContext) -> None: + extension_runners: list[runner_info.ExtensionRunnerInfo] = [ + runner.result() for runner in tasks if runner is not None + ] + + for new_runner in extension_runners: + save_runner_in_context(runner=new_runner, ws_context=ws_context) + + +def save_runner_in_context(runner: runner_info.ExtensionRunnerInfo, ws_context: context.WorkspaceContext) -> None: + if runner.working_dir_path not in ws_context.ws_projects_extension_runners: + ws_context.ws_projects_extension_runners[runner.working_dir_path] = {} + ws_context.ws_projects_extension_runners[runner.working_dir_path][runner.env_name] = runner + + + async def send_opened_files( runner: runner_info.ExtensionRunnerInfo, opened_files: list[domain.TextDocumentInfo] ): diff --git a/src/finecode/workspace_manager/runner/runner_info.py b/src/finecode/workspace_manager/runner/runner_info.py index 80abf40..ec81c2a 100644 --- a/src/finecode/workspace_manager/runner/runner_info.py +++ b/src/finecode/workspace_manager/runner/runner_info.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import enum import logging import shlex from dataclasses import dataclass @@ -65,14 +66,25 @@ async def server_exit(self, server): @dataclass class ExtensionRunnerInfo: working_dir_path: Path + env_name: str + status: RunnerStatus # NOTE: initialized doesn't mean the runner is running, check its status initialized_event: asyncio.Event - client: CustomJsonRpcClient + # e.g. if there is no venv for env, client can be None + client: CustomJsonRpcClient | None = None keep_running_request_task: asyncio.Task | None = None @property def process_id(self) -> int: - if self.client._server is not None: + if self.client is not None and self.client._server is not None: return self.client._server.pid else: return 0 + + +class RunnerStatus(enum.Enum): + READY_TO_START = enum.auto() + NO_VENV = enum.auto() + FAILED = enum.auto() + RUNNING = enum.auto() + EXITED = enum.auto() diff --git a/src/finecode/workspace_manager/services.py b/src/finecode/workspace_manager/services.py index 1a69aea..134da54 100644 --- a/src/finecode/workspace_manager/services.py +++ b/src/finecode/workspace_manager/services.py @@ -2,6 +2,7 @@ import typing from loguru import logger +import ordered_set from finecode.workspace_manager import ( context, @@ -9,49 +10,62 @@ payload_preprocessor, user_messages, ) -from finecode.workspace_manager.runner import manager as runner_manager +from finecode.workspace_manager.runner import manager as runner_manager, runner_info from finecode.workspace_manager.runner import runner_client -async def restart_extension_runner( +async def restart_extension_runners( runner_working_dir_path: pathlib.Path, ws_context: context.WorkspaceContext ) -> None: # TODO: reload config? try: - runner = ws_context.ws_projects_extension_runners[runner_working_dir_path] + runners_by_env = ws_context.ws_projects_extension_runners[runner_working_dir_path] except KeyError: logger.error(f"Cannot find runner for {runner_working_dir_path}") return - await runner_manager.stop_extension_runner(runner) - del ws_context.ws_projects_extension_runners[runner_working_dir_path] - - new_runner = await runner_manager.start_extension_runner( - runner_dir=runner_working_dir_path, ws_context=ws_context - ) - if new_runner is None: - logger.error("Extension runner didn't start") - return - - ws_context.ws_projects_extension_runners[runner_working_dir_path] = new_runner - await runner_manager._init_runner( - new_runner, - ws_context.ws_projects[runner.working_dir_path], - ws_context, - ) + new_runners_by_env: dict[str, runner_info.ExtensionRunnerInfo] = {} + for runner in runners_by_env.values(): + await runner_manager.stop_extension_runner(runner) + + new_runner = await runner_manager.start_extension_runner( + runner_dir=runner_working_dir_path, env_name=runner.env_name, ws_context=ws_context + ) + if new_runner is None: + logger.error("Extension runner didn't start") + continue + new_runners_by_env[runner.env_name] = new_runner + + ws_context.ws_projects_extension_runners[runner_working_dir_path] = new_runners_by_env + + # parallel? + for runner in new_runners_by_env.values(): + await runner_manager._init_runner( + runner, + ws_context.ws_projects[runner.working_dir_path], + ws_context, + ) def on_shutdown(ws_context: context.WorkspaceContext): - running_runners = [ - runner - for runner in ws_context.ws_projects_extension_runners.values() - if ws_context.ws_projects[runner.working_dir_path].status - == domain.ProjectStatus.RUNNING - ] + + running_runners = [] + for runners_by_env in ws_context.ws_projects_extension_runners.values(): + for runner in runners_by_env.values(): + if runner.status == runner_info.RunnerStatus.RUNNING: + running_runners.append(runner) + logger.trace(f"Stop all {len(running_runners)} running extension runners") for runner in running_runners: runner_manager.stop_extension_runner_sync(runner=runner) + + # TODO: stop MCP if running + + +class ActionRunFailed(Exception): + def __init__(self, message: str) -> None: + self.message = message RunResultFormat = runner_client.RunResultFormat @@ -64,36 +78,90 @@ async def run_action( project_def: domain.Project, ws_context: context.WorkspaceContext, result_format: RunResultFormat = RunResultFormat.JSON, + preprocess_payload: bool = True ) -> RunActionResponse: formatted_params = str(params) if len(formatted_params) > 100: formatted_params = f"{formatted_params[:100]}..." logger.trace(f"Execute action {action_name} with {formatted_params}") - if project_def.status != domain.ProjectStatus.RUNNING: - logger.error( - f"Extension runner is not running in {project_def.dir_path}." - " Please check logs." + if project_def.status != domain.ProjectStatus.CONFIG_VALID: + raise ActionRunFailed(f"Project {project_def.dir_path} has no valid configuration and finecode." + " Please check logs.") + + if preprocess_payload: + payload = payload_preprocessor.preprocess_for_project( + action_name=action_name, payload=params, project_dir_path=project_def.dir_path, + ws_context=ws_context + ) + else: + payload = params + + # cases: + # - base: all action handlers are in one env + # -> send `run_action` request to runner in env and let it handle concurrency etc. + # It could be done also in workspace manager, but handlers share run context + # - mixed envs: action handlers are in different envs + # -- concurrent execution of handlers + # -- sequential execution of handlers + assert project_def.actions is not None + action = next(action for action in project_def.actions if action.name == action_name) + all_handlers_envs = ordered_set.OrderedSet([handler.env for handler in action.handlers]) + all_handlers_are_in_one_env = len(all_handlers_envs) == 1 + + if all_handlers_are_in_one_env: + env_name = all_handlers_envs[0] + response = await _run_action_in_env_runner( + action_name=action_name, + payload=payload, + env_name=env_name, + project_def=project_def, + ws_context=ws_context, + result_format=result_format ) - return RunActionResponse(result={}, return_code=1) + else: + # TODO: concurrent vs sequential, this value should be taken from action config + run_concurrently = False # action_name == 'lint' + if run_concurrently: + ... + raise NotImplementedError() + else: + for handler in action.handlers: + # TODO: manage run context + response = await _run_action_in_env_runner( + action_name=action_name, + payload=payload, + env_name=handler.env, + project_def=project_def, + ws_context=ws_context, + result_format=result_format + ) - payload = payload_preprocessor.preprocess_for_project( - action_name=action_name, payload=params, project_dir_path=project_def.dir_path - ) + return response - # extension runner is running for this project, send command to it + +async def _run_action_in_env_runner( + action_name: str, + payload: dict[str, typing.Any], + env_name: str, + project_def: domain.Project, + ws_context: context.WorkspaceContext, + result_format: RunResultFormat = RunResultFormat.JSON, +): + runners_by_env = ws_context.ws_projects_extension_runners[project_def.dir_path] + runner = runners_by_env[env_name] + if runner.status != runner_info.RunnerStatus.RUNNING: + raise ActionRunFailed(f"Runner {env_name} in project {project_def.dir_path} is not running. Status: {runner.status}") + try: response = await runner_client.run_action( - runner=ws_context.ws_projects_extension_runners[project_def.dir_path], + runner=runner, action_name=action_name, params=payload, options={"result_format": result_format}, ) except runner_client.BaseRunnerRequestException as error: await user_messages.error(f"Action {action_name} failed: {error.message}") - if result_format == runner_client.RunResultFormat.JSON: - return RunActionResponse(result={}, return_code=1) - else: - return RunActionResponse(result="", return_code=1) - + raise ActionRunFailed(f"Action {action_name} failed: {error.message}") + return response diff --git a/tests/__testdata__/list_ws/cli_tool/finecode.sh b/tests/__testdata__/list_ws/cli_tool/finecode.sh deleted file mode 100644 index 176acc2..0000000 --- a/tests/__testdata__/list_ws/cli_tool/finecode.sh +++ /dev/null @@ -1 +0,0 @@ -poetry run python \ No newline at end of file diff --git a/tests/__testdata__/list_ws/ui_app/finecode.sh b/tests/__testdata__/list_ws/ui_app/finecode.sh deleted file mode 100644 index 176acc2..0000000 --- a/tests/__testdata__/list_ws/ui_app/finecode.sh +++ /dev/null @@ -1 +0,0 @@ -poetry run python \ No newline at end of file diff --git a/tests/__testdata__/nested_package/pyback/finecode.sh b/tests/__testdata__/nested_package/pyback/finecode.sh deleted file mode 100644 index 176acc2..0000000 --- a/tests/__testdata__/nested_package/pyback/finecode.sh +++ /dev/null @@ -1 +0,0 @@ -poetry run python \ No newline at end of file