445 lines
17 KiB
Python
445 lines
17 KiB
Python
|
import contextlib
|
|||
|
import os
|
|||
|
import re
|
|||
|
import shutil
|
|||
|
import sys
|
|||
|
from pathlib import Path
|
|||
|
|
|||
|
from pipenv import environments, exceptions
|
|||
|
from pipenv.patched.pip._vendor import rich
|
|||
|
from pipenv.utils.dependencies import python_version
|
|||
|
from pipenv.utils.environment import ensure_environment
|
|||
|
from pipenv.utils.processes import subprocess_run
|
|||
|
from pipenv.utils.shell import find_python, shorten_path
|
|||
|
from pipenv.vendor import click
|
|||
|
|
|||
|
console = rich.console.Console()
|
|||
|
err = rich.console.Console(stderr=True)
|
|||
|
|
|||
|
|
|||
|
def warn_in_virtualenv(project):
|
|||
|
# Only warn if pipenv isn't already active.
|
|||
|
if environments.is_in_virtualenv() and not project.s.is_quiet():
|
|||
|
click.echo(
|
|||
|
"{}: Pipenv found itself running within a virtual environment, "
|
|||
|
"so it will automatically use that environment, instead of "
|
|||
|
"creating its own for any project. You can set "
|
|||
|
"{} to force pipenv to ignore that environment and create "
|
|||
|
"its own instead. You can set {} to suppress this "
|
|||
|
"warning.".format(
|
|||
|
click.style("Courtesy Notice", fg="green"),
|
|||
|
click.style("PIPENV_IGNORE_VIRTUALENVS=1", bold=True),
|
|||
|
click.style("PIPENV_VERBOSITY=-1", bold=True),
|
|||
|
),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
def do_create_virtualenv(project, python=None, site_packages=None, pypi_mirror=None):
|
|||
|
"""Creates a virtualenv."""
|
|||
|
|
|||
|
click.secho("Creating a virtualenv for this project...", bold=True, err=True)
|
|||
|
|
|||
|
click.echo(
|
|||
|
"Pipfile: " + click.style(project.pipfile_location, fg="yellow", bold=True),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
|
|||
|
# Default to using sys.executable, if Python wasn't provided.
|
|||
|
using_string = "Using"
|
|||
|
if not python:
|
|||
|
python = sys.executable
|
|||
|
using_string = "Using default python from"
|
|||
|
click.echo(
|
|||
|
"{} {} {} {}".format(
|
|||
|
click.style(using_string, bold=True),
|
|||
|
click.style(python, fg="yellow", bold=True),
|
|||
|
click.style(f"({python_version(python)})", fg="green"),
|
|||
|
click.style("to create virtualenv...", bold=True),
|
|||
|
),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
|
|||
|
if site_packages:
|
|||
|
click.secho("Making site-packages available...", bold=True, err=True)
|
|||
|
|
|||
|
if pypi_mirror:
|
|||
|
pip_config = {"PIP_INDEX_URL": pypi_mirror}
|
|||
|
else:
|
|||
|
pip_config = {}
|
|||
|
|
|||
|
error = None
|
|||
|
with console.status(
|
|||
|
"Creating virtual environment...", spinner=project.s.PIPENV_SPINNER
|
|||
|
):
|
|||
|
cmd = _create_virtualenv_cmd(project, python, site_packages=site_packages)
|
|||
|
c = subprocess_run(cmd, env=pip_config)
|
|||
|
click.secho(f"{c.stdout}", fg="cyan", err=True)
|
|||
|
if c.returncode != 0:
|
|||
|
error = (
|
|||
|
c.stderr if project.s.is_verbose() else exceptions.prettify_exc(c.stderr)
|
|||
|
)
|
|||
|
err.print(
|
|||
|
environments.PIPENV_SPINNER_FAIL_TEXT.format(
|
|||
|
"Failed creating virtual environment"
|
|||
|
)
|
|||
|
)
|
|||
|
else:
|
|||
|
err.print(
|
|||
|
environments.PIPENV_SPINNER_OK_TEXT.format(
|
|||
|
"Successfully created virtual environment!"
|
|||
|
)
|
|||
|
)
|
|||
|
if error is not None:
|
|||
|
raise exceptions.VirtualenvCreationException(
|
|||
|
extra=click.style(f"{error}", fg="red")
|
|||
|
)
|
|||
|
|
|||
|
# Associate project directory with the environment.
|
|||
|
project_file_name = os.path.join(project.virtualenv_location, ".project")
|
|||
|
with open(project_file_name, "w") as f:
|
|||
|
f.write(project.project_directory)
|
|||
|
from pipenv.environment import Environment
|
|||
|
|
|||
|
sources = project.pipfile_sources()
|
|||
|
# project.get_location_for_virtualenv is only for if we are creating a new virtualenv
|
|||
|
# whereas virtualenv_location is for the current path to the runtime
|
|||
|
project._environment = Environment(
|
|||
|
prefix=project.virtualenv_location,
|
|||
|
is_venv=True,
|
|||
|
sources=sources,
|
|||
|
pipfile=project.parsed_pipfile,
|
|||
|
project=project,
|
|||
|
)
|
|||
|
# Say where the virtualenv is.
|
|||
|
do_where(project, virtualenv=True, bare=False)
|
|||
|
|
|||
|
|
|||
|
def _create_virtualenv_cmd(project, python, site_packages=False):
|
|||
|
cmd = [
|
|||
|
Path(sys.executable).absolute().as_posix(),
|
|||
|
"-m",
|
|||
|
"virtualenv",
|
|||
|
]
|
|||
|
if project.s.PIPENV_VIRTUALENV_CREATOR:
|
|||
|
cmd.append(f"--creator={project.s.PIPENV_VIRTUALENV_CREATOR}")
|
|||
|
cmd.append(f"--prompt={project.name}")
|
|||
|
cmd.append(f"--python={python}")
|
|||
|
cmd.append(project.get_location_for_virtualenv())
|
|||
|
if project.s.PIPENV_VIRTUALENV_COPIES:
|
|||
|
cmd.append("--copies")
|
|||
|
|
|||
|
# Pass site-packages flag to virtualenv, if desired...
|
|||
|
if site_packages:
|
|||
|
cmd.append("--system-site-packages")
|
|||
|
|
|||
|
return cmd
|
|||
|
|
|||
|
|
|||
|
def ensure_virtualenv(project, python=None, site_packages=None, pypi_mirror=None):
|
|||
|
"""Creates a virtualenv, if one doesn't exist."""
|
|||
|
|
|||
|
def abort():
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
if not project.virtualenv_exists:
|
|||
|
try:
|
|||
|
# Ensure environment variables are set properly.
|
|||
|
ensure_environment()
|
|||
|
# Ensure Python is available.
|
|||
|
python = ensure_python(project, python=python)
|
|||
|
if python is not None and not isinstance(python, str):
|
|||
|
python = python.path.as_posix()
|
|||
|
# Create the virtualenv.
|
|||
|
# Abort if --system (or running in a virtualenv).
|
|||
|
if project.s.PIPENV_USE_SYSTEM:
|
|||
|
click.secho(
|
|||
|
"You are attempting to re–create a virtualenv that "
|
|||
|
"Pipenv did not create. Aborting.",
|
|||
|
fg="red",
|
|||
|
)
|
|||
|
sys.exit(1)
|
|||
|
do_create_virtualenv(
|
|||
|
project,
|
|||
|
python=python,
|
|||
|
site_packages=site_packages,
|
|||
|
pypi_mirror=pypi_mirror,
|
|||
|
)
|
|||
|
except KeyboardInterrupt:
|
|||
|
# If interrupted, cleanup the virtualenv.
|
|||
|
cleanup_virtualenv(project, bare=False)
|
|||
|
sys.exit(1)
|
|||
|
# If --python or was passed...
|
|||
|
elif (python) or (site_packages is not None):
|
|||
|
project.s.USING_DEFAULT_PYTHON = False
|
|||
|
# Ensure python is installed before deleting existing virtual env
|
|||
|
python = ensure_python(project, python=python)
|
|||
|
if python is not None and not isinstance(python, str):
|
|||
|
python = python.path.as_posix()
|
|||
|
|
|||
|
click.secho("Virtualenv already exists!", fg="red", err=True)
|
|||
|
# If VIRTUAL_ENV is set, there is a possibility that we are
|
|||
|
# going to remove the active virtualenv that the user cares
|
|||
|
# about, so confirm first.
|
|||
|
if "VIRTUAL_ENV" in os.environ and not (
|
|||
|
project.s.PIPENV_YES
|
|||
|
or click.confirm("Use existing virtualenv?", default=True)
|
|||
|
):
|
|||
|
abort()
|
|||
|
click.echo(click.style("Using existing virtualenv...", bold=True), err=True)
|
|||
|
# Remove the virtualenv.
|
|||
|
cleanup_virtualenv(project, bare=True)
|
|||
|
# Call this function again.
|
|||
|
ensure_virtualenv(
|
|||
|
project,
|
|||
|
python=python,
|
|||
|
site_packages=site_packages,
|
|||
|
pypi_mirror=pypi_mirror,
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
def cleanup_virtualenv(project, bare=True):
|
|||
|
"""Removes the virtualenv directory from the system."""
|
|||
|
if not bare:
|
|||
|
click.secho("Environment creation aborted.", fg="red")
|
|||
|
try:
|
|||
|
# Delete the virtualenv.
|
|||
|
shutil.rmtree(project.virtualenv_location)
|
|||
|
except OSError as e:
|
|||
|
click.echo(
|
|||
|
"{} An error occurred while removing {}!".format(
|
|||
|
click.style("Error: ", fg="red", bold=True),
|
|||
|
click.style(project.virtualenv_location, fg="green"),
|
|||
|
),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
click.secho(e, fg="cyan", err=True)
|
|||
|
|
|||
|
|
|||
|
def ensure_python(project, python=None):
|
|||
|
# Runtime import is necessary due to the possibility that the environments module may have been reloaded.
|
|||
|
if project.s.PIPENV_PYTHON and not python:
|
|||
|
python = project.s.PIPENV_PYTHON
|
|||
|
|
|||
|
def abort(msg=""):
|
|||
|
click.echo(
|
|||
|
"{}\nYou can specify specific versions of Python with:\n{}".format(
|
|||
|
click.style(msg, fg="red"),
|
|||
|
click.style(
|
|||
|
"$ pipenv --python {}".format(os.sep.join(("path", "to", "python"))),
|
|||
|
fg="yellow",
|
|||
|
),
|
|||
|
),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
project.s.USING_DEFAULT_PYTHON = not python
|
|||
|
# Find out which python is desired.
|
|||
|
if not python:
|
|||
|
python = project.required_python_version
|
|||
|
if python:
|
|||
|
range_pattern = r"^[<>]=?|!="
|
|||
|
if re.search(range_pattern, python):
|
|||
|
err.print(
|
|||
|
f"[bold red]Error[/bold red]: Python version range specifier '[cyan]{python}[/cyan]' is not supported. "
|
|||
|
"[yellow]Please use an absolute version number or specify the path to the Python executable on Pipfile.[/yellow]"
|
|||
|
)
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
if not python:
|
|||
|
python = project.s.PIPENV_DEFAULT_PYTHON_VERSION
|
|||
|
path_to_python = find_a_system_python(python)
|
|||
|
if project.s.is_verbose():
|
|||
|
click.echo(f"Using python: {python}", err=True)
|
|||
|
click.echo(f"Path to python: {path_to_python}", err=True)
|
|||
|
if not path_to_python and python is not None:
|
|||
|
# We need to install Python.
|
|||
|
click.echo(
|
|||
|
"{}: Python {} {}".format(
|
|||
|
click.style("Warning", fg="red", bold=True),
|
|||
|
click.style(python, fg="cyan"),
|
|||
|
"was not found on your system...",
|
|||
|
),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
# check for python installers
|
|||
|
from pipenv.installers import Asdf, InstallerError, InstallerNotFound, Pyenv
|
|||
|
|
|||
|
# prefer pyenv if both pyenv and asdf are installed as it's
|
|||
|
# dedicated to python installs so probably the preferred
|
|||
|
# method of the user for new python installs.
|
|||
|
installer = None
|
|||
|
if not project.s.PIPENV_DONT_USE_PYENV:
|
|||
|
with contextlib.suppress(InstallerNotFound):
|
|||
|
installer = Pyenv(project)
|
|||
|
|
|||
|
if installer is None and not project.s.PIPENV_DONT_USE_ASDF:
|
|||
|
with contextlib.suppress(InstallerNotFound):
|
|||
|
installer = Asdf(project)
|
|||
|
|
|||
|
if not installer:
|
|||
|
abort("Neither 'pyenv' nor 'asdf' could be found to install Python.")
|
|||
|
else:
|
|||
|
if environments.SESSION_IS_INTERACTIVE or project.s.PIPENV_YES:
|
|||
|
try:
|
|||
|
version = installer.find_version_to_install(python)
|
|||
|
except ValueError:
|
|||
|
abort()
|
|||
|
except InstallerError as e:
|
|||
|
abort(f"Something went wrong while installing Python:\n{e.err}")
|
|||
|
s = "{} {} {}".format(
|
|||
|
"Would you like us to install",
|
|||
|
click.style(f"CPython {version}", fg="green"),
|
|||
|
f"with {installer}?",
|
|||
|
)
|
|||
|
# Prompt the user to continue...
|
|||
|
if not (project.s.PIPENV_YES or click.confirm(s, default=True)):
|
|||
|
abort()
|
|||
|
else:
|
|||
|
# Tell the user we're installing Python.
|
|||
|
click.echo(
|
|||
|
"{} {} {} {}{}".format(
|
|||
|
click.style("Installing", bold=True),
|
|||
|
click.style(f"CPython {version}", bold=True, fg="green"),
|
|||
|
click.style(f"with {installer.cmd}", bold=True),
|
|||
|
click.style("(this may take a few minutes)"),
|
|||
|
click.style("...", bold=True),
|
|||
|
)
|
|||
|
)
|
|||
|
with console.status(
|
|||
|
"Installing python...", spinner=project.s.PIPENV_SPINNER
|
|||
|
):
|
|||
|
try:
|
|||
|
c = installer.install(version)
|
|||
|
except InstallerError as e:
|
|||
|
err.print(
|
|||
|
environments.PIPENV_SPINNER_FAIL_TEXT.format("Failed...")
|
|||
|
)
|
|||
|
click.echo("Something went wrong...", err=True)
|
|||
|
click.secho(e.err, fg="cyan", err=True)
|
|||
|
else:
|
|||
|
console.print(
|
|||
|
environments.PIPENV_SPINNER_OK_TEXT.format("Success!")
|
|||
|
)
|
|||
|
# Print the results, in a beautiful blue...
|
|||
|
click.secho(c.stdout, fg="cyan", err=True)
|
|||
|
# Find the newly installed Python, hopefully.
|
|||
|
version = str(version)
|
|||
|
path_to_python = find_a_system_python(version)
|
|||
|
try:
|
|||
|
assert python_version(path_to_python) == version
|
|||
|
except AssertionError:
|
|||
|
click.echo(
|
|||
|
"{}: The Python you just installed is not available on your {}, apparently."
|
|||
|
"".format(
|
|||
|
click.style("Warning", fg="red", bold=True),
|
|||
|
click.style("PATH", bold=True),
|
|||
|
),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
sys.exit(1)
|
|||
|
return path_to_python
|
|||
|
|
|||
|
|
|||
|
def find_a_system_python(line):
|
|||
|
"""Find a Python installation from a given line.
|
|||
|
|
|||
|
This tries to parse the line in various of ways:
|
|||
|
|
|||
|
* Looks like an absolute path? Use it directly.
|
|||
|
* Looks like a py.exe call? Use py.exe to get the executable.
|
|||
|
* Starts with "py" something? Looks like a python command. Try to find it
|
|||
|
in PATH, and use it directly.
|
|||
|
* Search for "python" and "pythonX.Y" executables in PATH to find a match.
|
|||
|
* Nothing fits, return None.
|
|||
|
"""
|
|||
|
|
|||
|
from pipenv.vendor.pythonfinder import Finder
|
|||
|
|
|||
|
finder = Finder(system=True, global_search=True)
|
|||
|
if not line:
|
|||
|
return next(iter(finder.find_all_python_versions()), None)
|
|||
|
# Use the windows finder executable
|
|||
|
if (line.startswith(("py ", "py.exe "))) and os.name == "nt":
|
|||
|
line = line.split(" ", 1)[1].lstrip("-")
|
|||
|
python_entry = find_python(finder, line)
|
|||
|
return python_entry
|
|||
|
|
|||
|
|
|||
|
def do_where(project, virtualenv=False, bare=True):
|
|||
|
"""Executes the where functionality."""
|
|||
|
if not virtualenv:
|
|||
|
if not project.pipfile_exists:
|
|||
|
click.echo(
|
|||
|
"No Pipfile present at project home. Consider running "
|
|||
|
"{} first to automatically generate a Pipfile for you."
|
|||
|
"".format(click.style("`pipenv install`", fg="green")),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
return
|
|||
|
location = project.pipfile_location
|
|||
|
# Shorten the virtual display of the path to the virtualenv.
|
|||
|
if not bare:
|
|||
|
location = shorten_path(location)
|
|||
|
click.echo(
|
|||
|
"Pipfile found at {}.\n Considering this to be the project home."
|
|||
|
"".format(click.style(location, fg="green")),
|
|||
|
err=True,
|
|||
|
)
|
|||
|
else:
|
|||
|
click.echo(project.project_directory)
|
|||
|
else:
|
|||
|
location = project.virtualenv_location
|
|||
|
if not bare:
|
|||
|
click.secho(f"Virtualenv location: {location}", fg="green", err=True)
|
|||
|
else:
|
|||
|
click.echo(location)
|
|||
|
|
|||
|
|
|||
|
def inline_activate_virtual_environment(project):
|
|||
|
root = project.virtualenv_location
|
|||
|
if os.path.exists(os.path.join(root, "pyvenv.cfg")):
|
|||
|
_inline_activate_venv(project)
|
|||
|
else:
|
|||
|
_inline_activate_virtualenv(project)
|
|||
|
if "VIRTUAL_ENV" not in os.environ:
|
|||
|
os.environ["VIRTUAL_ENV"] = root
|
|||
|
|
|||
|
|
|||
|
def _inline_activate_venv(project):
|
|||
|
"""Built-in venv doesn't have activate_this.py, but doesn't need it anyway.
|
|||
|
|
|||
|
As long as we find the correct executable, built-in venv sets up the
|
|||
|
environment automatically.
|
|||
|
|
|||
|
See: https://bugs.python.org/issue21496#msg218455
|
|||
|
"""
|
|||
|
components = []
|
|||
|
for name in ("bin", "Scripts"):
|
|||
|
bindir = os.path.join(project.virtualenv_location, name)
|
|||
|
if os.path.exists(bindir):
|
|||
|
components.append(bindir)
|
|||
|
if "PATH" in os.environ:
|
|||
|
components.append(os.environ["PATH"])
|
|||
|
os.environ["PATH"] = os.pathsep.join(components)
|
|||
|
|
|||
|
|
|||
|
def _inline_activate_virtualenv(project):
|
|||
|
try:
|
|||
|
activate_this = project._which("activate_this.py")
|
|||
|
if not activate_this or not os.path.exists(activate_this):
|
|||
|
raise exceptions.VirtualenvActivationException()
|
|||
|
with open(activate_this) as f:
|
|||
|
code = compile(f.read(), activate_this, "exec")
|
|||
|
exec(code, {"__file__": activate_this})
|
|||
|
# Catch all errors, just in case.
|
|||
|
except Exception:
|
|||
|
click.echo(
|
|||
|
"{}: There was an unexpected error while activating your "
|
|||
|
"virtualenv. Continuing anyway...".format(
|
|||
|
click.style("Warning", fg="red", bold=True)
|
|||
|
),
|
|||
|
err=True,
|
|||
|
)
|