Source code for venv_management.api

"""The public API.
"""

import subprocess
from contextlib import contextmanager
from pathlib import Path
import logging
from typing import List, Tuple, Union

from venv_management.driver import driver
from venv_management.errors import ImplementationNotFound
from venv_management.utilities import compatible_versions
from venv_management.shell import sub_shell_command, get_status_output

logger = logging.getLogger(__file__)


[docs]def check_environment() -> Tuple[int, str]: """ Returns: A 2-tuple containing the status output of the setup command, and text output """ command = sub_shell_command("", suppress_setup_output=False) return get_status_output(command)
# TODO: Use a more generic name for this function
[docs]def has_virtualenvwrapper(): """Determine whether virtualenvwrapper available and working. Returns: True if virtualenvwrapper is available and working, otherwise False. """ try: driver() except ImplementationNotFound: return False return True
[docs]def list_virtual_envs() -> List[str]: """A list of virtualenv names. Returns: A list of string names in case-sensitive alphanumeric order. Raises: ImplementationNotFound: If no virtualenvwrapper implementation could be found. """ return driver().list_virtual_envs()
[docs]def make_virtual_env( name, *, python=None, project_path=None, packages=None, requirements_file=None, system_site_packages=False, pip=True, setuptools=True, wheel=True, ): """Make a virtual env. Args: name: The name of the virtual environment. python: The target interpreter for which to create a virtual environment, either the name of the executable, or full path. project_path: An optional path to a project which will be associated with the new virtual environment. packages: An optional sequence of package names for packages to be installed. requirements_file: An optional path to a requirements file to be installed. system_site_packages: If True, give access to the system site packages. pip: If True, or 'latest' the latest pip will be installed. If False, pip will not be installed. If 'bundled', the bundled version will be installed. If a specific version string is given, that version will be installed. setuptools: If True, or 'latest' the latest pip will be installed. If False, setuptools will not be installed. If 'bundled', the bundled version will be installed. If a specific version string is given, that version will be installed. wheel: If True, or 'latest' the latest pip will be installed. If False, wheel will not be installed. If 'bundled', the bundled version will be installed. If a specific version string is given, that version will be installed. Returns: The Path to the root of the virtualenv, or None if the path could not be determined. Raises: RuntimeError: If the virtualenv could not be created. """ return driver().make_virtual_env( name, python=python, project_path=project_path, packages=packages, requirements_file=requirements_file, system_site_packages=system_site_packages, pip=pip, setuptools=setuptools, wheel=wheel )
[docs]def resolve_virtual_env(name): """Given the name of a virtual environment, get its path. Args: The name of a virtual environment. Returns: The path to the virtual environment directory. Raises: ValueError: If the virtual environment name is not known. RuntimeError: If the path could not be determined. """ return driver().resolve_virtual_env(name)
[docs]@contextmanager def virtual_env(name, expected_version=None, *, force=False, **kwargs): """A context manager that ensures a virtualenv with the given name and version exists. Irrespective of whether the virtual environment already exists, it will be removed when the context manager exits. Args: name: The name of the environment to check for. expected_version: An optional required version as a string. "3.8" will match "3.8.2" force: Force replacement of an existing virtual environment which has the wrong version. **kwargs: Arguments which will be forwarded to mkvirtualenv if the environment needs to be created. Returns: A context manager that manages the lifecycle of the virtual environment. Raises: RuntimeError: If the virtual environment couldn't be created or replaced. """ venv_path = ensure_virtual_env(name, expected_version, force=force, **kwargs) try: yield venv_path finally: remove_virtual_env(name)
[docs]def ensure_virtual_env(name, expected_version=None, *, force=False, **kwargs): """Ensure a virtualenv with the given name and version exists. Args: name: The name of the environment to check for. expected_version: An optional required version as a string. "3.8" will match "3.8.2" force: Force replacement of an existing virtual environment which has the wrong version. **kwargs: Arguments which will be forwarded to mkvirtualenv if the environment needs to be created. Returns: The path to the virtual environment. Raises: RuntimeError: If the virtual environment couldn't be created or replaced. """ status, output = check_environment() if status != 0: raise RuntimeError(output) python_arg = f"python{expected_version}" if (expected_version is not None) else None try: env_dirpath = resolve_virtual_env(name) except ValueError: # No such virtual environment, so make it env_dirpath = make_virtual_env(name, python=python_arg, **kwargs) else: # An environment with the right name exists. Does it have the right version? actual_version = python_version(env_dirpath) if (expected_version is not None) and ( not compatible_versions(actual_version, expected_version) ): message = ( f"Virtual environment at {env_dirpath} has actual version {actual_version}, " f"not expected version {expected_version}" ) logger.warning(message) if force: remove_virtual_env(name) env_dirpath = make_virtual_env(name, python=python_arg, **kwargs) else: raise RuntimeError(message) return env_dirpath
[docs]def remove_virtual_env(name: str): """Remove a virtual environment. Args: name: The name of the virtual environment to remove. Raises: ValueError: If there is no environment with the given name. RuntimeError: If the virtualenv could not be removed. """ return driver().remove_virtual_env(name)
[docs]def discard_virtual_env(name: str): """Discard a virtual environment. Args: name: The name of the virtual environment to remove. Raises: RuntimeError: If the virtualenv could not be removed. ValueError: If the name is empty. """ if not name: raise ValueError("The name passed to remove_virtual_env cannot be empty") try: remove_virtual_env(name) except ValueError: pass
[docs]def python_executable_path(env_dirpath: Union[Path, str]) -> Path: """Find the Python executable for a virtual environment. Args: env_dirpath: The path to the root of a virtual environment (Path or str). Returns: A Path object to the executable. Raises: ValueError: If the env_dirpath is not a virtual environment. """ dirpath = Path(env_dirpath) exe_filepath = dirpath / "bin" / "python" if not exe_filepath.exists(): raise ValueError( f"Could not locate Python executable for supposed virtual environment {env_dirpath}" ) return exe_filepath
[docs]def python_name(env_dirpath: Union[Path, str]) -> str: """Find the name of the Python in a virtual environment. Args: env_dirpath: The path to the root of a virtual environment (Path or str). Returns: A descriptive string. Raises: ValueError: If the env_dirpath is not a virtual environment. """ exe = python_executable_path(env_dirpath) command = f"{exe} --version" status, output = subprocess.getstatusoutput(command) if status != 0: raise RuntimeError(f"Could not run {command}") return output.splitlines(keepends=False)[0]
[docs]def python_version(env_dirpath: Union[Path, str]) -> str: """Find the version of the Python in virtual environment. Args: env_dirpath: The path to the root of a virtual environment (Path or str). Returns: A version string, such as "3.8.1" Raises: ValueError: If the env_dirpath is not a virtual environment. """ name = python_name(env_dirpath) version = name.split()[-1] return version
[docs]def driver_name() -> str: """Get the name of the driver.""" return driver().name