mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 04:16:34 +00:00
Add BasedPyright as alternative LSP for Python (#1705)
This commit is contained in:
1 parent
2c10e25c97
commit
96ee9b65e2
10 files changed
+499
-12
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||
|
||||
* Language Servers:
|
||||
- Add `python_basedpyright` as an alternative Python language server
|
||||
|
||||
# v1.6.1 (2026-07-21)
|
||||
|
||||
* General:
|
||||
|
||||
@@ -126,10 +126,11 @@ Some languages require additional installations or setup steps, as noted.
|
||||
and the experimental [PHPantom](https://github.com/PHPantom-dev/phpantom_lsp) backend (language `php_phpantom`)
|
||||
* **Python**
|
||||
(by default, uses [Pyright](https://github.com/microsoft/pyright) (language `python`);
|
||||
alternatives: [ty](https://github.com/astral-sh/ty) (language `python_ty`),
|
||||
alternatives: [BasedPyright](https://github.com/DetachHead/basedpyright) (language `python_basedpyright`),
|
||||
[ty](https://github.com/astral-sh/ty) (language `python_ty`),
|
||||
[pyrefly](https://github.com/facebook/pyrefly) (language `python_pyrefly`),
|
||||
[Jedi](https://github.com/palotas/jedi-language-server) (language `python_jedi`);
|
||||
ty and pyrefly require `uv`/`uvx` in PATH)
|
||||
Pyright, BasedPyright, ty, and pyrefly require `uv`/`uvx` in PATH)
|
||||
* **QML**
|
||||
(requires Qt 6, provides `qmlls` or `qmlls6` on PATH; see the [Qt qmlls documentation](https://doc.qt.io/qt-6/qtqml-tool-qmlls.html))
|
||||
* **R**
|
||||
|
||||
@@ -969,16 +969,43 @@ Supported settings:
|
||||
|
||||
#### Python
|
||||
|
||||
Serena uses Pyright for the `python` language key.
|
||||
Serena supports several Python language servers through separate language keys.
|
||||
|
||||
##### Pyright (`python`)
|
||||
|
||||
Pyright is the default Python language server.
|
||||
|
||||
Supported settings:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `ls_path` | current Python executable | Override the Python interpreter Serena uses to run `-m pyright.langserver`. |
|
||||
| `pyright_version` | `1.1.403` | Override the exact Pyright package version Serena launches through `uvx` / `uv tool run`. |
|
||||
| `ls_path` | managed executable | Override `pyright-langserver` and bypass the managed `uvx` / `uv tool run` invocation. The default `--stdio` argument is still applied. |
|
||||
|
||||
Note:
|
||||
- There is currently no separate `python_ty` language key in Serena's current SolidLSP implementation.
|
||||
##### BasedPyright (`python_basedpyright`)
|
||||
|
||||
To use [BasedPyright](https://github.com/DetachHead/basedpyright), select its separate experimental
|
||||
language key:
|
||||
|
||||
```yaml
|
||||
languages: [python_basedpyright]
|
||||
ls_specific_settings:
|
||||
python_basedpyright:
|
||||
basedpyright_version: "1.39.9"
|
||||
```
|
||||
|
||||
Supported settings:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `basedpyright_version` | `1.39.9` | Override the exact BasedPyright package version Serena launches through `uvx` / `uv tool run`. |
|
||||
| `ls_path` | managed executable | Override `basedpyright-langserver` and bypass the managed `uvx` / `uv tool run` invocation. The default `--stdio` argument is still applied. |
|
||||
|
||||
The generic [language-server launch settings](override-ls-path), including `ls_base_cmd`, `ls_args`,
|
||||
and `ls_extra_args`, apply to both servers. `ls_args` replaces the default arguments, while
|
||||
`ls_extra_args` appends to them.
|
||||
|
||||
Other alternative Python language keys are `python_ty`, `python_pyrefly`, and `python_jedi`.
|
||||
|
||||
#### Ruby
|
||||
|
||||
|
||||
@@ -109,7 +109,10 @@ Some parts of Serena rely on `uv` / `uvx`.
|
||||
|
||||
One important detail is that `uvx` ignores the lockfile when installing directly from a Git repository. Because of that, we pin Serena's Python dependencies exactly in `pyproject.toml` so that installations from Git still resolve to exact dependency versions rather than floating ranges.
|
||||
|
||||
For the `ty` Python language server, Serena also uses an exact pinned version when invoking it through `uvx`.
|
||||
For the Pyright, BasedPyright, and `ty` Python language servers, Serena uses exact pinned versions when
|
||||
invoking them through `uvx` / `uv tool run`. Pyright and BasedPyright default to `1.1.403` and `1.39.9`,
|
||||
respectively; `ls_specific_settings.python.pyright_version` and
|
||||
`ls_specific_settings.python_basedpyright.basedpyright_version` can override those pins.
|
||||
|
||||
```{dropdown} What Serena Downloads by Default for Language Servers
|
||||
:open:
|
||||
@@ -162,11 +165,14 @@ All of the above are installed with exact pinned package versions by default, in
|
||||
|
||||
- **F#**: installs pinned `fsautocomplete` via `dotnet tool install`.
|
||||
- **Ruby (`ruby-lsp`)**: if not already available through Bundler or as a global executable, Serena installs a pinned `ruby-lsp` gem.
|
||||
- **Python (`ty`)**: launched through `uvx` / `uv x` using an exact pinned `ty` version.
|
||||
- **Python (`python`)**: Pyright is launched through `uvx` / `uv tool run` using an exact pinned version.
|
||||
Supplying `ls_path` bypasses this managed invocation.
|
||||
- **Python (`python_basedpyright`)**: BasedPyright is launched through `uvx` / `uv tool run` using an exact
|
||||
pinned version. Supplying `ls_path` bypasses this managed invocation.
|
||||
- **Python (`python_ty`)**: launched through `uvx` / `uv tool run` using an exact pinned `ty` version.
|
||||
- **HLSL on macOS**: if no prebuilt binary is used, Serena builds `shader_language_server` from a pinned version using Cargo.
|
||||
|
||||
### No Automatic Download by Serena
|
||||
|
||||
- **Python (`pyright`)**: Serena uses the locally available Python environment and starts `pyright.langserver` from there.
|
||||
- **Go (`gopls`)**, **Rust (`rust-analyzer`)**, and several other system-tool based integrations expect the language server to be available locally and do not download it automatically.
|
||||
```
|
||||
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Provides a standalone BasedPyright language server integration for Python.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from typing import cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
BASEDPYRIGHT_VERSION = "1.39.9"
|
||||
|
||||
|
||||
class BasedPyrightLanguageServer(SolidLanguageServer):
|
||||
"""Provides Python language support using BasedPyright."""
|
||||
|
||||
_TIMEOUT_FOR_INITIAL_ANALYSIS = 60.0
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a BasedPyrightLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
super().__init__(
|
||||
config,
|
||||
repository_root_path,
|
||||
None,
|
||||
str(config.code_language),
|
||||
solidlsp_settings,
|
||||
)
|
||||
|
||||
self.analysis_complete = threading.Event()
|
||||
self.found_source_files = False
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return LanguageServerDependencyProviderUvx(
|
||||
self._custom_settings,
|
||||
self._ls_resources_dir,
|
||||
package="basedpyright",
|
||||
entrypoint="basedpyright-langserver",
|
||||
default_version=BASEDPYRIGHT_VERSION,
|
||||
version_setting_key="basedpyright_version",
|
||||
extra_args=("--stdio",),
|
||||
)
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"]
|
||||
|
||||
def _create_base_initialize_params(self) -> dict:
|
||||
initialize_params = {
|
||||
"initializationOptions": {
|
||||
"exclude": [
|
||||
"**/__pycache__",
|
||||
"**/.venv",
|
||||
"**/.env",
|
||||
"**/build",
|
||||
"**/dist",
|
||||
"**/.pixi",
|
||||
],
|
||||
"reportMissingImports": "error",
|
||||
},
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"workspaceEdit": {"documentChanges": True},
|
||||
"didChangeConfiguration": {"dynamicRegistration": True},
|
||||
"didChangeWatchedFiles": {"dynamicRegistration": True},
|
||||
"symbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"executeCommand": {"dynamicRegistration": True},
|
||||
},
|
||||
"textDocument": {
|
||||
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
|
||||
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
|
||||
"signatureHelp": {
|
||||
"dynamicRegistration": True,
|
||||
"signatureInformation": {
|
||||
"documentationFormat": ["markdown", "plaintext"],
|
||||
"parameterInformation": {"labelOffsetSupport": True},
|
||||
},
|
||||
},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
},
|
||||
"publishDiagnostics": {"relatedInformation": True},
|
||||
},
|
||||
},
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""Starts BasedPyright and waits for initial workspace analysis to complete."""
|
||||
|
||||
def execute_client_command_handler(params: dict) -> list:
|
||||
return []
|
||||
|
||||
def do_nothing(params: dict) -> None:
|
||||
return
|
||||
|
||||
def window_log_message(msg: dict) -> None:
|
||||
message_text = msg.get("message", "")
|
||||
log.info("LSP: window/logMessage: %s", message_text)
|
||||
|
||||
if re.search(r"Found \d+ source files?", message_text):
|
||||
log.info("BasedPyright workspace scanning complete")
|
||||
self.found_source_files = True
|
||||
self.analysis_complete.set()
|
||||
|
||||
def handle_basedpyright_progress_notification(progress_kind: str, params: object | None) -> None:
|
||||
message_text = ""
|
||||
percentage: object | None = None
|
||||
if isinstance(params, dict):
|
||||
params_dict = cast("dict[str, object]", params)
|
||||
raw_message = params_dict.get("message")
|
||||
message_text = "" if raw_message is None else str(raw_message)
|
||||
percentage = params_dict.get("percentage")
|
||||
elif params is not None:
|
||||
message_text = str(params)
|
||||
|
||||
progress_label = f"{message_text} ({percentage}%)" if percentage is not None else message_text
|
||||
|
||||
if progress_kind == "begin":
|
||||
log.info("BasedPyright progress started: %s", progress_label)
|
||||
return
|
||||
|
||||
if progress_kind == "report":
|
||||
log.debug("BasedPyright progress update: %s", progress_label)
|
||||
return
|
||||
|
||||
log.info("BasedPyright progress finished: %s", progress_label)
|
||||
self.analysis_complete.set()
|
||||
|
||||
def basedpyright_begin_progress(params: object | None) -> None:
|
||||
handle_basedpyright_progress_notification("begin", params)
|
||||
|
||||
def basedpyright_report_progress(params: object | None) -> None:
|
||||
handle_basedpyright_progress_notification("report", params)
|
||||
|
||||
def basedpyright_end_progress(params: object | None) -> None:
|
||||
handle_basedpyright_progress_notification("end", params)
|
||||
|
||||
def check_experimental_status(params: dict) -> None:
|
||||
if params.get("quiescent") == True:
|
||||
log.info("Received experimental/serverStatus with quiescent=true")
|
||||
if not self.found_source_files:
|
||||
self.analysis_complete.set()
|
||||
|
||||
self.server.on_request("client/registerCapability", do_nothing)
|
||||
self.server.on_notification("language/status", do_nothing)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
# BasedPyright retains the Pyright progress-notification method names.
|
||||
self.server.on_notification("pyright/beginProgress", basedpyright_begin_progress)
|
||||
self.server.on_notification("pyright/reportProgress", basedpyright_report_progress)
|
||||
self.server.on_notification("pyright/endProgress", basedpyright_end_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
log.info("Starting basedpyright-langserver server process")
|
||||
self.server.start()
|
||||
|
||||
initialize_params = self._create_initialize_params()
|
||||
|
||||
log.info("Sending initialize request from LSP client to BasedPyright server and awaiting response")
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
log.info("Received initialize response from BasedPyright server: %s", init_response)
|
||||
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
log.info(
|
||||
"Waiting up to %ss for BasedPyright to complete initial workspace analysis...",
|
||||
self._TIMEOUT_FOR_INITIAL_ANALYSIS,
|
||||
)
|
||||
if self.analysis_complete.wait(timeout=self._TIMEOUT_FOR_INITIAL_ANALYSIS):
|
||||
log.info("BasedPyright initial analysis complete, server ready")
|
||||
else:
|
||||
log.warning("Timeout waiting for BasedPyright analysis completion, proceeding anyway")
|
||||
self.analysis_complete.set()
|
||||
@@ -194,6 +194,8 @@ class Language(str, Enum):
|
||||
"""Ty language server for Python (instead of pyright, which is the default)."""
|
||||
PYTHON_PYREFLY = "python_pyrefly"
|
||||
"""Pyrefly language server for Python (instead of pyright, which is the default)."""
|
||||
PYTHON_BASEDPYRIGHT = "python_basedpyright"
|
||||
"""BasedPyright language server for Python (instead of pyright, which is the default)."""
|
||||
CSHARP_OMNISHARP = "csharp_omnisharp"
|
||||
"""OmniSharp language server for C# (instead of the default csharp-ls by microsoft).
|
||||
Currently has problems with finding references, and generally seems less stable and performant.
|
||||
@@ -306,6 +308,7 @@ class Language(str, Enum):
|
||||
self.PYTHON_JEDI,
|
||||
self.PYTHON_TY,
|
||||
self.PYTHON_PYREFLY,
|
||||
self.PYTHON_BASEDPYRIGHT,
|
||||
self.CSHARP_OMNISHARP,
|
||||
self.RUBY_SOLARGRAPH,
|
||||
self.PHP_PHPACTOR,
|
||||
@@ -359,7 +362,7 @@ class Language(str, Enum):
|
||||
@cache
|
||||
def get_source_fn_matcher(self) -> FilenameMatcher:
|
||||
match self:
|
||||
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY | self.PYTHON_PYREFLY:
|
||||
case self.PYTHON | self.PYTHON_JEDI | self.PYTHON_TY | self.PYTHON_PYREFLY | self.PYTHON_BASEDPYRIGHT:
|
||||
return FilenameMatcher(".py", ".pyi")
|
||||
case self.JAVA:
|
||||
return FilenameMatcher(".java")
|
||||
@@ -609,6 +612,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.pyrefly_server import PyreflyLanguageServer
|
||||
|
||||
return PyreflyLanguageServer
|
||||
case self.PYTHON_BASEDPYRIGHT:
|
||||
from solidlsp.language_servers.basedpyright_server import BasedPyrightLanguageServer
|
||||
|
||||
return BasedPyrightLanguageServer
|
||||
case self.JAVA:
|
||||
from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
|
||||
|
||||
|
||||
+3
-1
@@ -47,13 +47,14 @@ _LANGUAGE_REPO_ALIASES: dict[Language, Language] = {
|
||||
Language.PHP_PHPACTOR: Language.PHP,
|
||||
Language.PHP_PHPANTOM: Language.PHP,
|
||||
Language.PYTHON_JEDI: Language.PYTHON,
|
||||
Language.PYTHON_BASEDPYRIGHT: Language.PYTHON,
|
||||
Language.PYTHON_TY: Language.PYTHON,
|
||||
Language.RUBY_SOLARGRAPH: Language.RUBY,
|
||||
Language.PYTHON_TY: Language.PYTHON,
|
||||
Language.PYTHON_PYREFLY: Language.PYTHON,
|
||||
}
|
||||
|
||||
PYTHON_LANGUAGE_BACKENDS = [Language.PYTHON, Language.PYTHON_TY]
|
||||
PYTHON_LANGUAGE_BACKENDS = [Language.PYTHON, Language.PYTHON_TY, Language.PYTHON_BASEDPYRIGHT]
|
||||
|
||||
|
||||
def get_repo_path(language: Language) -> Path:
|
||||
@@ -295,6 +296,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = {
|
||||
Language.PYTHON_JEDI: [pytest.mark.python],
|
||||
Language.PYTHON_TY: [pytest.mark.python],
|
||||
Language.PYTHON_PYREFLY: [pytest.mark.python],
|
||||
Language.PYTHON_BASEDPYRIGHT: [pytest.mark.python],
|
||||
Language.RUST: [pytest.mark.rust],
|
||||
Language.TYPESCRIPT: [pytest.mark.typescript],
|
||||
Language.BSL: [pytest.mark.bsl],
|
||||
|
||||
@@ -4,7 +4,12 @@ from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
|
||||
|
||||
PYTHON_BACKEND_LANGUAGES = [Language.PYTHON, Language.PYTHON_TY, Language.PYTHON_PYREFLY]
|
||||
PYTHON_BACKEND_LANGUAGES = [
|
||||
Language.PYTHON,
|
||||
Language.PYTHON_TY,
|
||||
Language.PYTHON_PYREFLY,
|
||||
Language.PYTHON_BASEDPYRIGHT,
|
||||
]
|
||||
|
||||
|
||||
def read_repo_file(language_server: SolidLanguageServer, relative_path: str) -> str:
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from test.conftest import start_ls_context
|
||||
from test.solidlsp.conftest import document_symbol_names
|
||||
|
||||
pytestmark = pytest.mark.python
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def basedpyright_project(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
project_root = tmp_path_factory.mktemp("basedpyright-project")
|
||||
files = {
|
||||
"pyproject.toml": """
|
||||
[tool.basedpyright]
|
||||
typeCheckingMode = "basic"
|
||||
reportExplicitAny = "error"
|
||||
""",
|
||||
"models.py": """
|
||||
class Greeter:
|
||||
def greet(self, name: str) -> str:
|
||||
return f"Hello, {name}"
|
||||
""",
|
||||
"service.py": """
|
||||
from models import Greeter
|
||||
|
||||
|
||||
def build_message(name: str) -> str:
|
||||
greeter = Greeter()
|
||||
return greeter.greet(name)
|
||||
""",
|
||||
"consumer.py": """
|
||||
from service import build_message
|
||||
|
||||
|
||||
MESSAGE = build_message("Ada")
|
||||
""",
|
||||
"diagnostics.py": """
|
||||
from typing import Any
|
||||
|
||||
|
||||
def echo(value: Any) -> Any:
|
||||
return value
|
||||
""",
|
||||
}
|
||||
for relative_path, content in files.items():
|
||||
(project_root / relative_path).write_text(dedent(content).lstrip(), encoding="utf-8")
|
||||
return project_root
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def basedpyright_language_server(basedpyright_project: Path) -> Iterator[SolidLanguageServer]:
|
||||
with start_ls_context(
|
||||
language=Language.PYTHON_BASEDPYRIGHT,
|
||||
repo_path=str(basedpyright_project),
|
||||
) as language_server:
|
||||
yield language_server
|
||||
|
||||
|
||||
def test_basedpyright_starts(
|
||||
basedpyright_language_server: SolidLanguageServer,
|
||||
basedpyright_project: Path,
|
||||
) -> None:
|
||||
assert basedpyright_language_server.language == Language.PYTHON_BASEDPYRIGHT
|
||||
assert basedpyright_language_server.is_running()
|
||||
assert Path(basedpyright_language_server.language_server.repository_root_path).resolve() == basedpyright_project.resolve()
|
||||
|
||||
|
||||
def test_basedpyright_document_symbols(basedpyright_language_server: SolidLanguageServer) -> None:
|
||||
assert "build_message" in document_symbol_names(basedpyright_language_server, "service.py")
|
||||
|
||||
|
||||
def test_basedpyright_definitions_and_references(basedpyright_language_server: SolidLanguageServer) -> None:
|
||||
defining_symbol = basedpyright_language_server.request_defining_symbol("service.py", 4, 15)
|
||||
assert defining_symbol is not None
|
||||
assert defining_symbol["name"] == "Greeter"
|
||||
assert defining_symbol["location"]["relativePath"] == "models.py"
|
||||
|
||||
symbols = basedpyright_language_server.request_document_symbols("models.py").get_all_symbols_and_roots()
|
||||
greeter_symbol = next(symbol for symbol in symbols[0] if symbol.get("name") == "Greeter")
|
||||
selection_start = greeter_symbol["selectionRange"]["start"]
|
||||
references = basedpyright_language_server.request_references(
|
||||
"models.py",
|
||||
selection_start["line"],
|
||||
selection_start["character"],
|
||||
)
|
||||
assert any(reference["uri"].endswith("/service.py") for reference in references), references
|
||||
|
||||
|
||||
def test_basedpyright_specific_diagnostics(basedpyright_language_server: SolidLanguageServer) -> None:
|
||||
diagnostics = basedpyright_language_server.request_text_document_diagnostics("diagnostics.py", min_severity=1)
|
||||
messages = [diagnostic["message"] for diagnostic in diagnostics]
|
||||
assert any("Any" in message for message in messages), messages
|
||||
@@ -0,0 +1,141 @@
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.dependency_provider import LanguageServerDependencyProviderUvx
|
||||
from solidlsp.language_servers.basedpyright_server import BASEDPYRIGHT_VERSION, BasedPyrightLanguageServer
|
||||
from solidlsp.language_servers.pyright_server import PyrightServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
def _make_basedpyright_server(
|
||||
tmp_path: Path,
|
||||
custom_settings: dict[str, object] | None = None,
|
||||
) -> BasedPyrightLanguageServer:
|
||||
settings = SolidLSPSettings(
|
||||
solidlsp_dir=str(tmp_path / "global"),
|
||||
project_data_path=str(tmp_path / "project"),
|
||||
ls_specific_settings={Language.PYTHON_BASEDPYRIGHT: custom_settings or {}},
|
||||
)
|
||||
server_interface = Mock()
|
||||
with patch.object(BasedPyrightLanguageServer, "_create_language_server_interface", return_value=server_interface):
|
||||
return BasedPyrightLanguageServer(
|
||||
LanguageServerConfig(code_language=Language.PYTHON_BASEDPYRIGHT),
|
||||
str(tmp_path),
|
||||
settings,
|
||||
)
|
||||
|
||||
|
||||
def _make_pyright_server(tmp_path: Path) -> PyrightServer:
|
||||
settings = SolidLSPSettings(
|
||||
solidlsp_dir=str(tmp_path / "global"),
|
||||
project_data_path=str(tmp_path / "project"),
|
||||
)
|
||||
server_interface = Mock()
|
||||
with patch.object(PyrightServer, "_create_language_server_interface", return_value=server_interface):
|
||||
return PyrightServer(LanguageServerConfig(code_language=Language.PYTHON), str(tmp_path), settings)
|
||||
|
||||
|
||||
def test_dependency_provider_uses_basedpyright_profile(tmp_path: Path) -> None:
|
||||
server = _make_basedpyright_server(tmp_path)
|
||||
provider = server._create_dependency_provider()
|
||||
|
||||
assert isinstance(provider, LanguageServerDependencyProviderUvx)
|
||||
assert provider._package == "basedpyright"
|
||||
assert provider._entrypoint == "basedpyright-langserver"
|
||||
assert provider._default_version == BASEDPYRIGHT_VERSION
|
||||
assert provider._version_setting_key == "basedpyright_version"
|
||||
|
||||
|
||||
def test_version_override_builds_uvx_command(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
custom_version = "1.99.0"
|
||||
server = _make_basedpyright_server(tmp_path, {"basedpyright_version": custom_version})
|
||||
provider = server._create_dependency_provider()
|
||||
monkeypatch.delenv("UVX", raising=False)
|
||||
|
||||
with patch("solidlsp.dependency_provider.shutil.which", return_value="/opt/bin/uvx"):
|
||||
command = provider.create_launch_command()
|
||||
|
||||
assert command == [
|
||||
"/opt/bin/uvx",
|
||||
"-p",
|
||||
"3.13",
|
||||
"--from",
|
||||
f"basedpyright=={custom_version}",
|
||||
"basedpyright-langserver",
|
||||
"--stdio",
|
||||
]
|
||||
|
||||
|
||||
def test_builds_uv_tool_run_fallback_command(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
server = _make_basedpyright_server(tmp_path)
|
||||
provider = server._create_dependency_provider()
|
||||
monkeypatch.delenv("UVX", raising=False)
|
||||
|
||||
def find_uv(executable: str) -> str | None:
|
||||
return None if executable == "uvx" else "/opt/bin/uv"
|
||||
|
||||
with patch("solidlsp.dependency_provider.shutil.which", side_effect=find_uv):
|
||||
command = provider.create_launch_command()
|
||||
|
||||
assert command == [
|
||||
"/opt/bin/uv",
|
||||
"tool",
|
||||
"run",
|
||||
"-p",
|
||||
"3.13",
|
||||
"--from",
|
||||
f"basedpyright=={BASEDPYRIGHT_VERSION}",
|
||||
"basedpyright-langserver",
|
||||
"--stdio",
|
||||
]
|
||||
|
||||
|
||||
def test_ls_path_preserves_default_and_extra_arguments(tmp_path: Path) -> None:
|
||||
server = _make_basedpyright_server(
|
||||
tmp_path,
|
||||
{
|
||||
"ls_path": "/custom/basedpyright-langserver",
|
||||
"ls_extra_args": ["--verbose"],
|
||||
},
|
||||
)
|
||||
|
||||
assert server._create_dependency_provider().create_launch_command() == [
|
||||
"/custom/basedpyright-langserver",
|
||||
"--stdio",
|
||||
"--verbose",
|
||||
]
|
||||
|
||||
|
||||
def test_base_command_and_args_overrides_are_preserved(tmp_path: Path) -> None:
|
||||
server = _make_basedpyright_server(
|
||||
tmp_path,
|
||||
{
|
||||
"ls_base_cmd": ["custom-launcher", "basedpyright-langserver"],
|
||||
"ls_args": ["--custom-stdio"],
|
||||
"ls_extra_args": ["--verbose"],
|
||||
},
|
||||
)
|
||||
|
||||
assert server._create_dependency_provider().create_launch_command() == [
|
||||
"custom-launcher",
|
||||
"basedpyright-langserver",
|
||||
"--custom-stdio",
|
||||
"--verbose",
|
||||
]
|
||||
|
||||
|
||||
def test_language_registry_uses_separate_server_classes() -> None:
|
||||
assert Language.PYTHON.get_ls_class() is PyrightServer
|
||||
assert Language.PYTHON_BASEDPYRIGHT.get_ls_class() is BasedPyrightLanguageServer
|
||||
|
||||
|
||||
def test_language_identity_separates_cache_directories(tmp_path: Path) -> None:
|
||||
pyright_server = _make_pyright_server(tmp_path)
|
||||
basedpyright_server = _make_basedpyright_server(tmp_path)
|
||||
|
||||
assert pyright_server.language == Language.PYTHON
|
||||
assert basedpyright_server.language == Language.PYTHON_BASEDPYRIGHT
|
||||
assert pyright_server.cache_dir != basedpyright_server.cache_dir
|
||||
Reference in new issue
Block a user