Skip to content

Commit

Permalink
Update ruff commands and pyproject dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
zackees committed Mar 25, 2024
1 parent 749aff5 commit 321fd98
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 22 deletions.
4 changes: 2 additions & 2 deletions lint
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ set -e
cd $( dirname ${BASH_SOURCE[0]})
. ./activate.sh
echo Running ruff src
ruff --fix src
ruff check --fix src
echo Running ruff tests
ruff --fix tests
ruff check --fix tests
echo Running black src tests
black src tests
echo Running isort src tests
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies = [
"wheel",
"filelock",
"semver",
"setuptools"
"setuptools",
]
# Change this with the version number bump.
version = "1.3.1"
Expand Down
13 changes: 10 additions & 3 deletions src/isolated_environment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@
# the python interpreter. On linux it will drop you into the
# python interpreter and you will not be able to exit.
def isolated_environment(
env_path: Union[Path, str], requirements: list[str] | None = None
env_path: Union[Path, str],
requirements: list[str] | None = None,
full_isolation: bool = False,
) -> dict[str, Any]:
"""Creates an isolated environment."""
if isinstance(env_path, str):
env_path = Path(env_path) # type: ignore
requirements = requirements or []
reqs = Requirements(requirements)
iso_env = IsolatedEnvironment(env_path, reqs)
iso_env = IsolatedEnvironment(
env_path=env_path, requirements=reqs, full_isolation=full_isolation
)
env = iso_env.environment()
return env

Expand All @@ -26,6 +30,7 @@ def isolated_environment_run(
env_path: Union[Path, str],
requirements: list[str] | None,
cmd_list: list[str],
full_isolation: bool = False,
**kwargs: Any,
) -> CompletedProcess:
"""
Expand All @@ -40,7 +45,9 @@ def isolated_environment_run(
env_path = Path(env_path) # type: ignore
requirements = requirements or []
reqs = Requirements(requirements)
iso_env = IsolatedEnvironment(env_path, reqs)
iso_env = IsolatedEnvironment(
env_path=env_path, requirements=reqs, full_isolation=full_isolation
)
iso_env.ensure_installed(reqs)
cp = iso_env.run(cmd_list, **kwargs)
return cp
Expand Down
66 changes: 55 additions & 11 deletions src/isolated_environment/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import warnings
from contextlib import contextmanager
from pathlib import Path
from shutil import which
from typing import Any, Iterator

from filelock import FileLock
Expand Down Expand Up @@ -43,9 +44,34 @@ def _create_virtual_env(env_path: Path) -> Path:
return env_path


def _get_activated_environment(env_path: Path) -> dict[str, str]:
def has_python_or_pip(path: str) -> bool:
"""Returns True if python or pip is in the path."""
python = which("python", path=path) or which("python3", path=path)
pip = which("pip", path=path) or which("pip3", path=path)
return (python is not None) or (pip is not None)


def _remove_python_paths_from_env(env: dict[str, str]) -> dict[str, str]:
"""Removes PYTHONPATH from the environment."""
out_env = env.copy()
if "PYTHONPATH" in out_env:
del out_env["PYTHONPATH"]
path_list = out_env["PATH"].split(os.pathsep)

exported_path_list: list[str] = []
for p in path_list:
if not has_python_or_pip(p):
exported_path_list.append(p)
exported_path_list = [os.path.basename(sys.executable)] + exported_path_list
out_env["PATH"] = os.pathsep.join(exported_path_list)
return out_env


def _get_activated_environment(env_path: Path, full_isolation: bool) -> dict[str, str]:
"""Gets the activate environment for the environment."""
out_env = os.environ.copy()
if full_isolation:
out_env = _remove_python_paths_from_env(out_env)
if sys.platform == "win32":
out_env["PATH"] = str(env_path / "Scripts") + ";" + out_env["PATH"]
else:
Expand All @@ -60,11 +86,11 @@ def _get_activated_environment(env_path: Path) -> dict[str, str]:


def _pip_install(
env_path: Path, package: str, build_options: str | None = None
env_path: Path, package: str, build_options: str | None, full_isolation: bool
) -> None:
"""Installs a package in the virtual environment."""
# Activate the environment and install packages
env = _get_activated_environment(env_path)
env = _get_activated_environment(env_path, full_isolation)
cmd_list = ["pip", "install", package]
if build_options:
cmd_list.extend(build_options.split(" "))
Expand All @@ -85,15 +111,18 @@ class IsolatedEnvironment:
"""An isolated environment."""

def __init__(
self, env_path: Path, requirements: Requirements | None = None
self,
env_path: Path,
requirements: Requirements | None = None,
full_isolation: bool = False, # For absolute isolation, set to False
) -> None:
self.env_path = env_path
self.full_isolation = full_isolation
self.env_path.mkdir(parents=True, exist_ok=True)
# file_lock is side-by-side with the environment.
self.file_lock = FileLock(str(env_path) + ".lock")
self.packages_json = env_path / "packages.json"
if requirements is not None:
self.ensure_installed(requirements)
self.ensure_installed(requirements or Requirements([]))

def install_environment(self) -> None:
"""Installs the environment."""
Expand Down Expand Up @@ -141,7 +170,10 @@ def lock(self) -> Iterator[None]:
self.file_lock.release()

def pip_install(
self, package: str | list[str], build_options: str | None = None
self,
package: str | list[str],
build_options: str | None,
full_isolation: bool,
) -> None:
"""Installs a package in the virtual environment."""
assert (
Expand All @@ -150,18 +182,18 @@ def pip_install(
reqs = self._read_reqs()
if isinstance(package, list):
for p in package:
_pip_install(self.env_path, p, build_options)
_pip_install(self.env_path, p, build_options, full_isolation)
reqs.add(package)
elif isinstance(package, str):
_pip_install(self.env_path, package, build_options)
_pip_install(self.env_path, package, build_options, full_isolation)
reqs.add(package)
else:
raise TypeError(f"Unknown type for package: {type(package)}")
self._write_reqs(reqs)

def environment(self) -> dict[str, str]:
"""Gets the activated environment, which should be applied to subprocess environments."""
return _get_activated_environment(self.env_path)
return _get_activated_environment(self.env_path, self.full_isolation)

def run(self, cmd_list: list[str], **kwargs) -> subprocess.CompletedProcess:
"""Runs a command in the environment."""
Expand All @@ -186,6 +218,14 @@ def run(self, cmd_list: list[str], **kwargs) -> subprocess.CompletedProcess:
text = kwargs.get("text", universal_newlines)
if "text" in kwargs:
del kwargs["text"]
scripts = "Scripts" if sys.platform == "win32" else "bin"
python_name = "python.exe" if sys.platform == "win32" else "python"
if cmd_list and (
cmd_list[0] == "python"
or cmd_list[0] == "python.exe"
or cmd_list[0] == "python3"
):
cmd_list[0] = str(self.env_path / scripts / python_name)
cp = subprocess.run(
cmd_list,
env=env,
Expand Down Expand Up @@ -248,7 +288,11 @@ def ensure_installed(self, reqs: Requirements) -> dict[str, Any]:
if req not in prev_reqs:
package_str = req.get_package_str()
build_options = req.build_options
self.pip_install(package=package_str, build_options=build_options)
self.pip_install(
package=package_str,
build_options=build_options,
full_isolation=self.full_isolation,
)
self._write_reqs(reqs)
return self.environment()

Expand Down
2 changes: 1 addition & 1 deletion test
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ set -e
# cd to self bash script directory
cd $( dirname ${BASH_SOURCE[0]})
. ./activate.sh
pytest
pytest -n auto -v tests
1 change: 1 addition & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Unit test file.
"""

import os
import unittest

Expand Down
18 changes: 18 additions & 0 deletions tests/test_data/inner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import sys

try:
from isolated_environment import api

print(api)
print("Successfully imported IsolatedEnvironment")
# print the path to find the isolated_environment module
# print(api.__file__)
print(f"IsolatedEnvironment path: {api.__file__}")
# print out the python path
# print(sys.path)
# print out the python executable
print(f"Python executable: {sys.executable}")
sys.exit(0)
except ImportError:
print("Failed to import IsolatedEnvironment")
sys.exit(1)
49 changes: 49 additions & 0 deletions tests/test_full_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Unit test file.
"""

import json
import os
import shutil
import subprocess
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any

from isolated_environment.api import IsolatedEnvironment

HERE = Path(__file__).parent.absolute()
TEST_DATA = HERE / "test_data"
INNER_PY = TEST_DATA / "inner.py"

assert INNER_PY.exists(), f"Missing: {INNER_PY}"


def pretty(data: Any) -> str:
"""Make JSON beautiful."""
return json.dumps(data, indent=4, sort_keys=True)


class FullIoslationTester(unittest.TestCase):
"""Main tester class."""

def test_ensure_installed(self) -> None:
"""Tests that ensure_installed works."""
with TemporaryDirectory() as tmp_dir:
prev_dir = os.getcwd()
os.chdir(tmp_dir)
shutil.copy(INNER_PY, tmp_dir)
try:
iso_env = IsolatedEnvironment(
Path(tmp_dir) / "venv", requirements=None, full_isolation=True
)
# now create an inner environment without the static-ffmpeg
cp: subprocess.CompletedProcess = iso_env.run(["python", "inner.py"])
self.assertEqual(1, cp.returncode)
finally:
os.chdir(prev_dir)


if __name__ == "__main__":
unittest.main()
5 changes: 3 additions & 2 deletions tests/test_iso_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ def test_ensure_installed(self) -> None:
self.assertEqual(installed_reqs, reqs)
try:
subprocess.check_output(
["static_ffmpeg", "--help"], env=env,
shell=True # shell=True is allowed only when NOT running python.
["static_ffmpeg", "--help"],
env=env,
shell=True, # shell=True is allowed only when NOT running python.
)
except subprocess.CalledProcessError as exc:
# doesn't fail on Windows, but it does on other platforms
Expand Down
1 change: 1 addition & 0 deletions tests/test_requirements.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Unit test file.
"""

import unittest

from isolated_environment.requirements import Requirements
Expand Down
6 changes: 4 additions & 2 deletions tests/test_run_py_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ def test_isolated_environment_run(self) -> None:
with TemporaryDirectory() as tmp_dir:
venv_path = Path(tmp_dir) / "venv"
cp = isolated_environment_run(
env_path=venv_path, requirements=[], cmd_list=["python", str(RUN_PY)],
capture_output=True
env_path=venv_path,
requirements=[],
cmd_list=["python", str(RUN_PY)],
capture_output=True,
)
self.assertEqual(0, cp.returncode)
self.assertEqual("Hello World!\n", cp.stdout)
Expand Down

0 comments on commit 321fd98

Please sign in to comment.