| """Script to help with the creation of python virtual envs.""" |
| import glob |
| import logging |
| import os |
| from pathlib import Path |
| import sys |
| from typing import Iterable |
| |
| DEFAULT_LOCAL_PACKAGES = 'python_packages' |
| VENV_RECIPE_PY2 = '~/venv_recipe/bin' |
| |
| |
| def _get_pip(venv_root): |
| return os.path.join(venv_root, 'bin', 'pip') |
| |
| |
| def add_import_paths(venv_root: Path, paths: Iterable[Path]) -> None: |
| """Add an entry into the custom import locations for a venv. |
| |
| This will create a python path file in the provided virtual |
| environment such that python can import modules from |
| the provided path. |
| |
| Args: |
| venv_root: The root directory of a virtualenv to install into. |
| paths: The paths to add |
| """ |
| venv_lib = os.path.join(venv_root, 'lib', '*') |
| for version_lib in glob.glob(venv_lib): |
| pathfile = os.path.join(version_lib, 'site-packages', 'google.pth') |
| try: |
| with open(pathfile, 'r') as f: |
| content = f.read() |
| old_paths = [Path(p) for p in content.split('\n') if p] |
| new_paths = sorted(old_paths + paths) |
| except IOError: |
| new_paths = paths |
| with open(pathfile, 'w') as f: |
| f.write('\n'.join(str(p) for p in new_paths)) |
| logging.info('Wrote the following paths to %s: %s', pathfile, paths) |
| |
| |
| def create_virtual_env( |
| executor, directory, gcs_dir, local_packages=None, py3=False): |
| """Creates a python virtual env. |
| |
| Args: |
| executor: Executes the subprocess. |
| directory: Directory to execute the subprocess in. |
| gcs_dir: The directory that mirrors the gcs bucket |
| local_packages: Relative path to python packages under ~/gcs-mirror |
| py3: if True use python3 |
| |
| Returns: |
| The path to the new virtualenv directory root. |
| """ |
| python_sandbox_dir = os.path.join(directory, 'python-sandbox') |
| env = {} |
| if py3: |
| cmd = ['python3', '-m', 'venv', python_sandbox_dir] |
| else: |
| cmd = ['python2', '-m', 'virtualenv', |
| '--extra-search-dir={}'.format( |
| _get_find_links_path(gcs_dir, local_packages)), |
| '--no-download', |
| python_sandbox_dir] |
| env['PATH'] = os.pathsep.join([ |
| os.path.expanduser(VENV_RECIPE_PY2), os.getenv('PATH')]) |
| executor.exec_subprocess(cmd, env=env, check_output=True) |
| return python_sandbox_dir |
| |
| |
| def install_package(executor, venv_root, path, gcs_dir, local_packages=None): |
| """Install a local package from disk. |
| |
| Args: |
| executor: Executes the subprocess. |
| venv_root: The root directory of a virtualenv to install into. |
| path: The path to the package on local disk |
| gcs_dir: The path to the gcs mirror directory. |
| local_packages: Relative path to python packages under ~/gcs-mirror |
| """ |
| # Perform necessary default package upgrades. |
| _update_default_deps( |
| executor, |
| venv_root, |
| ['pip', 'wheel', 'setuptools'], |
| local_packages, |
| gcs_dir |
| ) |
| |
| venv_pip = _get_pip(venv_root) |
| |
| executor.exec_subprocess( |
| [venv_pip, 'install', '-e', path], |
| check_output=True) |
| |
| |
| def install_py_deps( |
| executor, venv_root, requirements, gcs_dir, local_packages=None): |
| """Install the python dependencies. |
| |
| Args: |
| executor: Executes the subprocess. |
| venv_root: The root directory of a virtualenv to install |requirements| in. |
| requirements: The path to the requirements.txt to install. |
| gcs_dir: The path to the gcs mirror directory. |
| local_packages: Relative path to python packages under ~/gcs-mirror |
| """ |
| if not os.path.isfile(requirements): |
| logging.warning('No requirements file found: %s', requirements) |
| return |
| |
| # Perform necessary default package upgrades. |
| _update_default_deps( |
| executor, |
| venv_root, |
| ['pip', 'wheel', 'setuptools'], |
| local_packages, |
| gcs_dir |
| ) |
| |
| venv_pip = _get_pip(venv_root) |
| |
| executor.exec_subprocess( |
| [venv_pip, 'install', '--no-index', |
| '--find-links=file://{}'.format( |
| _get_find_links_path(gcs_dir, local_packages)), |
| '-r', requirements], |
| check_output=True) |
| |
| |
| def _update_default_deps(executor, venv_root, deps, local_packages, gcs_dir): |
| """Update the pip packages required to install dependencies. |
| |
| Args: |
| executor: Executes the subprocess. |
| venv_root: The root directory of the virtualenv. |
| deps: List of pip packages to update. |
| local_packages: Relative path to python packages under ~/gcs-mirror. |
| gcs_dir: The path to the gcs mirror directory. |
| """ |
| venv_pip = _get_pip(venv_root) |
| for dep in deps: |
| executor.exec_subprocess( |
| [venv_pip, 'install', '--no-index', |
| '--find-links=file://{}'.format( |
| _get_find_links_path(gcs_dir, local_packages)), |
| '--upgrade', dep], |
| check_output=True) |
| |
| |
| def exec_within_virtual_env( |
| executor, venv_root, cmd, env=None): |
| """Executes |cmd| within a python virtualenv. |
| |
| Args: |
| executor: Executes the subprocess. |
| venv_root: The root directory of a virtualenv to run in. |
| cmd: The command list to pass to virtualenv python interpreter. This will |
| be prepended with virtualenv python, so it should not start with python. |
| (e.g. If you want to run `python my_python_script.py --flag1 --arg=3` |
| then pass cmd = ['my_python_script.py', '--flag1', '--arg=3'].) |
| env: A dictionary with additional environment variables. |
| |
| Returns: |
| returncode, stdout, stderr from the subprocess. |
| """ |
| venv_python = os.path.join(venv_root, 'bin', 'python') |
| full_cmd = [venv_python] + cmd |
| return executor.exec_subprocess(full_cmd, env=env) |
| |
| |
| def _get_find_links_path(gcs_dir: str, local_packages=None): |
| if not local_packages: |
| local_packages = DEFAULT_LOCAL_PACKAGES |
| return os.path.expanduser(os.path.join(gcs_dir, local_packages)) |
| |
| |
| class ActivateVenv(object): |
| """Context manage to activate python virtual environment. |
| |
| Will deactive on exit, and revert back to the previous venv. |
| This is designed to run from inside another venv. If not, it |
| will raise KeyError since sys.real_prefix only exists |
| when Python is run in a venv. Since our infras always run |
| from a venv, this is an acceptable assumption. |
| |
| Args: |
| venv_root: Path to the python virtual env root. |
| """ |
| def __init__(self, venv_root): |
| self._venv_root = venv_root |
| self._prev_os_path = None |
| self._prev_sys_path = None |
| self._prev_prefix = None |
| self._prev_real_prefix = None |
| self._save_current_env() |
| |
| def __enter__(self): |
| env_activation_file = os.path.join( |
| self._venv_root, 'bin', 'activate_this.py') |
| exec(compile(open(env_activation_file, "rb").read(), |
| env_activation_file, 'exec'), |
| dict(__file__=env_activation_file)) |
| |
| def __exit__(self, *args): |
| sys.path[:0] = self._prev_sys_path |
| sys.prefix = self._prev_prefix |
| os.environ['PATH'] = self._prev_os_path |
| if self._prev_real_prefix is not None: |
| sys.real_prefix = self._prev_real_prefix |
| |
| def _save_current_env(self): |
| self._prev_os_path = os.environ['PATH'] |
| self._prev_sys_path = list(sys.path) |
| self._prev_prefix = sys.prefix |
| # real_prefix attribute only exists if Python is run from a venv. |
| if hasattr(sys, 'real_prefix'): |
| self._prev_real_prefix = sys.real_prefix |
| |