Commit 567604d9 by Sébastien Eustace Committed by GitHub

Improve venv detection and management (#404)

parent 1c195b36
from .init import InitCommand
from .venv_command import VenvCommand
from .env_command import EnvCommand
class AddCommand(VenvCommand, InitCommand):
class AddCommand(EnvCommand, InitCommand):
"""
Add a new dependency to <comment>pyproject.toml</>.
......@@ -125,7 +125,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
installer = Installer(
self.output,
self.venv,
self.env,
self.poetry.package,
self.poetry.locker,
self.poetry.pool,
......
from .venv_command import VenvCommand
from .env_command import EnvCommand
class BuildCommand(VenvCommand):
class BuildCommand(EnvCommand):
"""
Builds a package, as a tarball and a wheel by default.
......@@ -23,5 +23,5 @@ class BuildCommand(VenvCommand):
)
)
builder = Builder(self.poetry, self.venv, self.output)
builder = Builder(self.poetry, self.env, self.output)
builder.build(fmt)
import os
import sys
from ..venv_command import VenvCommand
from ..command import Command
class DebugInfoCommand(VenvCommand):
class DebugInfoCommand(Command):
"""
Shows debug information.
......@@ -12,9 +12,11 @@ class DebugInfoCommand(VenvCommand):
"""
def handle(self):
from ....utils.env import Env
poetry = self.poetry
package = poetry.package
venv = self.venv
env = Env.get()
poetry_python_version = ".".join(str(s) for s in sys.version_info[:3])
......@@ -28,18 +30,18 @@ class DebugInfoCommand(VenvCommand):
self.line("")
venv_python_version = ".".join(str(s) for s in venv.version_info[:3])
env_python_version = ".".join(str(s) for s in env.version_info[:3])
self.output.title("Virtualenv")
self.output.listing(
[
"<info>Python</info>: <comment>{}</>".format(
venv_python_version
env_python_version
),
"<info>Implementation</info>: <comment>{}</>".format(
venv.python_implementation
env.python_implementation
),
"<info>Path</info>: <comment>{}</>".format(
venv.venv if venv.is_venv() else "NA"
env.path if env.is_venv() else "NA"
),
]
)
......@@ -51,6 +53,7 @@ class DebugInfoCommand(VenvCommand):
[
"<info>Platform</info>: <comment>{}</>".format(sys.platform),
"<info>OS</info>: <comment>{}</>".format(os.name),
"<info>Python</info>: <comment>{}</>".format(env.base),
]
)
......
import os
from .venv_command import VenvCommand
from .env_command import EnvCommand
class DevelopCommand(VenvCommand):
class DevelopCommand(EnvCommand):
"""
Installs the current project in development mode.
......@@ -18,7 +18,7 @@ The <info>develop</info> command installs the current project in development mod
from poetry.masonry.builders import SdistBuilder
from poetry.io import NullIO
from poetry.utils._compat import decode
from poetry.utils.venv import NullVenv
from poetry.utils.env import NullEnv
setup = self.poetry.file.parent / "setup.py"
has_setup = setup.exists()
......@@ -26,7 +26,7 @@ The <info>develop</info> command installs the current project in development mod
if has_setup:
self.line("<warning>A setup.py file already exists. Using it.</warning>")
else:
builder = SdistBuilder(self.poetry, NullVenv(), NullIO())
builder = SdistBuilder(self.poetry, NullEnv(), NullIO())
with setup.open("w") as f:
f.write(decode(builder.build_setup()))
......@@ -45,4 +45,4 @@ The <info>develop</info> command installs the current project in development mod
self.poetry.package.pretty_name, self.poetry.package.pretty_version
)
)
self.venv.run("pip", "install", "-e", str(setup.parent), "--no-deps")
self.env.run("pip", "install", "-e", str(setup.parent), "--no-deps")
from .command import Command
class EnvCommand(Command):
def __init__(self):
self._env = None
super(EnvCommand, self).__init__()
def initialize(self, i, o):
from poetry.utils.env import Env
super(EnvCommand, self).initialize(i, o)
self._env = Env.create_venv(
o, self.poetry.package.name, cwd=self.poetry.file.parent
)
if self._env.is_venv() and o.is_verbose():
o.writeln("Using virtualenv: <comment>{}</>".format(self._env.path))
@property
def env(self):
return self._env
......@@ -7,7 +7,7 @@ from typing import List
from typing import Tuple
from .command import Command
from .venv_command import VenvCommand
from .env_command import EnvCommand
class InitCommand(Command):
......@@ -305,7 +305,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
from poetry.repositories import Pool
from poetry.repositories.pypi_repository import PyPiRepository
if isinstance(self, VenvCommand):
if isinstance(self, EnvCommand):
return self.poetry.pool
if self._pool is None:
......
from .venv_command import VenvCommand
from .env_command import EnvCommand
class InstallCommand(VenvCommand):
class InstallCommand(EnvCommand):
"""
Installs the project dependencies.
......@@ -28,7 +28,7 @@ exist it will look for <comment>pyproject.toml</> and do the same.
installer = Installer(
self.output,
self.venv,
self.env,
self.poetry.package,
self.poetry.locker,
self.poetry.pool,
......
from .venv_command import VenvCommand
from .env_command import EnvCommand
class LockCommand(VenvCommand):
class LockCommand(EnvCommand):
"""
Locks the project dependencies.
......@@ -21,7 +21,7 @@ the current directory, processes it, and locks the depdencies in the <comment>py
installer = Installer(
self.output,
self.venv,
self.env,
self.poetry.package,
self.poetry.locker,
self.poetry.pool,
......
from .venv_command import VenvCommand
from .env_command import EnvCommand
class RemoveCommand(VenvCommand):
class RemoveCommand(EnvCommand):
"""
Removes a package from the project dependencies.
......@@ -56,7 +56,7 @@ list of installed packages
installer = Installer(
self.output,
self.venv,
self.env,
self.poetry.package,
self.poetry.locker,
self.poetry.pool,
......
from .venv_command import VenvCommand
from .env_command import EnvCommand
class RunCommand(VenvCommand):
class RunCommand(EnvCommand):
"""
Runs a command in the appropriate environment.
......@@ -17,9 +17,7 @@ class RunCommand(VenvCommand):
if scripts and script in scripts:
return self.run_script(scripts[script], args)
venv = self.venv
return venv.execute(*args)
return self.env.execute(*args)
def run_script(self, script, args):
module, callable_ = script.split(":")
......@@ -37,7 +35,7 @@ class RunCommand(VenvCommand):
)
]
return self.venv.run(*cmd, shell=True, call=True)
return self.env.run(*cmd, shell=True, call=True)
@property
def _module(self):
......
from .venv_command import VenvCommand
from .env_command import EnvCommand
class ScriptCommand(VenvCommand):
class ScriptCommand(EnvCommand):
"""
Executes a script defined in <comment>pyproject.toml</comment>. (<error>Deprecated</error>)
......@@ -42,7 +42,7 @@ class ScriptCommand(VenvCommand):
)
]
self.venv.run(*cmd, shell=True, call=True)
self.env.run(*cmd, shell=True, call=True)
@property
def _module(self):
......
import sys
from os import environ
from distutils.util import strtobool
from .venv_command import VenvCommand
from .env_command import EnvCommand
class ShellCommand(VenvCommand):
class ShellCommand(EnvCommand):
"""
Spawns a shell within the virtual environment.
......@@ -20,24 +22,21 @@ If one doesn't exist yet, it will be created.
from poetry.utils.shell import Shell
# Check if it's already activated or doesn't exist and won't be created
if strtobool(environ.get("POETRY_ACTIVE", "0")) or not self.venv.is_venv():
current_venv = environ.get("VIRTUAL_ENV")
if current_venv:
venv_activated = strtobool(environ.get("POETRY_ACTIVE", "0")) or getattr(
sys, "real_prefix", sys.prefix
) == str(self.env.path)
if venv_activated:
self.line(
"Virtual environment already activated: "
"<info>{}</>".format(current_venv)
"<info>{}</>".format(self.env.path)
)
else:
self.error("Virtual environment wasn't found")
return
self.line("Spawning shell within <info>{}</>".format(self.venv.venv))
self.line("Spawning shell within <info>{}</>".format(self.env.path))
# Setting this to avoid spawning unnecessary nested shells
environ["POETRY_ACTIVE"] = "1"
shell = Shell.get()
self.venv.execute(shell.path)
self.env.execute(shell.path)
environ.pop("POETRY_ACTIVE")
# -*- coding: utf-8 -*-
import sys
from .venv_command import VenvCommand
from .env_command import EnvCommand
class ShowCommand(VenvCommand):
class ShowCommand(EnvCommand):
"""
Shows information about packages.
......@@ -96,11 +96,11 @@ lists all packages available."""
width = terminal.width
name_length = version_length = latest_length = 0
latest_packages = {}
installed_repo = InstalledRepository.load(self.venv)
installed_repo = InstalledRepository.load(self.env)
skipped = []
platform = sys.platform
python = Version.parse(".".join([str(i) for i in self._venv.version_info[:3]]))
python = Version.parse(".".join([str(i) for i in self.env.version_info[:3]]))
# Computing widths
for locked in locked_packages:
......
from .venv_command import VenvCommand
from .env_command import EnvCommand
class UpdateCommand(VenvCommand):
class UpdateCommand(EnvCommand):
"""
Update dependencies as according to the <comment>pyproject.toml</> file.
......@@ -21,7 +21,7 @@ class UpdateCommand(VenvCommand):
installer = Installer(
self.output,
self.venv,
self.env,
self.poetry.package,
self.poetry.locker,
self.poetry.pool,
......
from .command import Command
class VenvCommand(Command):
def __init__(self):
self._venv = None
super(VenvCommand, self).__init__()
def initialize(self, i, o):
from poetry.utils.venv import Venv
super(VenvCommand, self).initialize(i, o)
self._venv = Venv.create(
o, self.poetry.package.name, cwd=self.poetry.file.parent
)
if self._venv.is_venv() and o.is_verbose():
o.writeln("Using virtualenv: <comment>{}</>".format(self._venv.venv))
@property
def venv(self):
return self._venv
......@@ -28,14 +28,14 @@ class Installer:
def __init__(
self,
io,
venv,
env,
package, # type: Package
locker, # type: Locker
pool, # type: Pool
installed=None, # type: (Union[InstalledRepository, None])
):
self._io = io
self._venv = venv
self._env = env
self._package = package
self._locker = locker
self._pool = pool
......@@ -183,7 +183,7 @@ class Installer:
self._populate_local_repo(local_repo, ops, locked_repository)
with self._package.with_python_versions(
".".join([str(i) for i in self._venv.version_info[:3]])
".".join([str(i) for i in self._env.version_info[:3]])
):
# We resolve again by only using the lock file
pool = Pool()
......@@ -458,7 +458,7 @@ class Installer:
op.unskip()
python = Version.parse(
".".join([str(i) for i in self._venv.version_info[:3]])
".".join([str(i) for i in self._env.version_info[:3]])
)
if "python" in package.requirements:
python_constraint = parse_constraint(package.requirements["python"])
......@@ -535,7 +535,7 @@ class Installer:
return _extra_packages(extra_packages)
def _get_installer(self): # type: () -> BaseInstaller
return PipInstaller(self._venv, self._io)
return PipInstaller(self._env, self._io)
def _get_installed(self): # type: () -> InstalledRepository
return InstalledRepository.load(self._venv)
return InstalledRepository.load(self._env)
......@@ -12,14 +12,14 @@ except ImportError:
import urlparse
from poetry.utils._compat import encode
from poetry.utils.venv import Venv
from poetry.utils.env import Env
from .base_installer import BaseInstaller
class PipInstaller(BaseInstaller):
def __init__(self, venv, io): # type: (Venv, ...) -> None
self._venv = venv
def __init__(self, env, io): # type: (Env, ...) -> None
self._env = env
self._io = io
def install(self, package, update=False):
......@@ -88,7 +88,7 @@ class PipInstaller(BaseInstaller):
raise
def run(self, *args, **kwargs): # type: (...) -> str
return self._venv.run("pip", *args, **kwargs)
return self._env.run("pip", *args, **kwargs)
def requirement(self, package, formatted=False):
if formatted and not package.source_type:
......
......@@ -2,11 +2,13 @@
PEP-517 compliant buildsystem API
"""
import logging
import sys
from pathlib import Path
from poetry.poetry import Poetry
from poetry.io import NullIO
from poetry.utils.venv import Venv
from poetry.utils.env import SystemEnv
from .builders import SdistBuilder
from .builders import WheelBuilder
......@@ -39,6 +41,8 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
def build_sdist(sdist_directory, config_settings=None):
"""Builds an sdist, places it in sdist_directory"""
path = SdistBuilder(poetry, Venv(), NullIO()).build(Path(sdist_directory))
path = SdistBuilder(poetry, SystemEnv(sys.prefix), NullIO()).build(
Path(sdist_directory)
)
return path.name
......@@ -7,15 +7,15 @@ class Builder:
_FORMATS = {"sdist": SdistBuilder, "wheel": WheelBuilder, "all": CompleteBuilder}
def __init__(self, poetry, venv, io):
def __init__(self, poetry, env, io):
self._poetry = poetry
self._venv = venv
self._env = env
self._io = io
def build(self, fmt):
if fmt not in self._FORMATS:
raise ValueError("Invalid format: {}".format(fmt))
builder = self._FORMATS[fmt](self._poetry, self._venv, self._io)
builder = self._FORMATS[fmt](self._poetry, self._env, self._io)
return builder.build()
......@@ -22,9 +22,9 @@ class Builder(object):
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"}
def __init__(self, poetry, venv, io):
def __init__(self, poetry, env, io):
self._poetry = poetry
self._venv = venv
self._env = env
self._io = io
self._package = poetry.package
self._path = poetry.file.parent
......
......@@ -14,7 +14,7 @@ class CompleteBuilder(Builder):
def build(self):
# We start by building the tarball
# We will use it to build the wheel
sdist_builder = SdistBuilder(self._poetry, self._venv, self._io)
sdist_builder = SdistBuilder(self._poetry, self._env, self._io)
sdist_file = sdist_builder.build()
self._io.writeln("")
......@@ -23,7 +23,7 @@ class CompleteBuilder(Builder):
with self.unpacked_tarball(sdist_file) as tmpdir:
WheelBuilder.make_in(
poetry.poetry.Poetry.create(tmpdir),
self._venv,
self._env,
self._io,
dist_dir,
original=self._poetry,
......
......@@ -34,8 +34,8 @@ Tag: {tag}
class WheelBuilder(Builder):
def __init__(self, poetry, venv, io, target_dir=None, original=None):
super(WheelBuilder, self).__init__(poetry, venv, io)
def __init__(self, poetry, env, io, target_dir=None, original=None):
super(WheelBuilder, self).__init__(poetry, env, io)
self._records = []
self._original_path = self._path
......@@ -44,14 +44,14 @@ class WheelBuilder(Builder):
self._original_path = original.file.parent
@classmethod
def make_in(cls, poetry, venv, io, directory=None, original=None):
wb = WheelBuilder(poetry, venv, io, target_dir=directory, original=original)
def make_in(cls, poetry, env, io, directory=None, original=None):
wb = WheelBuilder(poetry, env, io, target_dir=directory, original=original)
wb.build()
@classmethod
def make(cls, poetry, venv, io):
def make(cls, poetry, env, io):
"""Build a wheel in the dist/ directory, and optionally upload it."""
cls.make_in(poetry, venv, io)
cls.make_in(poetry, env, io)
def build(self):
self._io.writeln(" - Building <info>wheel</info>")
......@@ -86,7 +86,7 @@ class WheelBuilder(Builder):
current_path = os.getcwd()
try:
os.chdir(str(self._path))
self._venv.run(
self._env.run(
"python", str(setup), "build", "-b", str(self._path / "build")
)
finally:
......@@ -199,10 +199,10 @@ class WheelBuilder(Builder):
def tag(self):
if self._package.build:
platform = get_platform().replace(".", "_").replace("-", "_")
impl_name = get_abbr_impl(self._venv)
impl_ver = get_impl_ver(self._venv)
impl_name = get_abbr_impl(self._env)
impl_ver = get_impl_ver(self._env)
impl = impl_name + impl_ver
abi_tag = str(get_abi_tag(self._venv)).lower()
abi_tag = str(get_abi_tag(self._env)).lower()
tag = (impl, abi_tag, platform)
else:
platform = "any"
......
......@@ -3,7 +3,7 @@ Generate and work with PEP 425 Compatibility Tags.
Base implementation taken from
https://github.com/pypa/wheel/blob/master/wheel/pep425tags.py
and adapted to work with poetry's venv util.
and adapted to work with poetry's env util.
"""
from __future__ import unicode_literals
......@@ -12,9 +12,9 @@ import sys
import warnings
def get_abbr_impl(venv):
def get_abbr_impl(env):
"""Return abbreviated implementation name."""
impl = venv.python_implementation
impl = env.python_implementation
if impl == "PyPy":
return "pp"
......@@ -28,29 +28,29 @@ def get_abbr_impl(venv):
raise LookupError("Unknown Python implementation: " + impl)
def get_impl_ver(venv):
def get_impl_ver(env):
"""Return implementation version."""
impl_ver = venv.config_var("py_version_nodot")
if not impl_ver or get_abbr_impl(venv) == "pp":
impl_ver = "".join(map(str, get_impl_version_info(venv)))
impl_ver = env.config_var("py_version_nodot")
if not impl_ver or get_abbr_impl(env) == "pp":
impl_ver = "".join(map(str, get_impl_version_info(env)))
return impl_ver
def get_impl_version_info(venv):
def get_impl_version_info(env):
"""Return sys.version_info-like tuple for use in decrementing the minor
version."""
if get_abbr_impl(venv) == "pp":
if get_abbr_impl(env) == "pp":
# as per https://github.com/pypa/pip/issues/2882
return venv.version_info[:3]
return env.version_info[:3]
else:
return venv.version_info[:2]
return env.version_info[:2]
def get_flag(venv, var, fallback, expected=True, warn=True):
def get_flag(env, var, fallback, expected=True, warn=True):
"""Use a fallback method for determining SOABI flags if the needed config
var is unset or unavailable."""
val = venv.config_var(var)
val = env.config_var(var)
if val is None:
if warn:
warnings.warn(
......@@ -63,33 +63,33 @@ def get_flag(venv, var, fallback, expected=True, warn=True):
return val == expected
def get_abi_tag(venv):
def get_abi_tag(env):
"""Return the ABI tag based on SOABI (if available) or emulate SOABI
(CPython 2, PyPy)."""
soabi = venv.config_var("SOABI")
impl = get_abbr_impl(venv)
soabi = env.config_var("SOABI")
impl = get_abbr_impl(env)
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
d = ""
m = ""
u = ""
if get_flag(
venv,
env,
"Py_DEBUG",
lambda: hasattr(sys, "gettotalrefcount"),
warn=(impl == "cp"),
):
d = "d"
if get_flag(venv, "WITH_PYMALLOC", lambda: impl == "cp", warn=(impl == "cp")):
if get_flag(env, "WITH_PYMALLOC", lambda: impl == "cp", warn=(impl == "cp")):
m = "m"
if get_flag(
venv,
env,
"Py_UNICODE_SIZE",
lambda: sys.maxunicode == 0x10ffff,
expected=4,
warn=(impl == "cp" and venv.version_info < (3, 3)),
) and venv.version_info < (3, 3):
warn=(impl == "cp" and env.version_info < (3, 3)),
) and env.version_info < (3, 3):
u = "u"
abi = "%s%s%s%s%s" % (impl, get_impl_ver(venv), d, m, u)
abi = "%s%s%s%s%s" % (impl, get_impl_ver(env), d, m, u)
elif soabi and soabi.startswith("cpython-"):
abi = "cp" + soabi.split("-")[1]
elif soabi:
......@@ -109,7 +109,7 @@ def get_platform():
return result
def get_supported(venv, versions=None, supplied_platform=None):
def get_supported(env, versions=None, supplied_platform=None):
"""Return a list of supported tags for each version specified in
`versions`.
:param versions: a list of string versions, of the form ["33", "32"],
......@@ -120,17 +120,17 @@ def get_supported(venv, versions=None, supplied_platform=None):
# Versions must be given with respect to the preference
if versions is None:
versions = []
version_info = get_impl_version_info(venv)
version_info = get_impl_version_info(env)
major = version_info[:-1]
# Support all previous minor Python versions.
for minor in range(version_info[-1], -1, -1):
versions.append("".join(map(str, major + (minor,))))
impl = get_abbr_impl(venv)
impl = get_abbr_impl(env)
abis = []
abi = get_abi_tag(venv)
abi = get_abi_tag(env)
if abi:
abis[0:0] = [abi]
......
......@@ -9,8 +9,8 @@ from poetry.utils._compat import Path
from poetry.utils._compat import decode
from poetry.utils.helpers import parse_requires
from poetry.utils.toml_file import TomlFile
from poetry.utils.venv import NullVenv
from poetry.utils.venv import Venv
from poetry.utils.env import NullEnv
from poetry.utils.env import Env
from .dependency import Dependency
......@@ -68,7 +68,7 @@ class DirectoryDependency(Dependency):
from poetry.poetry import Poetry
poetry = Poetry.create(self._full_path)
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
with setup.open("w") as f:
f.write(decode(builder.build_setup()))
......@@ -87,7 +87,7 @@ class DirectoryDependency(Dependency):
try:
cwd = base
venv = Venv.create(NullIO(), cwd=cwd)
venv = Env.create_venv(NullIO(), cwd=cwd)
venv.run("python", "setup.py", "egg_info")
finally:
os.chdir(current_dir)
......
......@@ -28,7 +28,7 @@ from poetry.repositories import Pool
from poetry.utils._compat import Path
from poetry.utils.helpers import parse_requires
from poetry.utils.toml_file import TomlFile
from poetry.utils.venv import Venv
from poetry.utils.env import Env
from poetry.vcs.git import Git
......@@ -185,7 +185,7 @@ class Provider:
# to figure the information we need
# We need to place ourselves in the proper
# folder for it to work
venv = Venv.create(self._io)
venv = Env.get(self._io)
current_dir = os.getcwd()
os.chdir(tmp_dir.as_posix())
......
from poetry.packages import Package
from poetry.utils.venv import Venv
from poetry.utils.env import Env
from .repository import Repository
class InstalledRepository(Repository):
@classmethod
def load(cls, venv): # type: (Venv) -> InstalledRepository
def load(cls, env): # type: (Env) -> InstalledRepository
"""
Load installed packages.
......@@ -14,7 +14,7 @@ class InstalledRepository(Repository):
"""
repo = cls()
freeze_output = venv.run("pip", "freeze")
freeze_output = env.run("pip", "freeze")
for line in freeze_output.split("\n"):
if "==" in line:
name, version = line.split("==")
......
......@@ -38,7 +38,7 @@ from poetry.utils._compat import Path
from poetry.utils._compat import to_str
from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import temporary_directory
from poetry.utils.venv import Venv
from poetry.utils.env import Env
from poetry.version.markers import InvalidMarker
from .repository import Repository
......@@ -499,7 +499,7 @@ class PyPiRepository(Repository):
if not setup.exists():
return info
venv = Venv.create(NullIO())
venv = Env.create_venv(NullIO())
current_dir = os.getcwd()
os.chdir(sdist_dir.as_posix())
......
......@@ -7,6 +7,9 @@ import warnings
from contextlib import contextmanager
from subprocess import CalledProcessError
from typing import Any
from typing import Optional
from typing import Tuple
from poetry.config import Config
from poetry.locations import CACHE_DIR
......@@ -14,44 +17,105 @@ from poetry.utils._compat import Path
from poetry.utils._compat import decode
class VenvError(Exception):
class EnvError(Exception):
pass
class VenvCommandError(VenvError):
class EnvCommandError(EnvError):
def __init__(self, e): # type: (CalledProcessError) -> None
message = "Command {} errored with the following output: \n{}".format(
e.cmd, decode(e.output)
)
super(VenvCommandError, self).__init__(message)
super(EnvCommandError, self).__init__(message)
class Venv(object):
def __init__(self, venv=None):
self._venv = venv
if self._venv:
self._venv = Path(self._venv)
class Env(object):
"""
An abstract Python environment.
"""
_env = None
def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None
self._is_windows = sys.platform == "win32"
self._path = path
bin_dir = "bin" if not self._is_windows else "Scripts"
self._bin_dir = self._path / bin_dir
self._base = base or path
self._version_info = self.get_version_info()
self._python_implementation = self.get_python_implementation()
@property
def path(self): # type: () -> Path
return self._path
@property
def base(self): # type: () -> Path
return self._base
@property
def version_info(self): # type: () -> Tuple[int]
return self._version_info
@property
def python_implementation(self): # type: () -> str
return self._python_implementation
@property
def python(self): # type: () -> str
"""
Path to current python executable
"""
return self._bin("python")
self._windows = sys.platform == "win32"
@property
def pip(self): # type: () -> str
"""
Path to current pip executable
"""
return self._bin("pip")
self._bin_dir = None
if venv:
bin_dir = "bin" if not self._windows else "Scripts"
self._bin_dir = self._venv / bin_dir
@classmethod
def get(cls, reload=False): # type: (IO, bool) -> Env
if cls._env is not None and not reload:
return cls._env
# Check if we are inside a virtualenv or not
in_venv = (
os.environ.get("VIRTUAL_ENV") is not None
or hasattr(sys, "real_prefix")
or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)
)
self._version_info = None
self._python_implementation = None
if not in_venv:
return SystemEnv(Path(sys.prefix))
return VirtualEnv(
Path(getattr(sys, "real_prefix", sys.prefix)),
Path(getattr(sys, "base_prefix", sys.prefix)),
)
@classmethod
def create(cls, io, name=None, cwd=None): # type: (...) -> Venv
if "VIRTUAL_ENV" not in os.environ:
# Not in a virtualenv
# Checking if we need to create one
def create_venv(cls, io, name=None, cwd=None): # type: (IO, bool) -> Env
if cls._env is not None:
return cls._env
# Check if we are inside a virtualenv or not
in_venv = (
os.environ.get("VIRTUAL_ENV") is not None
or hasattr(sys, "real_prefix")
or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)
)
# First we check if there is a .venv
# at the root of the project.
venv = os.environ.get("VIRTUAL_ENV", getattr(sys, "real_prefix", sys.prefix))
venv = Path(venv)
if not in_venv:
# Not currently in a virtual env, we create one
if cwd and (cwd / ".venv").exists():
venv = cwd / ".venv"
else:
......@@ -94,7 +158,7 @@ class Venv(object):
"</>"
)
return cls()
return SystemEnv(Path(sys.prefix))
io.writeln(
"Creating virtualenv <info>{}</> in {}".format(
......@@ -102,15 +166,13 @@ class Venv(object):
)
)
cls.build(str(venv))
cls.build_venv(str(venv))
else:
if io.is_very_verbose():
io.writeln(
"Virtualenv <info>{}</> already exists.".format(name)
)
os.environ["VIRTUAL_ENV"] = str(venv)
# venv detection:
# stdlib venv may symlink sys.executable, so we can't use realpath.
# but others can symlink *to* the venv Python,
......@@ -122,17 +184,17 @@ class Venv(object):
p = os.path.normcase(os.path.join(os.path.dirname(p), os.readlink(p)))
paths.append(p)
p_venv = os.path.normcase(os.environ["VIRTUAL_ENV"])
p_venv = os.path.normcase(str(venv))
if any(p.startswith(p_venv) for p in paths):
# Running properly in the virtualenv, don't need to do anything
return cls()
venv = os.environ["VIRTUAL_ENV"]
return SystemEnv(
Path(sys.prefix), Path(getattr(sys, "base_prefix", sys.prefix))
)
return cls(venv)
return VirtualEnv(venv, Path(getattr(sys, "base_prefix", sys.prefix)))
@classmethod
def build(cls, path):
def build_venv(cls, path):
try:
from venv import EnvBuilder
......@@ -146,32 +208,92 @@ class Venv(object):
build(path)
@property
def venv(self):
return self._venv
def get_version_info(self): # type: () -> Tuple[int]
raise NotImplementedError()
@property
def python(self): # type: () -> str
def get_python_implementation(self): # type: () -> str
raise NotImplementedError()
def config_var(self, var): # type: () -> Any
raise NotImplementedError()
def run(self, bin, *args, **kwargs):
"""
Path to current python executable
Run a command inside the Python environment.
"""
return self._bin("python")
bin = self._bin(bin)
@property
def pip(self): # type: () -> str
cmd = [bin] + list(args)
shell = kwargs.get("shell", False)
call = kwargs.pop("call", False)
if shell:
cmd = " ".join(cmd)
try:
if self._is_windows:
kwargs["shell"] = True
if call:
return subprocess.call(cmd, stderr=subprocess.STDOUT, **kwargs)
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, **kwargs)
except CalledProcessError as e:
raise EnvCommandError(e)
return decode(output)
def execute(self, bin, *args, **kwargs):
bin = self._bin(bin)
return subprocess.call([bin] + list(args), **kwargs)
def is_venv(self): # type: () -> bool
raise NotImplementedError()
def _bin(self, bin): # type: (str) -> str
"""
Path to current pip executable
Return path to the given executable.
"""
return self._bin("pip")
bin_path = (self._bin_dir / bin).with_suffix(".exe" if self._is_windows else "")
if not bin_path.exists():
return bin
@property
def version_info(self): # type: () -> tuple
if self._version_info is not None:
return self._version_info
return str(bin_path)
if not self.is_venv():
self._version_info = sys.version_info
else:
def __repr__(self):
return '{}("{}")'.format(self.__class__.__name__, self._base)
class SystemEnv(Env):
"""
A system (i.e. not a virtualenv) Python environment.
"""
def get_version_info(self): # type: () -> Tuple[int]
return sys.version_info
def get_python_implementation(self): # type: () -> str
return platform.python_implementation()
def config_var(self, var): # type: (str) -> Any
try:
return sysconfig.get_config_var(var)
except IOError as e:
warnings.warn("{0}".format(e), RuntimeWarning)
return
def is_venv(self): # type: () -> bool
return self._path != self._base
class VirtualEnv(Env):
"""
A virtual Python environment.
"""
def get_version_info(self): # type: () -> Tuple[int]
output = self.run(
"python",
"-c",
......@@ -179,37 +301,17 @@ class Venv(object):
shell=True,
)
self._version_info = tuple([int(s) for s in output.strip().split(".")])
return self._version_info
@property
def python_implementation(self):
if self._python_implementation is not None:
return self._python_implementation
return tuple([int(s) for s in output.strip().split(".")])
if not self.is_venv():
impl = platform.python_implementation()
else:
impl = self.run(
def get_python_implementation(self): # type: () -> str
return self.run(
"python",
"-c",
'"import platform; print(platform.python_implementation())"',
shell=True,
).strip()
self._python_implementation = impl
return self._python_implementation
def config_var(self, var):
if not self.is_venv():
try:
return sysconfig.get_config_var(var)
except IOError as e:
warnings.warn("{0}".format(e), RuntimeWarning)
return None
def config_var(self, var): # type: (str) -> Any
try:
value = self.run(
"python",
......@@ -231,65 +333,28 @@ class Venv(object):
return value
def run(self, bin, *args, **kwargs):
"""
Run a command inside the virtual env.
"""
if self._windows:
bin = self._bin(bin)
cmd = [bin] + list(args)
shell = kwargs.get("shell", False)
call = kwargs.pop("call", False)
if shell:
cmd = " ".join(cmd)
try:
if not self.is_venv():
if call:
return subprocess.call(cmd, stderr=subprocess.STDOUT, **kwargs)
output = subprocess.check_output(
cmd, stderr=subprocess.STDOUT, **kwargs
)
else:
if self._windows:
kwargs["shell"] = True
def is_venv(self): # type: () -> bool
return True
def run(self, bin, *args, **kwargs):
with self.temp_environ():
os.environ["PATH"] = self._path()
os.environ["VIRTUAL_ENV"] = str(self._venv)
os.environ["PATH"] = self._updated_path()
os.environ["VIRTUAL_ENV"] = str(self._path)
self.unset_env("PYTHONHOME")
self.unset_env("__PYVENV_LAUNCHER__")
if call:
return subprocess.call(cmd, stderr=subprocess.STDOUT, **kwargs)
output = subprocess.check_output(
cmd, stderr=subprocess.STDOUT, **kwargs
)
except CalledProcessError as e:
raise VenvCommandError(e)
return decode(output)
return super(VirtualEnv, self).run(bin, *args, **kwargs)
def execute(self, bin, *args, **kwargs):
if not self.is_venv():
return subprocess.call([bin] + list(args))
else:
if self._windows:
bin = self._bin(bin)
with self.temp_environ():
os.environ["PATH"] = self._path()
os.environ["VIRTUAL_ENV"] = str(self._venv)
os.environ["PATH"] = self._updated_path()
os.environ["VIRTUAL_ENV"] = str(self._path)
self.unset_env("PYTHONHOME")
self.unset_env("__PYVENV_LAUNCHER__")
return subprocess.call([bin] + list(args), **kwargs)
return super(VirtualEnv, self).execute(bin, *args, **kwargs)
@contextmanager
def temp_environ(self):
......@@ -300,47 +365,29 @@ class Venv(object):
os.environ.clear()
os.environ.update(environ)
def _path(self):
return os.pathsep.join([str(self._bin_dir), os.environ["PATH"]])
def unset_env(self, key):
if key in os.environ:
del os.environ[key]
def get_shell(self):
shell = Path(os.environ.get("SHELL", "")).stem
if shell in ("bash", "zsh", "fish"):
return shell
def _bin(self, bin): # type: (str) -> str
"""
Return path to the given executable.
"""
if not self.is_venv():
return bin
bin_path = (self._bin_dir / bin).with_suffix(".exe" if self._windows else "")
if not bin_path.exists():
return bin
return str(bin_path)
def _updated_path(self):
return os.pathsep.join([str(self._bin_dir), os.environ["PATH"]])
def is_venv(self): # type: () -> bool
return self._venv is not None
class NullEnv(SystemEnv):
def __init__(self, path=None, base=None, execute=False):
if path is None:
path = Path(sys.prefix)
class NullVenv(Venv):
def __init__(self, execute=False):
super(NullVenv, self).__init__()
super(NullEnv, self).__init__(path, base=base)
self.executed = []
self._execute = execute
self.executed = []
def run(self, bin, *args):
self.executed.append([bin] + list(args))
if self._execute:
return super(NullVenv, self).run(bin, *args)
return super(NullEnv, self).run(bin, *args)
def _bin(self, bin):
return bin
......@@ -15,7 +15,7 @@ from poetry.repositories.installed_repository import InstalledRepository
from poetry.utils._compat import Path
from poetry.utils._compat import PY2
from poetry.utils.toml_file import TomlFile
from poetry.utils.venv import NullVenv
from poetry.utils.env import NullEnv
from tests.helpers import get_dependency
from tests.helpers import get_package
......@@ -29,7 +29,7 @@ class Installer(BaseInstaller):
class CustomInstalledRepository(InstalledRepository):
@classmethod
def load(cls, venv):
def load(cls, env):
return cls()
......@@ -121,13 +121,13 @@ def locker():
@pytest.fixture()
def venv():
return NullVenv()
def env():
return NullEnv()
@pytest.fixture()
def installer(package, pool, locker, venv, installed):
return Installer(NullIO(), venv, package, locker, pool, installed=installed)
def installer(package, pool, locker, env, installed):
return Installer(NullIO(), env, package, locker, pool, installed=installed)
def fixture(name):
......@@ -583,7 +583,7 @@ def test_installer_with_pypi_repository(package, locker, installed):
pool.add_repository(MockRepository())
installer = Installer(
NullIO(), NullVenv(), package, locker, pool, installed=installed
NullIO(), NullEnv(), package, locker, pool, installed=installed
)
package.add_dependency("pytest", "^3.5", category="dev")
......
......@@ -14,7 +14,7 @@ from poetry.masonry.builders import CompleteBuilder
from poetry.poetry import Poetry
from poetry.utils._compat import Path
from poetry.utils._compat import decode
from poetry.utils.venv import NullVenv
from poetry.utils.env import NullEnv
fixtures_dir = Path(__file__).parent / "fixtures"
......@@ -40,7 +40,9 @@ def clear_samples_dist():
)
def test_wheel_c_extension():
module_path = fixtures_dir / "extended"
builder = CompleteBuilder(Poetry.create(module_path), NullVenv(True), NullIO())
builder = CompleteBuilder(
Poetry.create(module_path), NullEnv(execute=True), NullIO()
)
builder.build()
sdist = fixtures_dir / "extended" / "dist" / "extended-0.1.tar.gz"
......@@ -87,7 +89,9 @@ $""".format(
def test_complete():
module_path = fixtures_dir / "complete"
builder = CompleteBuilder(Poetry.create(module_path), NullVenv(True), NullIO())
builder = CompleteBuilder(
Poetry.create(module_path), NullEnv(execute=True), NullIO()
)
builder.build()
whl = module_path / "dist" / "my_package-1.2.3-py3-none-any.whl"
......@@ -159,7 +163,9 @@ My Package
def test_module_src():
module_path = fixtures_dir / "source_file"
builder = CompleteBuilder(Poetry.create(module_path), NullVenv(True), NullIO())
builder = CompleteBuilder(
Poetry.create(module_path), NullEnv(execute=True), NullIO()
)
builder.build()
sdist = module_path / "dist" / "module-src-0.1.tar.gz"
......@@ -183,7 +189,9 @@ def test_module_src():
def test_package_src():
module_path = fixtures_dir / "source_package"
builder = CompleteBuilder(Poetry.create(module_path), NullVenv(True), NullIO())
builder = CompleteBuilder(
Poetry.create(module_path), NullEnv(execute=True), NullIO()
)
builder.build()
sdist = module_path / "dist" / "package-src-0.1.tar.gz"
......
......@@ -14,7 +14,7 @@ from poetry.packages import Package
from poetry.poetry import Poetry
from poetry.utils._compat import Path
from poetry.utils._compat import to_str
from poetry.utils.venv import NullVenv
from poetry.utils.env import NullEnv
from tests.helpers import get_dependency
......@@ -106,7 +106,7 @@ def test_convert_dependencies():
def test_make_setup():
poetry = Poetry.create(project("complete"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
setup = builder.build_setup()
setup_ast = ast.parse(setup)
......@@ -131,7 +131,7 @@ def test_make_setup():
def test_make_pkg_info():
poetry = Poetry.create(project("complete"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
pkg_info = builder.build_pkg_info()
p = Parser()
parsed = p.parsestr(to_str(pkg_info))
......@@ -169,7 +169,7 @@ def test_make_pkg_info():
def test_make_pkg_info_any_python():
poetry = Poetry.create(project("module1"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
pkg_info = builder.build_pkg_info()
p = Parser()
parsed = p.parsestr(to_str(pkg_info))
......@@ -180,7 +180,7 @@ def test_make_pkg_info_any_python():
def test_find_files_to_add():
poetry = Poetry.create(project("complete"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
result = builder.find_files_to_add()
assert sorted(result) == sorted(
......@@ -200,7 +200,7 @@ def test_find_files_to_add():
def test_find_packages():
poetry = Poetry.create(project("complete"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
base = project("complete")
include = PackageInclude(base, "my_package")
......@@ -217,7 +217,7 @@ def test_find_packages():
poetry = Poetry.create(project("source_package"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
base = project("source_package")
include = PackageInclude(base, "package_src", "src")
......@@ -232,7 +232,7 @@ def test_find_packages():
def test_package():
poetry = Poetry.create(project("complete"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
builder.build()
sdist = fixtures_dir / "complete" / "dist" / "my-package-1.2.3.tar.gz"
......@@ -246,7 +246,7 @@ def test_package():
def test_module():
poetry = Poetry.create(project("module1"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
builder.build()
sdist = fixtures_dir / "module1" / "dist" / "module1-0.1.tar.gz"
......@@ -260,7 +260,7 @@ def test_module():
def test_prelease():
poetry = Poetry.create(project("prerelease"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
builder.build()
sdist = fixtures_dir / "prerelease" / "dist" / "prerelease-0.1b1.tar.gz"
......@@ -271,7 +271,7 @@ def test_prelease():
def test_with_c_extensions():
poetry = Poetry.create(project("extended"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
builder.build()
sdist = fixtures_dir / "extended" / "dist" / "extended-0.1.tar.gz"
......@@ -286,7 +286,7 @@ def test_with_c_extensions():
def test_with_src_module_file():
poetry = Poetry.create(project("source_file"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
# Check setup.py
setup = builder.build_setup()
......@@ -311,7 +311,7 @@ def test_with_src_module_file():
def test_with_src_module_dir():
poetry = Poetry.create(project("source_package"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
# Check setup.py
setup = builder.build_setup()
......@@ -356,7 +356,7 @@ def test_package_with_include(mocker):
]
poetry = Poetry.create(project("with-include"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
# Check setup.py
setup = builder.build_setup()
......@@ -394,7 +394,7 @@ def test_package_with_include(mocker):
def test_proper_python_requires_if_single_version_specified():
poetry = Poetry.create(project("simple_version"))
builder = SdistBuilder(poetry, NullVenv(), NullIO())
builder = SdistBuilder(poetry, NullEnv(), NullIO())
pkg_info = builder.build_pkg_info()
p = Parser()
parsed = p.parsestr(to_str(pkg_info))
......
......@@ -6,7 +6,7 @@ from poetry.io import NullIO
from poetry.masonry.builders import WheelBuilder
from poetry.poetry import Poetry
from poetry.utils._compat import Path
from poetry.utils.venv import NullVenv
from poetry.utils.env import NullEnv
fixtures_dir = Path(__file__).parent / "fixtures"
......@@ -29,7 +29,7 @@ def clear_samples_dist():
def test_wheel_module():
module_path = fixtures_dir / "module1"
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullEnv(), NullIO())
whl = module_path / "dist" / "module1-0.1-py2.py3-none-any.whl"
......@@ -41,7 +41,7 @@ def test_wheel_module():
def test_wheel_package():
module_path = fixtures_dir / "complete"
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullEnv(), NullIO())
whl = module_path / "dist" / "my_package-1.2.3-py3-none-any.whl"
......@@ -53,7 +53,7 @@ def test_wheel_package():
def test_wheel_prerelease():
module_path = fixtures_dir / "prerelease"
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullEnv(), NullIO())
whl = module_path / "dist" / "prerelease-0.1b1-py2.py3-none-any.whl"
......@@ -62,7 +62,7 @@ def test_wheel_prerelease():
def test_wheel_package_src():
module_path = fixtures_dir / "source_package"
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullEnv(), NullIO())
whl = module_path / "dist" / "package_src-0.1-py2.py3-none-any.whl"
......@@ -75,7 +75,7 @@ def test_wheel_package_src():
def test_wheel_module_src():
module_path = fixtures_dir / "source_file"
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullEnv(), NullIO())
whl = module_path / "dist" / "module_src-0.1-py2.py3-none-any.whl"
......@@ -106,7 +106,7 @@ def test_package_with_include(mocker):
),
]
module_path = fixtures_dir / "with-include"
WheelBuilder.make(Poetry.create(str(module_path)), NullVenv(), NullIO())
WheelBuilder.make(Poetry.create(str(module_path)), NullEnv(), NullIO())
whl = module_path / "dist" / "with_include-1.2.3-py3-none-any.whl"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment