diff --git a/poetry/inspection/info.py b/poetry/inspection/info.py index bf880223937..7d589c5c405 100644 --- a/poetry/inspection/info.py +++ b/poetry/inspection/info.py @@ -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 from poetry.utils.toml_file import TomlFile @@ -441,13 +440,9 @@ def _pep517_metadata(cls, path): # type (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: diff --git a/poetry/installation/executor.py b/poetry/installation/executor.py index cc00a9d06e1..aabf7d9d6f4 100644 --- a/poetry/installation/executor.py +++ b/poetry/installation/executor.py @@ -20,6 +20,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 @@ -523,14 +524,14 @@ def _install_directory(self, operation): 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) diff --git a/poetry/installation/pip_installer.py b/poetry/installation/pip_installer.py index fac938e1142..cb67d26be0a 100644 --- a/poetry/installation/pip_installer.py +++ b/poetry/installation/pip_installer.py @@ -14,6 +14,7 @@ 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 try: @@ -235,14 +236,13 @@ def install_directory(self, package): with builder.setup_py(): if package.develop: - args.append("-e") - + return pip_editable_install(req, self._env) args.append(req) return self.run(*args) if package.develop: - args.append("-e") + return pip_editable_install(req, self._env) args.append(req) diff --git a/poetry/masonry/builders/editable.py b/poetry/masonry/builders/editable.py index 5c32fcb57e1..acaa1bb99e0 100644 --- a/poetry/masonry/builders/editable.py +++ b/poetry/masonry/builders/editable.py @@ -13,6 +13,7 @@ from poetry.utils._compat import WINDOWS from poetry.utils._compat import Path from poetry.utils._compat import decode +from poetry.utils.pip import pip_editable_install SCRIPT_TEMPLATE = """\ @@ -47,7 +48,6 @@ def build(self): self._debug( " - Falling back on using a setup.py" ) - return self._setup_build() self._run_build_script(self._package.build_script) @@ -76,14 +76,14 @@ def _setup_build(self): 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")), diff --git a/poetry/utils/env.py b/poetry/utils/env.py index 2e97957ebc9..634c6f46b7f 100644 --- a/poetry/utils/env.py +++ b/poetry/utils/env.py @@ -39,6 +39,7 @@ from poetry.utils._compat import encode from poetry.utils._compat import list_to_shell_command from poetry.utils._compat import subprocess +from poetry.utils.helpers import temporary_directory from poetry.utils.toml_file import TomlFile @@ -680,8 +681,13 @@ def create_venv( @classmethod def build_venv( - cls, path, executable=None, with_pip=False - ): # type: (Union[Path,str], Optional[Union[str, Path]], bool) -> virtualenv.run.session.Session + cls, + path, + executable=None, + with_pip=False, + with_wheel=None, + with_setuptools=None, + ): # type: (Union[Path,str], Optional[Union[str, Path]], bool, Optional[bool], Optional[bool]) -> virtualenv.run.session.Session if isinstance(executable, Path): executable = executable.resolve().as_posix() @@ -693,7 +699,16 @@ def build_venv( ] if not with_pip: - opts.extend(["--no-pip", "--no-wheel", "--no-setuptools"]) + opts.append("--no-pip") + else: + if with_wheel is None: + with_wheel = True + + if with_wheel is None or not with_wheel: + opts.append("--no-wheel") + + if with_setuptools is None or not with_setuptools: + opts.append("--no-setuptools") opts.append(str(path)) @@ -1250,6 +1265,21 @@ def _bin(self, bin): 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, diff --git a/poetry/utils/pip.py b/poetry/utils/pip.py new file mode 100644 index 00000000000..97e7dc6d0d7 --- /dev/null +++ b/poetry/utils/pip.py @@ -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 + ) diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index bb659321d0f..3064a0451fb 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -57,8 +57,12 @@ def callback(request, uri, headers): def test_execute_executes_a_batch_of_operations( - config, pool, io, tmp_dir, mock_file_downloads + mocker, config, pool, io, tmp_dir, mock_file_downloads ): + pip_editable_install = mocker.patch( + "poetry.installation.executor.pip_editable_install" + ) + config = Config() config.merge({"cache-dir": tmp_dir}) @@ -123,7 +127,8 @@ def test_execute_executes_a_batch_of_operations( expected = set(expected.splitlines()) output = set(io.fetch_output().splitlines()) assert expected == output - assert 5 == len(env.executed) + assert 4 == len(env.executed) + pip_editable_install.assert_called_once() def test_execute_shows_skipped_operations_if_verbose(config, pool, io): diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 5197d72981c..243c908d86d 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -6,8 +6,8 @@ import pytest from clikit.io import NullIO - from deepdiff import DeepDiff + from poetry.core.packages import ProjectPackage from poetry.factory import Factory from poetry.installation import Installer as BaseInstaller diff --git a/tests/installation/test_installer_old.py b/tests/installation/test_installer_old.py index cc7e5bf36e8..04448527c69 100644 --- a/tests/installation/test_installer_old.py +++ b/tests/installation/test_installer_old.py @@ -5,8 +5,8 @@ import pytest from clikit.io import NullIO - from deepdiff import DeepDiff + from poetry.core.packages import ProjectPackage from poetry.factory import Factory from poetry.installation import Installer as BaseInstaller diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py index ab9cca50075..a827728024f 100644 --- a/tests/masonry/builders/test_editable_builder.py +++ b/tests/masonry/builders/test_editable_builder.py @@ -164,16 +164,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, + mocker, extended_poetry, ): + pip_editable_install = mocker.patch( + "poetry.masonry.builders.editable.pip_editable_install" + ) env = MockEnv(path=Path("/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(