import logging
import re
import subprocess
import sys
from os.path import expanduser
from pathlib import Path
from typing import List
from venv_management.driver import Driver
from venv_management.errors import CommandNotFound, ImplementationNotFound, PythonNotFound
from venv_management.utilities import parse_package_arg
from venv_management.shell import (
sub_shell_command, get_status_output,
remove_interactive_shell_warnings,
)
from venv_management.environment import shell_is_interactive
logger = logging.getLogger(__name__)
DESTINATION_PATTERN = r"dest=([^,]+)"
DESTINATION_REGEX = re.compile(DESTINATION_PATTERN)
NO_SUCH_PYTHON_PATTERN = r"failed to find interpreter for Builtin discover of python_spec='([^']*)'"
NO_SUCH_PYTHON_REGEX = re.compile(NO_SUCH_PYTHON_PATTERN)
[docs]class VirtualEnvWrapperDriver(Driver):
def _check_availability(self):
try:
self.list_virtual_envs()
except CommandNotFound:
raise ImplementationNotFound(f"No implementation for {self.name}")
[docs] def list_virtual_envs(self) -> List[str]:
"""A list of virtualenv names.
Returns:
A list of string names in case-sensitive alphanumeric order.
Raises:
FileNotFoundError: If virtualenvwrapper.sh could not be located.
"""
# Accommodate the fact that virtualenvwrapper is not disciplined about success/failure exit codes
# https://bitbucket.org/virtualenvwrapper/virtualenvwrapper/issues/283/some-commands-give-non-zero-exit-codes
lsvirtualenv_command = "lsvirtualenv -b"
command = sub_shell_command(lsvirtualenv_command)
logger.debug(command)
success_statuses = {0, 1}
status, output = get_status_output(command, success_statuses=success_statuses)
if status in success_statuses:
return output.splitlines(keepends=False)
logger.error(output)
if status == 127:
raise CommandNotFound(f"{output}. Have you installed virtualenvwrapper?")
raise RuntimeError(output)
[docs] def make_virtual_env(
self,
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.
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.
python: The target interpreter for which to create a virtual environment, either
the name of the executable, or full path.
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:
PythonNotFound: If the requested Python version could not be found.
RuntimeError: If the virtualenv could not be created.
"""
project_path_arg = f"-a {project_path}" if project_path else ""
packages_args = [f"-i {package}" for package in packages] if packages else []
requirements_arg = f"-r{requirements_file}" if requirements_file else ""
python_arg = f"--python={python}" if python else ""
system_site_packages_arg = "--system-site-packages" if system_site_packages else ""
pip_arg = parse_package_arg("pip", pip)
setuptools_arg = parse_package_arg("setuptools", setuptools)
wheel_arg = parse_package_arg("wheel", wheel)
args = " ".join(
(
project_path_arg,
*packages_args,
requirements_arg,
python_arg,
system_site_packages_arg,
pip_arg,
setuptools_arg,
wheel_arg,
)
)
command = sub_shell_command(f"mkvirtualenv {name} {args}")
logger.info(command)
# Accommodate the fact that virtualenvwrapper is not disciplined about success/failure exit codes
# https://bitbucket.org/virtualenvwrapper/virtualenvwrapper/issues/283/some-commands-give-non-zero-exit-codes
success_statuses = {0, 1}
status, output = get_status_output(command, success_statuses=success_statuses)
if status not in success_statuses:
raise RuntimeError(f"Could not run {command}")
m = NO_SUCH_PYTHON_REGEX.search(output)
if m is not None:
raise PythonNotFound(f"Could not locate Python {python} ; {m.group(0)}")
lines = output.splitlines(keepends=False)
for line in lines:
logger.debug("line = %s", line)
m = DESTINATION_REGEX.search(line)
if m is not None:
dest = m.group(1)
logger.debug("Found dest = %s", dest)
return Path(dest)
message = "Could not find dest for virtualenv {name!r}"
logger.warning(message)
raise RuntimeError(message)
[docs] def remove_virtual_env(self, name):
"""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.
"""
if not name:
# When provided with an empty string, rmvirtualenv removes all virtual environments (!)
# https://bitbucket.org/virtualenvwrapper/virtualenvwrapper/issues/346/rmvirtualenv-removes-all-virtualenvs
raise ValueError("The name passed to remove_virtual_env cannot be empty")
command = sub_shell_command(f"rmvirtualenv {name}")
logger.debug("command = %r", command)
process = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding=sys.getdefaultencoding()
)
# rmvirtualenv returns success (0) even when it fails because no such environment exists
# https://bitbucket.org/virtualenvwrapper/virtualenvwrapper/issues/283/some-commands-give-non-zero-exit-codes
# but it does return a message on stderr
stderr = process.stderr
if shell_is_interactive():
stderr = remove_interactive_shell_warnings(stderr)
if len(stderr) != 0:
raise ValueError(stderr)
[docs] def resolve_virtual_env(self, name: str) -> Path:
if not name:
raise ValueError("The name passed to resolve_virtual_env cannot be empty")
if name not in self.list_virtual_envs():
raise ValueError(f"No virtual environment called {name!r} to remove")
command = sub_shell_command("echo ${WORKON_HOME}")
logger.debug("command = %r", command)
status, output = get_status_output(command)
workon_home = Path(expanduser(output)) if len(output) > 0 else Path.home() / ".virtualenvs"
return workon_home / name