fix(lsp): select the analyzed project's Python first
Pyright selected the Hermes interpreter ahead of an explicit or local project environment. Keep PM as the fallback rather than hiding the project's dependencies. The regression drives the real server registry and spawn options with disposable venvs, then executes each selected interpreter. Correct the neighboring Windows pip fixture to patch the installer seam it uses. No language-server subprocess or user project was changed.
This commit is contained in:
@@ -165,13 +165,7 @@ def _spawn_pyright(root: str, ctx: ServerContext) -> Optional[SpawnSpec]:
|
||||
|
||||
|
||||
def _pm_store_python() -> Optional[str]:
|
||||
"""The pm-provisioned interpreter (facts + store layout), or None.
|
||||
|
||||
Under no-boot-through-venv the running process is the store python and
|
||||
``VIRTUAL_ENV`` is unset, so the old ambient-env probe had nothing to
|
||||
offer; pm's ``python`` fact names the store entry that owns the
|
||||
runtime. It takes the precedence the ambient ``VIRTUAL_ENV`` probe used
|
||||
to have; project-local candidates follow."""
|
||||
"""The PM interpreter used when the analyzed project has no environment."""
|
||||
try:
|
||||
from pm import paths
|
||||
from pm.lock import Facts
|
||||
@@ -189,14 +183,10 @@ def _pm_store_python() -> Optional[str]:
|
||||
|
||||
|
||||
def _detect_python(root: str) -> Optional[str]:
|
||||
# pm's store interpreter resolves to a full exe path (its layout is not
|
||||
# a venv), so it is checked directly rather than through the v/sub probe.
|
||||
pm_python = _pm_store_python()
|
||||
if pm_python:
|
||||
return pm_python
|
||||
# Pyright needs the project's dependencies, not Hermes's runtime packages.
|
||||
venvs = [v for v in (os.environ.get("VIRTUAL_ENV"), os.path.join(root, ".venv"), os.path.join(root, "venv")) if v]
|
||||
paths = (os.path.join(v, sub) for v in venvs for sub in ("bin/python", "bin/python3", "Scripts/python.exe"))
|
||||
return next((p for p in paths if os.path.exists(p)), None)
|
||||
return next((p for p in paths if os.path.exists(p)), None) or _pm_store_python()
|
||||
|
||||
|
||||
_warned_once: set = set()
|
||||
|
||||
@@ -81,15 +81,15 @@ def test_install_pip_finds_windows_scripts_launcher(tmp_path, monkeypatch):
|
||||
|
||||
from agent.lsp import install as install_mod
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
def fake_pip_install(args, **kwargs):
|
||||
scripts_dir = install_mod.hermes_lsp_bin_dir().parent / "python-packages" / "Scripts"
|
||||
scripts_dir.mkdir(parents=True, exist_ok=True)
|
||||
launcher = scripts_dir / "fake-language-server.exe"
|
||||
launcher.write_text("launcher\n")
|
||||
launcher.write_text("launcher\n", encoding="utf-8")
|
||||
launcher.chmod(0o755)
|
||||
return MagicMock(returncode=0, stderr="")
|
||||
|
||||
monkeypatch.setattr(install_mod.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr("hermes_cli.tools_config._pip_install", fake_pip_install)
|
||||
|
||||
resolved = install_mod._install_pip("fake-lsp", "fake-language-server")
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
"""LSP python discovery must resolve the pm store interpreter, not sniff
|
||||
VIRTUAL_ENV.
|
||||
|
||||
Under no-boot-through-venv the process is the store python
|
||||
(``sys.prefix == sys.base_prefix``) and ``VIRTUAL_ENV`` is unset in bundled
|
||||
installs, so the old ambient-env probe silently degraded to project-dir
|
||||
probing. pm's ``python`` fact (facts.json + store layout) is the authority.
|
||||
"""
|
||||
"""Pyright uses the project environment before the Hermes runtime fallback."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -81,3 +75,37 @@ def test_missing_store_entry_is_not_an_answer(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(store))
|
||||
|
||||
assert _pm_store_python() is None
|
||||
|
||||
|
||||
def test_pyright_uses_the_project_interpreter_before_hermes(tmp_path, monkeypatch):
|
||||
from agent.lsp import servers
|
||||
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||
monkeypatch.setattr(servers, "_pm_store_python", lambda: sys.executable)
|
||||
context = servers.ServerContext(
|
||||
workspace_root=str(project), install_strategy="off",
|
||||
binary_overrides={"pyright": [sys.executable]},
|
||||
)
|
||||
server = servers.find_server_for_file(str(project / "app.py"))
|
||||
assert server is not None
|
||||
spec = server.build_spawn(str(project), context)
|
||||
assert spec.initialization_options["python"]["pythonPath"] == sys.executable
|
||||
|
||||
for environment in (project / ".venv", tmp_path / "explicit-environment"):
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "venv", "--without-pip", str(environment)],
|
||||
check=True, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
||||
if environment.name == "explicit-environment":
|
||||
monkeypatch.setenv("VIRTUAL_ENV", str(environment))
|
||||
spec = server.build_spawn(str(project), context)
|
||||
selected = spec.initialization_options["python"]["pythonPath"]
|
||||
assert Path(selected) == python
|
||||
child = subprocess.run(
|
||||
[selected, "-I", "-c", "import sys; print(sys.prefix)"],
|
||||
check=True, capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
assert Path(child.stdout.strip()) == environment
|
||||
|
||||
Reference in New Issue
Block a user