|
4 | 4 | import subprocess
|
5 | 5 | import time
|
6 | 6 | from contextlib import ExitStack, closing, contextmanager
|
7 |
| -from dataclasses import dataclass |
| 7 | +from dataclasses import dataclass, field |
8 | 8 | from functools import partial
|
9 | 9 | from multiprocessing.connection import ConnectionWrapper, Listener
|
10 | 10 | from pathlib import Path
|
11 |
| -from typing import Any, ContextManager, Iterator, List, Tuple, Union |
| 11 | +from typing import Any, ContextManager, Dict, Iterator, List, Tuple, Union |
12 | 12 |
|
13 | 13 | from isolate.backends import (
|
14 | 14 | BasicCallable,
|
@@ -208,8 +208,8 @@ def start_process(
|
208 | 208 |
|
209 | 209 | def _get_python_env(self):
|
210 | 210 | return {
|
211 |
| - "PYTHONUNBUFFERED": "1", # We want to stream the logs as they come. |
212 | 211 | **os.environ,
|
| 212 | + "PYTHONUNBUFFERED": "1", # We want to stream the logs as they come. |
213 | 213 | }
|
214 | 214 |
|
215 | 215 | def _get_python_cmd(
|
@@ -247,26 +247,36 @@ def _parse_agent_and_log(self, line: str, level: LogLevel) -> None:
|
247 | 247 | self.log(line, level=level, source=source)
|
248 | 248 |
|
249 | 249 |
|
| 250 | +# TODO: should we actually merge this with PythonIPC since it is |
| 251 | +# simple enough and interchangeable? |
250 | 252 | @dataclass
|
251 |
| -class DualPythonIPC(PythonIPC): |
252 |
| - """A dual-environment Python IPC implementation that |
253 |
| - can run the agent process in an environment with its |
254 |
| - Python and also load the shared libraries from a different |
255 |
| - one. |
256 |
| -
|
257 |
| - The user of DualPythonIPC must ensure that the Python versions from |
258 |
| - both of these environments are the same. Using different versions is |
259 |
| - an undefined behavior. |
| 253 | +class ExtendedPythonIPC(PythonIPC): |
| 254 | + """A Python IPC implementation that can also inherit packages from |
| 255 | + other environments (e.g. a virtual environment that has the core |
| 256 | + requirements like `dill` can be inherited on a new environment). |
| 257 | +
|
| 258 | + The given extra_inheritance_paths should be a list of paths that |
| 259 | + comply with the sysconfig, and it should be ordered in terms of |
| 260 | + priority (e.g. the first path will be the most prioritized one, |
| 261 | + right after the current environment). So if two environments have |
| 262 | + conflicting versions of the same package, the first one present in |
| 263 | + the inheritance chain will be used. |
| 264 | +
|
| 265 | + This works by including the `site-packages` directory of the |
| 266 | + inherited environment in the `PYTHONPATH` when starting the |
| 267 | + agent process. |
260 | 268 | """
|
261 | 269 |
|
262 |
| - secondary_path: Path |
| 270 | + extra_inheritance_paths: List[Path] = field(default_factory=list) |
263 | 271 |
|
264 |
| - def _get_python_env(self): |
265 |
| - # We are going to use the primary environment to run the Python |
266 |
| - # interpreter, but at the same time we are going to inherit all |
267 |
| - # the packages from the secondary environment. |
268 |
| - |
269 |
| - # The search order is important, we want the primary path to |
270 |
| - # take precedence. |
271 |
| - python_path = python_path_for(self.environment_path, self.secondary_path) |
272 |
| - return {"PYTHONPATH": python_path, **super()._get_python_env()} |
| 272 | + def _get_python_env(self) -> Dict[str, str]: |
| 273 | + env_variables = super()._get_python_env() |
| 274 | + |
| 275 | + if self.extra_inheritance_paths: |
| 276 | + # The order here should reflect the order of the inheritance |
| 277 | + # where the actual environment already takes precedence. |
| 278 | + python_path = python_path_for( |
| 279 | + self.environment_path, *self.extra_inheritance_paths |
| 280 | + ) |
| 281 | + env_variables["PYTHONPATH"] = python_path |
| 282 | + return env_variables |
0 commit comments