Skip to content

Commit

Permalink
Use isolated ephemeral envs for editable installs
Browse files Browse the repository at this point in the history
  • Loading branch information
abn committed Mar 23, 2021
1 parent 9a68da3 commit 6400f42
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 48 deletions.
11 changes: 3 additions & 8 deletions poetry/inspection/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
from poetry.core.utils.helpers import temporary_directory
from poetry.core.version.markers import InvalidMarker
from poetry.utils.env import EnvCommandError
from poetry.utils.env import EnvManager
from poetry.utils.env import VirtualEnv
from poetry.utils.env import ephemeral_environment
from poetry.utils.setup_reader import SetupReader


Expand Down Expand Up @@ -451,13 +450,9 @@ def _pep517_metadata(cls, path: Path) -> "PackageInfo":
except PackageInfoError:
pass

with temporary_directory() as tmp_dir:
with ephemeral_environment(pip=True, wheel=True, setuptools=True) as venv:
# TODO: cache PEP 517 build environment corresponding to each project venv
venv_dir = Path(tmp_dir) / ".venv"
EnvManager.build_venv(venv_dir.as_posix(), with_pip=True)
venv = VirtualEnv(venv_dir, venv_dir)

dest_dir = Path(tmp_dir) / "dist"
dest_dir = venv.path.parent / "dist"
dest_dir.mkdir()

try:
Expand Down
5 changes: 3 additions & 2 deletions poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from poetry.utils._compat import decode
from poetry.utils.env import EnvCommandError
from poetry.utils.helpers import safe_rmtree
from poetry.utils.pip import pip_editable_install

from .authenticator import Authenticator
from .chef import Chef
Expand Down Expand Up @@ -572,14 +573,14 @@ def _install_directory(self, operation: Union[Install, Update]) -> int:

with builder.setup_py():
if package.develop:
args.append("-e")
return pip_editable_install(req, self._env)

args.append(req)

return self.run_pip(*args)

if package.develop:
args.append("-e")
return pip_editable_install(req, self._env)

args.append(req)

Expand Down
36 changes: 15 additions & 21 deletions poetry/installation/pip_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@

from cleo.io.io import IO

from poetry.installation.base_installer import BaseInstaller
from poetry.core.pyproject.toml import PyProjectTOML
from poetry.installation.base_installer import BaseInstaller
from poetry.repositories.pool import Pool
from poetry.utils._compat import encode
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
from poetry.utils.env import VirtualEnv
from poetry.utils.helpers import safe_rmtree
from poetry.utils.helpers import temporary_directory
from poetry.utils.pip import pip_editable_install
from poetry.utils.pip import pip_install


if TYPE_CHECKING:
from poetry.core.packages.package import Package
Expand Down Expand Up @@ -190,12 +190,12 @@ def install_directory(self, package: "Package") -> Union[str, int]:

from poetry.factory import Factory

req: Path

if package.root_dir:
req = (package.root_dir / package.source_url).as_posix()
else:
req = os.path.realpath(package.source_url)

args = ["install", "--no-deps", "-U", "--root", str(self._env.path)]
req = Path(package.source_url).resolve(strict=False)

pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml"))

Expand Down Expand Up @@ -230,22 +230,16 @@ def install_directory(self, package: "Package") -> Union[str, int]:

with builder.setup_py():
if package.develop:
args.append("-e")

args.append(req)

return self.run(*args)
return pip_editable_install(
directory=req, environment=self._env
)
return pip_install(
path=req, environment=self._env, deps=False, upgrade=True
)

if package.develop:
args.append("-e")

args.append(req)

with temporary_directory() as tmp_dir:
venv_dir = Path(tmp_dir) / ".venv"
EnvManager.build_venv(venv_dir.as_posix(), with_pip=True)
venv = VirtualEnv(venv_dir, venv_dir)
return venv.run("pip", *args)
return pip_editable_install(directory=req, environment=self._env)
return pip_install(path=req, environment=self._env, deps=False, upgrade=True)

def install_git(self, package: "Package") -> None:
from poetry.core.packages.package import Package
Expand Down
6 changes: 3 additions & 3 deletions poetry/masonry/builders/editable.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from poetry.utils._compat import WINDOWS
from poetry.utils._compat import decode
from poetry.utils.helpers import is_dir_writable
from poetry.utils.pip import pip_editable_install


if TYPE_CHECKING:
Expand Down Expand Up @@ -56,7 +57,6 @@ def build(self) -> None:
self._debug(
" - <warning>Falling back on using a <b>setup.py</b></warning>"
)

return self._setup_build()

self._run_build_script(self._package.build_script)
Expand Down Expand Up @@ -85,14 +85,14 @@ def _setup_build(self) -> None:

try:
if self._env.pip_version < Version(19, 0):
self._env.run_pip("install", "-e", str(self._path), "--no-deps")
pip_editable_install(self._path, self._env)
else:
# Temporarily rename pyproject.toml
shutil.move(
str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp"))
)
try:
self._env.run_pip("install", "-e", str(self._path), "--no-deps")
pip_editable_install(self._path, self._env)
finally:
shutil.move(
str(self._poetry.file.with_suffix(".tmp")),
Expand Down
29 changes: 28 additions & 1 deletion poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from poetry.utils._compat import list_to_shell_command
from poetry.utils.helpers import is_dir_writable
from poetry.utils.helpers import paths_csv
from poetry.utils.helpers import temporary_directory


GET_ENVIRONMENT_INFO = """\
Expand Down Expand Up @@ -812,6 +813,8 @@ def build_venv(
executable: Optional[Union[str, Path]] = None,
flags: Dict[str, bool] = None,
with_pip: bool = False,
with_wheel: bool = False,
with_setuptools: bool = False,
) -> virtualenv.run.session.Session:
flags = flags or {}

Expand All @@ -826,7 +829,16 @@ def build_venv(
]

if not with_pip:
args.extend(["--no-pip", "--no-wheel", "--no-setuptools"])
args.append("--no-pip")
else:
if with_wheel is None:
with_wheel = True

if with_wheel is None or not with_wheel:
args.append("--no-wheel")

if with_setuptools is None or not with_setuptools:
args.append("--no-setuptools")

for flag, value in flags.items():
if value is True:
Expand Down Expand Up @@ -1429,6 +1441,21 @@ def _bin(self, bin: str) -> str:
return bin


@contextmanager
def ephemeral_environment(executable=None, pip=False, wheel=None, setuptools=None):
with temporary_directory() as tmp_dir:
# TODO: cache PEP 517 build environment corresponding to each project venv
venv_dir = Path(tmp_dir) / ".venv"
EnvManager.build_venv(
path=venv_dir.as_posix(),
executable=executable,
with_pip=pip,
with_wheel=wheel,
with_setuptools=setuptools,
)
yield VirtualEnv(venv_dir, venv_dir)


class MockEnv(NullEnv):
def __init__(
self,
Expand Down
38 changes: 38 additions & 0 deletions poetry/utils/pip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from poetry.exceptions import PoetryException
from poetry.utils._compat import Path
from poetry.utils.env import Env
from poetry.utils.env import ephemeral_environment


def pip_install(
path, environment, editable=False, deps=False, upgrade=False
): # type: (Path, Env, bool, bool, bool) -> None
path = Path(path) if isinstance(path, str) else path

args = ["pip", "install", "--prefix", str(environment.path)]

if upgrade:
args.append("--upgrade")

if not deps:
args.append("--no-deps")

if editable:
if not path.is_dir():
raise PoetryException(
"Cannot install non directory dependencies in editable mode"
)
args.append("-e")

args.append(str(path))

with ephemeral_environment(
executable=environment.python, pip=True, setuptools=True
) as env:
return env.run(*args)


def pip_editable_install(directory, environment): # type: (Path, Env) -> None
return pip_install(
path=directory, environment=environment, editable=True, deps=False, upgrade=True
)
10 changes: 8 additions & 2 deletions tests/installation/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ def callback(request, uri, headers):


def test_execute_executes_a_batch_of_operations(
config, pool, io, tmp_dir, mock_file_downloads, env
mocker, config, pool, io, tmp_dir, mock_file_downloads, env
):
pip_editable_install = mocker.patch(
"poetry.installation.executor.pip_editable_install"
)

config = Config()
config.merge({"cache-dir": tmp_dir})

Expand Down Expand Up @@ -101,6 +105,7 @@ def test_execute_executes_a_batch_of_operations(
source_type="git",
source_reference="master",
source_url="https://github.com/demo/demo.git",
develop=True,
)

return_code = executor.execute(
Expand Down Expand Up @@ -131,8 +136,9 @@ def test_execute_executes_a_batch_of_operations(
expected = set(expected.splitlines())
output = set(io.fetch_output().splitlines())
assert expected == output
assert 6 == len(env.executed)
assert 4 == len(env.executed)
assert 0 == return_code
pip_editable_install.assert_called_once()


def test_execute_shows_skipped_operations_if_verbose(
Expand Down
5 changes: 2 additions & 3 deletions tests/installation/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from cleo.io.null_io import NullIO
from cleo.io.outputs.buffered_output import BufferedOutput
from cleo.io.outputs.output import Verbosity

from deepdiff import DeepDiff

from poetry.core.packages.project_package import ProjectPackage
from poetry.core.toml.file import TOMLFile
from poetry.factory import Factory
Expand Down Expand Up @@ -833,8 +833,7 @@ def test_installer_with_pypi_repository(package, locker, installed, config):
installer.run()

expected = fixture("with-pypi-repository")

assert not DeepDiff(locker.written_data, expected, ignore_order=True)
assert not DeepDiff(expected, locker.written_data, ignore_order=True)


def test_run_installs_with_local_file(installer, locker, repo, package, fixture_dir):
Expand Down
5 changes: 2 additions & 3 deletions tests/installation/test_installer_old.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import pytest

from cleo.io.null_io import NullIO

from deepdiff import DeepDiff

from poetry.core.packages.project_package import ProjectPackage
from poetry.core.toml.file import TOMLFile
from poetry.factory import Factory
Expand Down Expand Up @@ -738,8 +738,7 @@ def test_installer_with_pypi_repository(package, locker, installed, config):
installer.run()

expected = fixture("with-pypi-repository")

assert not DeepDiff(locker.written_data, expected, ignore_order=True)
assert not DeepDiff(expected, locker.written_data, ignore_order=True)


def test_run_installs_with_local_file(installer, locker, repo, package, fixture_dir):
Expand Down
13 changes: 8 additions & 5 deletions tests/masonry/builders/test_editable_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,19 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_


def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts(
extended_poetry, tmp_dir
mocker, extended_poetry, tmp_dir
):
pip_editable_install = mocker.patch(
"poetry.masonry.builders.editable.pip_editable_install"
)
env = MockEnv(path=Path(tmp_dir) / "foo")
builder = EditableBuilder(extended_poetry, env, NullIO())

builder.build()
assert [
env.get_pip_command()
+ ["install", "-e", str(extended_poetry.file.parent), "--no-deps"]
] == env.executed
pip_editable_install.assert_called_once_with(
extended_poetry.pyproject.file.path.parent, env
)
assert [] == env.executed


def test_builder_installs_proper_files_when_packages_configured(
Expand Down

0 comments on commit 6400f42

Please sign in to comment.