mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
Remove pyright and fortls from serena-agent package, instead install with uvx on the fly
Added a dedicated LSDependencyProvider for that
This commit is contained in:
1 parent
6941af6aab
commit
1122166b7c
8 files changed
+124
-104
No files matched your search
@@ -4,6 +4,8 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
|
||||
* General:
|
||||
- Not existing paths return `False` on is ignored checks (instead of raising an error)
|
||||
- Add `serena-agent` CLI command so that `uvx serena-agent` can be used as entrypoint.
|
||||
- Fortls and pyright are now installed on the fly instead of being bundled in the serena-agent package.
|
||||
|
||||
# v1.5.1 (2026-05-18)
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"requests==2.33.0",
|
||||
"pyright==1.1.403",
|
||||
"fortls==3.2.2",
|
||||
"overrides==7.7.0",
|
||||
"python-dotenv==1.2.2",
|
||||
"mcp==1.27.0",
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass, replace
|
||||
@@ -154,6 +155,42 @@ class RuntimeDependencyCollection:
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_UVX_PYTHON_VERSION = "3.13"
|
||||
|
||||
|
||||
def build_uvx_launch_command(
|
||||
package: str,
|
||||
version: str,
|
||||
entrypoint: str,
|
||||
extra_args: Sequence[str] = (),
|
||||
python_version: str = DEFAULT_UVX_PYTHON_VERSION,
|
||||
) -> list[str]:
|
||||
"""Build a command that runs a pinned PyPI package's console script on demand via ``uvx`` / ``uv x``.
|
||||
|
||||
Resolution order:
|
||||
1. Prefer ``uvx`` (env var ``UVX`` or PATH lookup).
|
||||
2. Fall back to ``uv x`` if only ``uv`` is on PATH.
|
||||
3. Raise ``RuntimeError`` if neither is available.
|
||||
|
||||
:param package: PyPI package name (e.g. ``"pyright"``).
|
||||
:param version: Pinned package version.
|
||||
:param entrypoint: Console script provided by the package (e.g. ``"pyright-langserver"``).
|
||||
:param extra_args: Arguments appended after the entrypoint (e.g. ``("--stdio",)``).
|
||||
:param python_version: Python interpreter version passed via ``-p`` (uv will fetch it if missing).
|
||||
"""
|
||||
base_args = ["-p", python_version, "--from", f"{package}=={version}", entrypoint, *extra_args]
|
||||
|
||||
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
|
||||
if uvx_path is not None:
|
||||
return [uvx_path, *base_args]
|
||||
|
||||
uv_path = shutil.which("uv")
|
||||
if uv_path is not None:
|
||||
return [uv_path, "x", *base_args]
|
||||
|
||||
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH. Install uv (https://docs.astral.sh/uv/).")
|
||||
|
||||
|
||||
def build_npm_install_command(package_name: str, version: str, registry: str | None = None) -> list[str]:
|
||||
"""Build a pinned npm install command for a package in a Serena-managed install directory."""
|
||||
command = ["npm", "install", "--prefix", "./"]
|
||||
|
||||
@@ -6,19 +6,26 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import DocumentSymbols, LSPConstants, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls import (
|
||||
DocumentSymbols,
|
||||
LanguageServerDependencyProvider,
|
||||
LanguageServerDependencyProviderUvx,
|
||||
LSPConstants,
|
||||
LSPFileBuffer,
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
FORTLS_VERSION = "3.2.2"
|
||||
|
||||
|
||||
class FortranLanguageServer(SolidLanguageServer):
|
||||
"""Fortran Language Server implementation using fortls."""
|
||||
@@ -170,24 +177,17 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
|
||||
return DocumentSymbols(fixed_root_symbols)
|
||||
|
||||
@staticmethod
|
||||
def _check_fortls_installation() -> str:
|
||||
"""Check if fortls is available."""
|
||||
fortls_path = shutil.which("fortls")
|
||||
if fortls_path is None:
|
||||
raise RuntimeError("fortls is not installed or not in PATH.\nInstall it with: pip install fortls")
|
||||
return fortls_path
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
# Check fortls installation
|
||||
fortls_path = self._check_fortls_installation()
|
||||
super().__init__(config, repository_root_path, None, "fortran", solidlsp_settings)
|
||||
|
||||
# Command to start fortls language server
|
||||
# fortls uses stdio for LSP communication by default
|
||||
fortls_cmd = f"{fortls_path}"
|
||||
|
||||
super().__init__(
|
||||
config, repository_root_path, ProcessLaunchInfo(cmd=fortls_cmd, cwd=repository_root_path), "fortran", solidlsp_settings
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return LanguageServerDependencyProviderUvx(
|
||||
self._custom_settings,
|
||||
self._ls_resources_dir,
|
||||
package="fortls",
|
||||
entrypoint="fortls",
|
||||
default_version=FORTLS_VERSION,
|
||||
version_setting_key="fortls_version",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -6,19 +6,20 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from typing import cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
PYRIGHT_VERSION = "1.1.403"
|
||||
|
||||
|
||||
class PyrightServer(SolidLanguageServer):
|
||||
"""
|
||||
@@ -44,14 +45,15 @@ class PyrightServer(SolidLanguageServer):
|
||||
self.found_source_files = False
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
return sys.executable
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "-m", "pyright.langserver", "--stdio"]
|
||||
return LanguageServerDependencyProviderUvx(
|
||||
self._custom_settings,
|
||||
self._ls_resources_dir,
|
||||
package="pyright",
|
||||
entrypoint="pyright-langserver",
|
||||
default_version=PYRIGHT_VERSION,
|
||||
version_setting_key="pyright_version",
|
||||
extra_args=("--stdio",),
|
||||
)
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
|
||||
@@ -10,12 +10,11 @@ You can pass the following entries in ``ls_specific_settings["python_ty"]``:
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
from typing import cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
@@ -44,28 +43,15 @@ class TyLanguageServer(SolidLanguageServer):
|
||||
)
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProvider):
|
||||
def create_launch_command(self) -> list[str]:
|
||||
# respecting an explicit override
|
||||
ls_path = self._custom_settings.get("ls_path")
|
||||
if ls_path is not None:
|
||||
return [ls_path, "server"]
|
||||
|
||||
ty_version = self._custom_settings.get("ty_version", TY_VERSION)
|
||||
|
||||
# preferring uvx for on-demand execution
|
||||
uvx_path = os.environ.get("UVX") or shutil.which("uvx")
|
||||
if uvx_path is not None:
|
||||
return [uvx_path, "--from", f"ty=={ty_version}", "ty", "server"]
|
||||
|
||||
# falling back to uv's uvx-compatible subcommand when only `uv` is available
|
||||
uv_path = shutil.which("uv")
|
||||
if uv_path is not None:
|
||||
return [uv_path, "x", "--from", f"ty=={ty_version}", "ty", "server"]
|
||||
|
||||
raise RuntimeError("Could not find 'uvx' or 'uv' in PATH.\nInstall uv or provide ls_specific_settings.python_ty.ls_path.")
|
||||
return LanguageServerDependencyProviderUvx(
|
||||
self._custom_settings,
|
||||
self._ls_resources_dir,
|
||||
package="ty",
|
||||
entrypoint="ty",
|
||||
default_version=TY_VERSION,
|
||||
version_setting_key="ty_version",
|
||||
extra_args=("server",),
|
||||
)
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
|
||||
+44
-1
@@ -8,7 +8,7 @@ import shutil
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Hashable, Iterator
|
||||
from collections.abc import Callable, Hashable, Iterator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
from pathlib import Path, PurePath
|
||||
@@ -22,6 +22,7 @@ from sensai.util.string import ToStringMixin
|
||||
from serena.util.file_system import match_path
|
||||
from serena.util.text_utils import MatchedConsecutiveLines
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import build_uvx_launch_command
|
||||
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_process import LanguageServerInterface, StdioLanguageServer
|
||||
@@ -333,6 +334,48 @@ class LanguageServerDependencyProviderSinglePath(LanguageServerDependencyProvide
|
||||
"""
|
||||
|
||||
|
||||
class LanguageServerDependencyProviderUvx(LanguageServerDependencyProvider):
|
||||
"""
|
||||
Dependency provider for language servers distributed as a PyPI package, run on demand via ``uvx`` / ``uv x``.
|
||||
|
||||
The pinned package version can be overridden by the user via the LS-specific setting given by
|
||||
``version_setting_key``. Alternatively, the LS-specific setting "ls_path" can be set to the path of an
|
||||
already-installed language server executable, in which case it is launched directly, bypassing uv entirely.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
custom_settings: "SolidLSPSettings.CustomLSSettings",
|
||||
ls_resources_dir: str,
|
||||
*,
|
||||
package: str,
|
||||
entrypoint: str,
|
||||
default_version: str,
|
||||
version_setting_key: str,
|
||||
extra_args: Sequence[str] = (),
|
||||
):
|
||||
"""
|
||||
:param package: the PyPI package name (e.g. ``"pyright"``)
|
||||
:param entrypoint: the console script provided by the package (e.g. ``"pyright-langserver"``)
|
||||
:param default_version: the package version to pin unless overridden
|
||||
:param version_setting_key: the LS-specific setting key through which the user can override the version
|
||||
:param extra_args: arguments appended after the entrypoint (e.g. ``("--stdio",)``)
|
||||
"""
|
||||
super().__init__(custom_settings, ls_resources_dir)
|
||||
self._package = package
|
||||
self._entrypoint = entrypoint
|
||||
self._default_version = default_version
|
||||
self._version_setting_key = version_setting_key
|
||||
self._extra_args = tuple(extra_args)
|
||||
|
||||
def create_launch_command(self) -> list[str]:
|
||||
ls_path = self._custom_settings.get("ls_path")
|
||||
if ls_path is not None:
|
||||
return [ls_path, *self._extra_args]
|
||||
version = self._custom_settings.get(self._version_setting_key, self._default_version)
|
||||
return build_uvx_launch_command(self._package, version, self._entrypoint, self._extra_args)
|
||||
|
||||
|
||||
class SolidLanguageServer(ABC):
|
||||
"""
|
||||
High-level abstraction for language server interaction, which wraps the underlying low-level LSP interface
|
||||
|
||||
@@ -671,19 +671,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fortls"
|
||||
version = "3.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "json5" },
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c1/2b/db1e5cd07fc9e74a2e4fb8f65946f8fd79ef72211001af00982a04d977d5/fortls-3.2.2.tar.gz", hash = "sha256:b43b2b8cbd447ae848c63b8f008c2df96fd48c3a967b33f6ed64b3421496883b", size = 7085222, upload-time = "2025-03-16T18:03:11.377Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/7e/41a55a49e764243fd72b1ed202a95d9a9193586d8665bdc94e4df92050e4/fortls-3.2.2-py3-none-any.whl", hash = "sha256:b7ae8378ce9741064b8340d3f1fbbbf7ab697285e174a0208fed98485c4edfe3", size = 278359, upload-time = "2025-03-16T18:03:09.969Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitdb"
|
||||
version = "4.0.12"
|
||||
@@ -1135,15 +1122,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.26.0"
|
||||
@@ -1616,15 +1594,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overrides"
|
||||
version = "7.7.0"
|
||||
@@ -2235,19 +2204,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8", size = 50680, upload-time = "2025-11-14T10:07:23.331Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.403"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pystray"
|
||||
version = "0.19.5"
|
||||
@@ -2867,7 +2823,6 @@ dependencies = [
|
||||
{ name = "dotenv" },
|
||||
{ name = "filelock" },
|
||||
{ name = "flask" },
|
||||
{ name = "fortls" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "joblib" },
|
||||
{ name = "lsprotocol" },
|
||||
@@ -2877,7 +2832,6 @@ dependencies = [
|
||||
{ name = "psutil" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pygls" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pystray" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "python-multipart" },
|
||||
@@ -2938,7 +2892,6 @@ requires-dist = [
|
||||
{ name = "dotenv", specifier = "==0.9.9" },
|
||||
{ name = "filelock", specifier = "==3.25.2" },
|
||||
{ name = "flask", specifier = "==3.1.3" },
|
||||
{ name = "fortls", specifier = "==3.2.2" },
|
||||
{ name = "google-genai", marker = "extra == 'google'", specifier = "==1.27.0" },
|
||||
{ name = "jinja2", specifier = "==3.1.6" },
|
||||
{ name = "jinja2", marker = "extra == 'dev'", specifier = "==3.1.6" },
|
||||
@@ -2956,7 +2909,6 @@ requires-dist = [
|
||||
{ name = "pygls", specifier = "==2.1.1" },
|
||||
{ name = "pyinstrument", marker = "extra == 'dev'", specifier = "==5.1.1" },
|
||||
{ name = "pyjwt", marker = "extra == 'dev'", specifier = "==2.12.0" },
|
||||
{ name = "pyright", specifier = "==1.1.403" },
|
||||
{ name = "pystray", specifier = "==0.19.5" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = "==8.4.1" },
|
||||
{ name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" },
|
||||
|
||||
Reference in new issue
Block a user