diff --git a/agent/lsp/servers.py b/agent/lsp/servers.py index 90eeea708e..954db12ff2 100644 --- a/agent/lsp/servers.py +++ b/agent/lsp/servers.py @@ -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() diff --git a/tests/agent/lsp/test_install_and_lint_fixes.py b/tests/agent/lsp/test_install_and_lint_fixes.py index 731954f49d..ae8c04d063 100644 --- a/tests/agent/lsp/test_install_and_lint_fixes.py +++ b/tests/agent/lsp/test_install_and_lint_fixes.py @@ -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") diff --git a/tests/agent/lsp/test_python_discovery.py b/tests/agent/lsp/test_python_discovery.py index ee968e9d15..074f4f7991 100644 --- a/tests/agent/lsp/test_python_discovery.py +++ b/tests/agent/lsp/test_python_discovery.py @@ -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