Skip to content

Commit b462426

Browse files
committed
lsp: Use fallback env when possible
If a client provides the `esbonio.sphinx.fallbackEnv`, repurpose the server's Python interpreter to launch the Sphinx agent.
1 parent cd77fde commit b462426

File tree

3 files changed

+118
-22
lines changed

3 files changed

+118
-22
lines changed

lib/esbonio/esbonio/server/features/sphinx_manager/config.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import importlib.util
44
import logging
55
import pathlib
6+
import sys
67
from typing import Any
78
from typing import Optional
89

@@ -60,6 +61,12 @@ class SphinxConfig:
6061
cwd: str = attrs.field(default="${scopeFsPath}")
6162
"""The working directory to use."""
6263

64+
fallback_env: str | None = attrs.field(default=None)
65+
"""Location of the fallback environment to use.
66+
67+
Intended to be used by clients to handle the case where the user has not configured
68+
``python_command`` themselves."""
69+
6370
python_path: list[pathlib.Path] = attrs.field(factory=list)
6471
"""The value of ``PYTHONPATH`` to use when injecting the sphinx agent into the
6572
target environment"""
@@ -89,8 +96,8 @@ def resolve(
8996
The fully resolved config object to use.
9097
If ``None``, a valid configuration could not be created.
9198
"""
92-
python_path = self._resolve_python_path(logger)
93-
if len(python_path) == 0:
99+
python_command, python_path = self._resolve_python(logger)
100+
if len(python_path) == 0 or len(python_command) == 0:
94101
return None
95102

96103
cwd = self._resolve_cwd(uri, workspace, logger)
@@ -109,7 +116,7 @@ def resolve(
109116
config_overrides=self.config_overrides,
110117
cwd=cwd,
111118
env_passthrough=self.env_passthrough,
112-
python_command=self.python_command,
119+
python_command=python_command,
113120
build_command=build_command,
114121
python_path=python_path,
115122
)
@@ -158,12 +165,25 @@ def _resolve_cwd(
158165

159166
return None
160167

161-
def _resolve_python_path(self, logger: logging.Logger) -> list[pathlib.Path]:
162-
"""Return the list of paths to put on the sphinx agent's ``PYTHONPATH``
168+
def _resolve_python(
169+
self, logger: logging.Logger
170+
) -> tuple[list[str], list[pathlib.Path]]:
171+
"""Return the python configuration to use when launching the sphinx agent.
172+
173+
The first element of the returned tuple is the command to use when running the
174+
sphinx agent. This could be as simple as the path to the python interpreter in a
175+
particular virtual environment or a complex command such as
176+
``hatch -e docs run python``.
163177
164178
Using the ``PYTHONPATH`` environment variable, we can inject additional Python
165-
packages into the user's Python environment. This method will locate the
166-
installation path of the sphinx agent and return it.
179+
packages into the user's Python environment. This method also locates the
180+
installation path of the sphinx agent and returns it in the second element of the
181+
tuple.
182+
183+
Finally, if the user has not configured a python environment and the client has
184+
set the ``fallback_env`` option, this method will construct a command based on
185+
the current interpreter to create an isolated environment based on
186+
``fallback_env``.
167187
168188
Parameters
169189
----------
@@ -172,19 +192,34 @@ def _resolve_python_path(self, logger: logging.Logger) -> list[pathlib.Path]:
172192
173193
Returns
174194
-------
175-
List[pathlib.Path]
176-
The list of paths to Python packages to inject into the sphinx agent's target
177-
environment. If empty, the ``esbonio.sphinx_agent`` package was not found.
195+
tuple[list[str], list[pathlib.Path]]
196+
A tuple of the form ``(python_command, python_path)``.
178197
"""
179-
if len(self.python_path) > 0:
180-
return self.python_path
181-
182-
if (sphinx_agent := get_module_path("esbonio.sphinx_agent")) is None:
183-
logger.error("Unable to locate the sphinx agent")
184-
return []
185-
186-
python_path = [sphinx_agent]
187-
return python_path
198+
if len(python_path := list(self.python_path)) == 0:
199+
if (sphinx_agent := get_module_path("esbonio.sphinx_agent")) is None:
200+
logger.error("Unable to locate the sphinx agent")
201+
return [], []
202+
203+
python_path.append(sphinx_agent)
204+
205+
if len(python_command := list(self.python_command)) == 0:
206+
if self.fallback_env is None:
207+
logger.error("No python command configured")
208+
return [], []
209+
210+
if not (fallback_env := pathlib.Path(self.fallback_env)).exists():
211+
logger.error(
212+
"Provided fallback environment %s does not exist", fallback_env
213+
)
214+
return [], []
215+
216+
# Since the client has provided a fallback environment we can isolate the
217+
# current Python interpreter from its environment and reuse it.
218+
logger.debug("Using fallback environment")
219+
python_path.append(fallback_env)
220+
python_command.extend([sys.executable, "-S"])
221+
222+
return python_command, python_path
188223

189224
def _resolve_build_command(self, uri: Uri, logger: logging.Logger) -> list[str]:
190225
"""Return the ``sphinx-build`` command to use.

lib/esbonio/tests/e2e/test_sphinx_manager.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,17 @@ async def test_get_client(
138138
async def test_get_client_with_error(
139139
server_manager: ServerManager, demo_workspace: Uri
140140
):
141-
"""Ensure that we correctly handle the case where there is an error with a client."""
141+
"""Ensure that we correctly handle the case where there is an error with the client."""
142142

143-
server, manager = server_manager(None)
143+
server, manager = server_manager(
144+
dict(
145+
esbonio=dict(
146+
sphinx=dict(
147+
pythonCommand=["/not/a/real/env/python"],
148+
),
149+
),
150+
),
151+
)
144152
# Ensure that the server is ready
145153
await server.ready
146154

@@ -165,7 +173,7 @@ async def test_get_client_with_error(
165173

166174
assert result is client
167175
assert client.state == ClientState.Errored
168-
assert "No python environment configured" in str(client.exception)
176+
assert "No such file or directory" in str(client.exception)
169177

170178
# Finally, if we request another uri from the same project we should get back
171179
# the same client instance - even though it failed to start.

lib/esbonio/tests/server/features/test_sphinx_config.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import os
33
import pathlib
4+
import sys
45
from typing import Optional
56

67
import pytest
@@ -19,6 +20,10 @@
1920
BUILD_CMD = ["sphinx-build", "-M", "html", "src", "dest"]
2021
PYPATH = [pathlib.Path("/path/to/site-packages/esbonio")]
2122

23+
# The value of FALLBACK_ENV must actually exist somewhere on the filesystem
24+
# But for the tests here, the actual location doesn't really matter
25+
FALLBACK_ENV = str(pathlib.Path(__file__).parent)
26+
2227

2328
def mk_uri(path: str) -> str:
2429
return str(Uri.for_file(path))
@@ -151,6 +156,54 @@ def mk_uri(path: str) -> str:
151156
),
152157
None,
153158
),
159+
( # If no python command provided, and no fallback env
160+
# available, the configuration is invalid
161+
"file:///path/to/workspace/file.rst",
162+
Workspace(None),
163+
SphinxConfig(
164+
python_command=[],
165+
build_command=BUILD_CMD,
166+
cwd=CWD,
167+
python_path=PYPATH,
168+
),
169+
None,
170+
),
171+
( # If no python command provided, but there is a fallback env
172+
# use that
173+
"file:///path/to/workspace/file.rst",
174+
Workspace(None),
175+
SphinxConfig(
176+
python_command=[],
177+
fallback_env=FALLBACK_ENV,
178+
build_command=BUILD_CMD,
179+
cwd=CWD,
180+
python_path=PYPATH,
181+
),
182+
SphinxConfig(
183+
python_command=[sys.executable, "-S"],
184+
build_command=BUILD_CMD,
185+
cwd=CWD,
186+
python_path=[PYPATH[0], pathlib.Path(FALLBACK_ENV)],
187+
),
188+
),
189+
( # If a fallback_env is available, but the user has provided their
190+
# own python_command, we should really use that.
191+
"file:///path/to/workspace/file.rst",
192+
Workspace(None),
193+
SphinxConfig(
194+
python_command=PYTHON_CMD,
195+
fallback_env=FALLBACK_ENV,
196+
build_command=BUILD_CMD,
197+
cwd=CWD,
198+
python_path=PYPATH,
199+
),
200+
SphinxConfig(
201+
python_command=PYTHON_CMD,
202+
build_command=BUILD_CMD,
203+
cwd=CWD,
204+
python_path=PYPATH,
205+
),
206+
),
154207
],
155208
)
156209
def test_resolve(

0 commit comments

Comments
 (0)