33import importlib .util
44import logging
55import pathlib
6+ import sys
67from typing import Any
78from 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.
0 commit comments