diff --git a/lib/esbonio/esbonio/server/features/sphinx_manager/config.py b/lib/esbonio/esbonio/server/features/sphinx_manager/config.py index 2e6187427..39d26828d 100644 --- a/lib/esbonio/esbonio/server/features/sphinx_manager/config.py +++ b/lib/esbonio/esbonio/server/features/sphinx_manager/config.py @@ -3,6 +3,7 @@ import importlib.util import logging import pathlib +import sys from typing import Any from typing import Optional @@ -60,6 +61,12 @@ class SphinxConfig: cwd: str = attrs.field(default="${scopeFsPath}") """The working directory to use.""" + fallback_env: str | None = attrs.field(default=None) + """Location of the fallback environment to use. + + Intended to be used by clients to handle the case where the user has not configured + ``python_command`` themselves.""" + python_path: list[pathlib.Path] = attrs.field(factory=list) """The value of ``PYTHONPATH`` to use when injecting the sphinx agent into the target environment""" @@ -89,8 +96,8 @@ def resolve( The fully resolved config object to use. If ``None``, a valid configuration could not be created. """ - python_path = self._resolve_python_path(logger) - if len(python_path) == 0: + python_command, python_path = self._resolve_python(logger) + if len(python_path) == 0 or len(python_command) == 0: return None cwd = self._resolve_cwd(uri, workspace, logger) @@ -109,7 +116,7 @@ def resolve( config_overrides=self.config_overrides, cwd=cwd, env_passthrough=self.env_passthrough, - python_command=self.python_command, + python_command=python_command, build_command=build_command, python_path=python_path, ) @@ -158,12 +165,25 @@ def _resolve_cwd( return None - def _resolve_python_path(self, logger: logging.Logger) -> list[pathlib.Path]: - """Return the list of paths to put on the sphinx agent's ``PYTHONPATH`` + def _resolve_python( + self, logger: logging.Logger + ) -> tuple[list[str], list[pathlib.Path]]: + """Return the python configuration to use when launching the sphinx agent. + + The first element of the returned tuple is the command to use when running the + sphinx agent. This could be as simple as the path to the python interpreter in a + particular virtual environment or a complex command such as + ``hatch -e docs run python``. Using the ``PYTHONPATH`` environment variable, we can inject additional Python - packages into the user's Python environment. This method will locate the - installation path of the sphinx agent and return it. + packages into the user's Python environment. This method also locates the + installation path of the sphinx agent and returns it in the second element of the + tuple. + + Finally, if the user has not configured a python environment and the client has + set the ``fallback_env`` option, this method will construct a command based on + the current interpreter to create an isolated environment based on + ``fallback_env``. Parameters ---------- @@ -172,19 +192,34 @@ def _resolve_python_path(self, logger: logging.Logger) -> list[pathlib.Path]: Returns ------- - List[pathlib.Path] - The list of paths to Python packages to inject into the sphinx agent's target - environment. If empty, the ``esbonio.sphinx_agent`` package was not found. + tuple[list[str], list[pathlib.Path]] + A tuple of the form ``(python_command, python_path)``. """ - if len(self.python_path) > 0: - return self.python_path - - if (sphinx_agent := get_module_path("esbonio.sphinx_agent")) is None: - logger.error("Unable to locate the sphinx agent") - return [] - - python_path = [sphinx_agent] - return python_path + if len(python_path := list(self.python_path)) == 0: + if (sphinx_agent := get_module_path("esbonio.sphinx_agent")) is None: + logger.error("Unable to locate the sphinx agent") + return [], [] + + python_path.append(sphinx_agent) + + if len(python_command := list(self.python_command)) == 0: + if self.fallback_env is None: + logger.error("No python command configured") + return [], [] + + if not (fallback_env := pathlib.Path(self.fallback_env)).exists(): + logger.error( + "Provided fallback environment %s does not exist", fallback_env + ) + return [], [] + + # Since the client has provided a fallback environment we can isolate the + # current Python interpreter from its environment and reuse it. + logger.debug("Using fallback environment") + python_path.append(fallback_env) + python_command.extend([sys.executable, "-S"]) + + return python_command, python_path def _resolve_build_command(self, uri: Uri, logger: logging.Logger) -> list[str]: """Return the ``sphinx-build`` command to use.