Notice that pip freeze for the invoking environment remains clean.
And this is what it looks like on disk:
| import sys | |
| import uuid | |
| import subprocess | |
| import os | |
| from subprocess import CalledProcessError | |
| from typing import List | |
| class RunContext: | |
| # venv_folder is the directory in which the venv dir will be created | |
| # python_executable is the python executable to use to create the venv, defaults to _invoking_ python executable | |
| def __init__(self, venv_folder: str, python_executable: str = sys.executable): | |
| random_guid = str(uuid.uuid4()) | |
| self.venv_dir = os.path.join(venv_folder, random_guid) | |
| # create venv | |
| subprocess.run([python_executable, "-m", "venv", self.venv_dir], check=True) # this will throw CalledProcessError if it fails | |
| # now save the python executable path | |
| if os.name == "nt": | |
| self.executable = os.path.join(self.venv_dir, "Scripts", f"python.exe") | |
| else: | |
| self.executable = os.path.abspath(os.path.join(self.venv_dir, "bin", f"python")) | |
| # probably should double check the path assumption on macs | |
| def run_command(self, command: List[str]): | |
| command.insert(0, self.executable) | |
| subprocess.run(command, check=True) # again, if anything goes wrong, we'll know. wrapper should handle CalledProcessError on failure | |
| if __name__ == "__main__": | |
| target_dir = "venv_dir" # this is bad, but for the sake of example this is just going to create a venv in the current directory/venv_dir | |
| run1 = RunContext(target_dir) | |
| try: | |
| run1.run_command(["-m", "pip", "install", "requests"]) | |
| except CalledProcessError as e: | |
| print(f"Failed to install requests into environment {run1.venv_dir} using executable {run1.executable}: {e}") | |