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:
Michael Panchenko committed 2026-05-26 15:34:37 +02:00
1 parent 6941af6aab
commit 1122166b7c
8 files changed
+124 -104

No files matched your search

+2
View File
@@ -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)
-2
View File
@@ -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",
+37
View File
@@ -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
+12 -10
View File
@@ -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 -24
View File
@@ -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
View File
@@ -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
Generated
-48
View File
@@ -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" },