diff --git a/src/isolate/connections/_local/_base.py b/src/isolate/connections/_local/_base.py index 4014375..907cfef 100644 --- a/src/isolate/connections/_local/_base.py +++ b/src/isolate/connections/_local/_base.py @@ -28,6 +28,28 @@ ConnectionType = TypeVar("ConnectionType") +def binary_path_for(*search_paths: Path) -> str: + """Return the binary search path for the given 'search_paths'. + It will be a combination of the 'bin/' folders in them and + the existing PATH environment variable.""" + + paths = [] + for search_path in search_paths: + path = sysconfig.get_path("scripts", vars={"base": search_path}) + paths.append(path) + # Some distributions (conda) might include both a 'bin' and + # a 'scripts' folder. + + auxilary_binary_path = search_path / "bin" + if path != auxilary_binary_path and auxilary_binary_path.exists(): + paths.append(str(auxilary_binary_path)) + + if "PATH" in os.environ: + paths.append(os.environ["PATH"]) + + return os.pathsep.join(paths) + + def python_path_for(*search_paths: Path) -> str: """Return the PYTHONPATH for the library paths residing in the given 'search_paths'. The order of the paths is @@ -98,13 +120,22 @@ def get_env_vars(self) -> Dict[str, str]: custom_vars = {} custom_vars[AGENT_SIGNATURE] = "1" custom_vars["PYTHONUNBUFFERED"] = "1" + + # NOTE: we don't have to manually set PYTHONPATH here if we are + # using a single environment since python will automatically + # use the proper path. if self.extra_inheritance_paths: # The order here should reflect the order of the inheritance # where the actual environment already takes precedence. - python_path = python_path_for( + custom_vars["PYTHONPATH"] = python_path_for( self.environment_path, *self.extra_inheritance_paths ) - custom_vars["PYTHONPATH"] = python_path + + # But the PATH must be always set since it will be not be + # automatically set by Python (think of this as ./venv/bin/activate) + custom_vars["PATH"] = binary_path_for( + self.environment_path, *self.extra_inheritance_paths + ) return { **os.environ, diff --git a/src/isolate/server/server.py b/src/isolate/server/server.py index b375930..9dde7b6 100644 --- a/src/isolate/server/server.py +++ b/src/isolate/server/server.py @@ -102,7 +102,7 @@ def Run( future = local_pool.submit( _proxy_to_queue, queue=messages, - connection=connection, + connection=cast(LocalPythonGRPC, connection), input=request.function, ) diff --git a/tests/test_backends.py b/tests/test_backends.py index 9fefb96..09fba4e 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -134,6 +134,17 @@ def test_invalid_project_building(self, tmp_path, monkeypatch): assert not environment.exists() + @pytest.mark.parametrize("executable", ["python"]) + def test_path_resolution(self, tmp_path, executable): + import shutil + + environment = self.get_project_environment(tmp_path, "example-binary") + environment_path = environment.create() + with environment.open_connection(environment_path) as connection: + executable_path = Path(connection.run(partial(shutil.which, executable))) + assert executable_path.exists() + assert executable_path.relative_to(environment_path) + class TestVirtualenv(GenericEnvironmentTests): @@ -151,6 +162,9 @@ class TestVirtualenv(GenericEnvironmentTests): "invalid-project": { "requirements": ["pyjokes==999.999.999"], }, + "example-binary": { + "requirements": [], + }, } creation_entry_point = ("virtualenv.cli_run", PermissionError) @@ -246,6 +260,9 @@ class TestConda(GenericEnvironmentTests): "r": { "packages": ["r-base=4.2.2"], }, + "example-binary": { + "packages": ["python"], + }, } creation_entry_point = ("subprocess.check_call", subprocess.SubprocessError) @@ -283,6 +300,14 @@ def test_local_python_environment(): local_env.destroy(connection_key) +def test_path_on_local(): + import shutil + + local_env = LocalPythonEnvironment() + with local_env.connect() as connection: + assert connection.run(partial(shutil.which, "python")) == shutil.which("python") + + def test_isolate_server_environment(isolate_server): environment = IsolateServer( host=isolate_server,