From d020c1954706108bfce260ef56f87730ee8a77f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sat, 11 Jan 2020 16:50:43 +0100 Subject: [PATCH 1/2] Add a bundle venv command --- docs/docs/cli.md | 42 ++++ poetry/bundle/__init__.py | 0 poetry/bundle/bundler.py | 22 ++ poetry/bundle/bundler_manager.py | 39 ++++ poetry/bundle/exceptions.py | 3 + poetry/bundle/venv_bundler.py | 198 +++++++++++++++++ poetry/console/application.py | 20 ++ poetry/console/commands/bundle/__init__.py | 0 .../console/commands/bundle/bundle_command.py | 26 +++ poetry/console/commands/bundle/venv.py | 57 +++++ poetry/utils/env.py | 85 +++---- tests/bundle/__init__.py | 0 .../bundle/fixtures/simple_project/README.rst | 2 + .../fixtures/simple_project/poetry.lock | 14 ++ .../fixtures/simple_project/pyproject.toml | 30 +++ .../simple_project/simple_project/__init__.py | 0 .../simple_project_with_no_module/README.rst | 2 + .../simple_project_with_no_module/poetry.lock | 14 ++ .../pyproject.toml | 30 +++ tests/bundle/test_bundler_manager.py | 51 +++++ tests/bundle/test_venv_bundler.py | 208 ++++++++++++++++++ tests/console/commands/bundle/__init__.py | 0 tests/console/commands/bundle/test_venv.py | 28 +++ 23 files changed, 819 insertions(+), 52 deletions(-) create mode 100644 poetry/bundle/__init__.py create mode 100644 poetry/bundle/bundler.py create mode 100644 poetry/bundle/bundler_manager.py create mode 100644 poetry/bundle/exceptions.py create mode 100644 poetry/bundle/venv_bundler.py create mode 100644 poetry/console/commands/bundle/__init__.py create mode 100644 poetry/console/commands/bundle/bundle_command.py create mode 100644 poetry/console/commands/bundle/venv.py create mode 100644 tests/bundle/__init__.py create mode 100644 tests/bundle/fixtures/simple_project/README.rst create mode 100644 tests/bundle/fixtures/simple_project/poetry.lock create mode 100644 tests/bundle/fixtures/simple_project/pyproject.toml create mode 100644 tests/bundle/fixtures/simple_project/simple_project/__init__.py create mode 100644 tests/bundle/fixtures/simple_project_with_no_module/README.rst create mode 100644 tests/bundle/fixtures/simple_project_with_no_module/poetry.lock create mode 100644 tests/bundle/fixtures/simple_project_with_no_module/pyproject.toml create mode 100644 tests/bundle/test_bundler_manager.py create mode 100644 tests/bundle/test_venv_bundler.py create mode 100644 tests/console/commands/bundle/__init__.py create mode 100644 tests/console/commands/bundle/test_venv.py diff --git a/docs/docs/cli.md b/docs/docs/cli.md index 4675ce1023b..abc9f6413cc 100644 --- a/docs/docs/cli.md +++ b/docs/docs/cli.md @@ -507,6 +507,48 @@ associated with a specific project. See [Managing environments](/docs/managing-environments/) for more information about these commands. +## bundle + +The `bundle` namespace regroups commands to bundle the current project +and its dependencies into various formats. These commands are particularly useful to deploy +Poetry-managed applications. + +### bundle venv + +The `bundle venv` command bundles the project and its dependencies into a virtual environment. + +The following command + +```bash +poetry bundle venv /path/to/environment +``` + +will bundle the project in the `/path/to/environment` directory by creating the virtual environment, +installing the dependencies and the current project inside it. If the directory does not exist, +it will be created automatically. + +By default, the command uses the current Python executable to build the virtual environment. +If you want to use a different one, you can specify it with the `--python/-p` option: + +```bash +poetry bundle venv /path/to/environment --python /full/path/to/python +poetry bundle venv /path/to/environment -p python3.8 +poetry bundle venv /path/to/environment -p 3.8 +``` + +!!!note + + If the virtual environment already exists, two things can happen: + + - **The python version of the virtual environment is the same as the main one**: the dependencies will be synced (updated or removed). + - **The python version of the virtual environment is different**: the virtual environment will be recreated from scratch. + + You can also ensure that the virtual environment is recreated by using the `--clear` option: + + ```bash + poetry bundle venv /path/to/environment --clear + ``` + ## cache The `cache` command regroups sub commands to interact with Poetry's cache. diff --git a/poetry/bundle/__init__.py b/poetry/bundle/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/poetry/bundle/bundler.py b/poetry/bundle/bundler.py new file mode 100644 index 00000000000..b6e5cb30e05 --- /dev/null +++ b/poetry/bundle/bundler.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING +from typing import Optional + + +if TYPE_CHECKING: + from pathlib import Path + from typing import Optional + + from clikit.api.io.io import IO + + from poetry.poetry import Poetry + + +class Bundler(object): + @property + def name(self) -> str: + raise NotImplementedError() + + def bundle( + self, poetry: "Poetry", io: "IO", path: "Path", executable: Optional[str] = None + ) -> None: + raise NotImplementedError() diff --git a/poetry/bundle/bundler_manager.py b/poetry/bundle/bundler_manager.py new file mode 100644 index 00000000000..8fecfbd86eb --- /dev/null +++ b/poetry/bundle/bundler_manager.py @@ -0,0 +1,39 @@ +from typing import TYPE_CHECKING +from typing import Dict +from typing import List + +from .exceptions import BundlerManagerError + + +if TYPE_CHECKING: + from .bundler import Bundler + + +class BundlerManager(object): + def __init__(self) -> None: + from .venv_bundler import VenvBundler + + self._bundlers: Dict[str, "Bundler"] = {} + + # Register default bundlers + self.register_bundler(VenvBundler()) + + @property + def bundlers(self) -> List["Bundler"]: + return list(self._bundlers.values()) + + def bundler(self, name: str) -> "Bundler": + if name.lower() not in self._bundlers: + raise BundlerManagerError('The bundler "{}" does not exist.'.format(name)) + + return self._bundlers[name.lower()] + + def register_bundler(self, bundler: "Bundler") -> "BundlerManager": + if bundler.name.lower() in self._bundlers: + raise BundlerManagerError( + 'A bundler with the name "{}" already exists.'.format(bundler.name) + ) + + self._bundlers[bundler.name.lower()] = bundler + + return self diff --git a/poetry/bundle/exceptions.py b/poetry/bundle/exceptions.py new file mode 100644 index 00000000000..d7977fc025b --- /dev/null +++ b/poetry/bundle/exceptions.py @@ -0,0 +1,3 @@ +class BundlerManagerError(Exception): + + pass diff --git a/poetry/bundle/venv_bundler.py b/poetry/bundle/venv_bundler.py new file mode 100644 index 00000000000..0e85ad1364e --- /dev/null +++ b/poetry/bundle/venv_bundler.py @@ -0,0 +1,198 @@ +import sys + +from typing import TYPE_CHECKING +from typing import Optional +from typing import cast + +from .bundler import Bundler + + +if TYPE_CHECKING: + from pathlib import Path + from typing import Optional + + from cleo.io.io import IO + from cleo.io.outputs.section_output import SectionOutput # noqa + + from poetry.poetry import Poetry + + +class VenvBundler(Bundler): + @property + def name(self): # type: () -> str + return "venv" + + def bundle( + self, + poetry: "Poetry", + io: "IO", + path: "Path", + executable: Optional[str] = None, + remove: bool = False, + ) -> bool: + from pathlib import Path + + from cleo.io.null_io import NullIO + + from poetry.core.masonry.builders.wheel import WheelBuilder + from poetry.core.masonry.utils.module import ModuleOrPackageNotFound + from poetry.core.packages.package import Package + from poetry.core.semver.version import Version + from poetry.installation.installer import Installer + from poetry.installation.operations.install import Install + from poetry.utils.env import EnvManager + from poetry.utils.env import SystemEnv + from poetry.utils.env import VirtualEnv + from poetry.utils.helpers import temporary_directory + + warnings = [] + + manager = EnvManager(poetry) + if executable is not None: + executable, python_version = manager.get_executable_info(executable) + else: + version_info = SystemEnv(Path(sys.prefix)).get_version_info() + python_version = Version(*version_info[:3]) + + message = self._get_message(poetry, path) + if io.is_decorated() and not io.is_debug(): + io = io.section() + + io.write_line(message) + + if path.exists(): + env = VirtualEnv(path) + env_python_version = Version(*env.version_info[:3]) + if not env.is_sane() or env_python_version != python_version or remove: + self._write( + io, message + ": Removing existing virtual environment" + ) + + manager.remove_venv(str(path)) + + self._write( + io, + message + + ": Creating a virtual environment using Python {}".format( + python_version + ), + ) + + manager.build_venv(str(path), executable=executable) + else: + self._write( + io, + message + + ": Using existing virtual environment".format( + python_version + ), + ) + else: + self._write( + io, + message + + ": Creating a virtual environment using Python {}".format( + python_version + ), + ) + + manager.build_venv(str(path), executable=executable) + + env = VirtualEnv(path) + + self._write( + io, + message + ": Installing dependencies".format(python_version), + ) + + installer = Installer( + NullIO() if not io.is_debug() else io, + env, + poetry.package, + poetry.locker, + poetry.pool, + poetry.config, + ) + installer.remove_untracked() + installer.use_executor(poetry.config.get("experimental.new-installer", False)) + + return_code = installer.run() + if return_code: + self._write( + io, + self._get_message(poetry, path, error=True) + + ": Failed at step Installing dependencies".format( + python_version + ), + ) + return False + + self._write( + io, + message + + ": Installing {} ({})".format( + poetry.package.pretty_name, poetry.package.pretty_version + ), + ) + + # Build a wheel of the project in a temporary directory + # and install it in the newly create virtual environment + with temporary_directory() as directory: + try: + wheel_name = WheelBuilder.make_in(poetry, directory=Path(directory)) + wheel = Path(directory).joinpath(wheel_name) + package = Package( + poetry.package.name, + poetry.package.version, + source_type="file", + source_url=wheel, + ) + installer.executor.execute([Install(package)]) + except ModuleOrPackageNotFound: + warnings.append( + "The root package was not installed because no matching module or package was found." + ) + + self._write(io, self._get_message(poetry, path, done=True)) + + if warnings: + for warning in warnings: + io.write_line( + " {}".format( + warning + ) + ) + + return True + + def _get_message( + self, poetry: "Poetry", path: "Path", done: bool = False, error: bool = False + ) -> str: + operation_color = "blue" + + if error: + operation_color = "red" + elif done: + operation_color = "green" + + verb = "Bundling" + if done: + verb = "Bundled" + + return " • {} {} ({}) into {}".format( + operation_color, + verb, + poetry.package.pretty_name, + poetry.package.pretty_version, + path, + ) + + def _write(self, io: "IO", message: str) -> None: + from cleo.io.outputs.section_output import SectionOutput # noqa + + if io.is_debug() or not io.is_decorated() or not isinstance(io, SectionOutput): + io.write_line(message) + return + + io = cast(SectionOutput, io) + io.overwrite(message) diff --git a/poetry/console/application.py b/poetry/console/application.py index e54cbb5d846..c5faee7eb32 100644 --- a/poetry/console/application.py +++ b/poetry/console/application.py @@ -59,6 +59,8 @@ def _load() -> Type[Command]: "show", "update", "version", + # Bundle commands + "bundle venv", # Cache commands "cache clear", "cache list", @@ -89,6 +91,7 @@ def __init__(self) -> None: dispatcher.add_listener(COMMAND, self.register_command_loggers) dispatcher.add_listener(COMMAND, self.set_env) dispatcher.add_listener(COMMAND, self.set_installer) + dispatcher.add_listener(COMMAND, self.configure_bundle_commands) self.set_event_dispatcher(dispatcher) command_loader = FactoryCommandLoader( @@ -272,6 +275,23 @@ def set_installer( installer.use_executor(poetry.config.get("experimental.new-installer", False)) command.set_installer(installer) + def configure_bundle_commands( + self, event: ConsoleCommandEvent, event_name: str, _: Any + ) -> None: + from .commands.bundle.bundle_command import BundleCommand + + command: BundleCommand = cast(BundleCommand, event.command) + if not isinstance(command, BundleCommand): + return + + # If the command already has a bundler manager, do nothing + if command.bundler_manager is not None: + return + + from poetry.bundle.bundler_manager import BundlerManager + + command.set_bundler_manager(BundlerManager()) + def main() -> int: return Application().run() diff --git a/poetry/console/commands/bundle/__init__.py b/poetry/console/commands/bundle/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/poetry/console/commands/bundle/bundle_command.py b/poetry/console/commands/bundle/bundle_command.py new file mode 100644 index 00000000000..5bf33b7395a --- /dev/null +++ b/poetry/console/commands/bundle/bundle_command.py @@ -0,0 +1,26 @@ +from typing import TYPE_CHECKING +from typing import Optional + +from poetry.console.commands.command import Command + + +if TYPE_CHECKING: + from poetry.bundle.bundler_manager import BundlerManager + + +class BundleCommand(Command): + """ + Base class for all bundle commands. + """ + + def __init__(self) -> None: + self._bundler_manager: Optional["BundlerManager"] = None + + super().__init__() + + @property + def bundler_manager(self) -> "BundlerManager": + return self._bundler_manager + + def set_bundler_manager(self, bundler_manager: "BundlerManager") -> None: + self._bundler_manager = bundler_manager diff --git a/poetry/console/commands/bundle/venv.py b/poetry/console/commands/bundle/venv.py new file mode 100644 index 00000000000..8a1c5b95722 --- /dev/null +++ b/poetry/console/commands/bundle/venv.py @@ -0,0 +1,57 @@ +from pathlib import Path +from typing import TYPE_CHECKING +from typing import cast + +from cleo.helpers import argument +from cleo.helpers import option + +from .bundle_command import BundleCommand + + +if TYPE_CHECKING: + from poetry.bundle.venv_bundler import VenvBundler # noqa + + +class BundleVenvCommand(BundleCommand): + + name = "bundle venv" + description = "Bundle the current project into a virtual environment" + + arguments = [ + argument("path", "The path to the virtual environment to bundle into."), + ] + + options = [ + option( + "python", + "p", + "The Python executable to use to create the virtual environment. " + "Defaults to the current Python executable", + flag=False, + value_required=True, + ), + option( + "clear", + None, + "Clear the existing virtual environment if it exists. ", + flag=True, + ), + ] + + def handle(self) -> int: + path = Path(self.argument("path")) + executable = self.option("python") + + bundler = cast("VenvBundler", self._bundler_manager.bundler("venv")) + + self.line("") + + return int( + not bundler.bundle( + self.poetry, + self._io, + path, + executable=executable, + remove=self.option("clear"), + ) + ) diff --git a/poetry/utils/env.py b/poetry/utils/env.py index ff78bf0161b..e9580e2304c 100644 --- a/poetry/utils/env.py +++ b/poetry/utils/env.py @@ -318,32 +318,7 @@ def activate(self, python: str, io: IO) -> "Env": envs_file = TOMLFile(venv_path / self.ENVS_FILE) - try: - python_version = Version.parse(python) - python = "python{}".format(python_version.major) - if python_version.precision > 1: - python += ".{}".format(python_version.minor) - except ValueError: - # Executable in PATH or full executable path - pass - - try: - python_version = decode( - subprocess.check_output( - list_to_shell_command( - [ - python, - "-c", - "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"", - ] - ), - shell=True, - ) - ) - except CalledProcessError as e: - raise EnvCommandError(e) - - python_version = Version.parse(python_version.strip()) + python, python_version = self.get_executable_info(python) minor = "{}.{}".format(python_version.major, python_version.minor) patch = python_version.text @@ -565,32 +540,7 @@ def remove(self, python: str) -> "Env": 'Environment "{}" does not exist.'.format(python) ) - try: - python_version = Version.parse(python) - python = "python{}".format(python_version.major) - if python_version.precision > 1: - python += ".{}".format(python_version.minor) - except ValueError: - # Executable in PATH or full executable path - pass - - try: - python_version = decode( - subprocess.check_output( - list_to_shell_command( - [ - python, - "-c", - "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"", - ] - ), - shell=True, - ) - ) - except CalledProcessError as e: - raise EnvCommandError(e) - - python_version = Version.parse(python_version.strip()) + python, python_version = self.get_executable_info(python) minor = "{}.{}".format(python_version.major, python_version.minor) name = "{}-py{}".format(base_env_name, minor) @@ -871,6 +821,37 @@ def generate_env_name(cls, name: str, cwd: str) -> str: return "{}-{}".format(sanitized_name, h) + @classmethod + def get_executable_info(cls, executable: str) -> Tuple[str, Version]: + try: + python_version = Version.parse(executable) + executable = "python{}".format(python_version.major) + if python_version.precision > 1: + executable += ".{}".format(python_version.minor) + except ValueError: + # Executable in PATH or full executable path + pass + + try: + python_version = decode( + subprocess.check_output( + list_to_shell_command( + [ + executable, + "-c", + "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"", + ] + ), + shell=True, + ) + ) + except CalledProcessError as e: + raise EnvCommandError(e) + + python_version = Version.parse(python_version.strip()) + + return executable, python_version + class Env(object): """ diff --git a/tests/bundle/__init__.py b/tests/bundle/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/bundle/fixtures/simple_project/README.rst b/tests/bundle/fixtures/simple_project/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/bundle/fixtures/simple_project/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/bundle/fixtures/simple_project/poetry.lock b/tests/bundle/fixtures/simple_project/poetry.lock new file mode 100644 index 00000000000..3b8c78ddcfc --- /dev/null +++ b/tests/bundle/fixtures/simple_project/poetry.lock @@ -0,0 +1,14 @@ +[[package]] +category = "main" +description = "" +name = "foo" +optional = false +python-versions = "*" +version = "1.0.0" + +[metadata] +content-hash = "e97e6b0e10d38d2bbc2549ae152a31c81e1d2ed5e65a2cdf395d4ade26e9bf41" +python-versions = "~2.7 || ^3.4" + +[metadata.files] +foo = [] diff --git a/tests/bundle/fixtures/simple_project/pyproject.toml b/tests/bundle/fixtures/simple_project/pyproject.toml new file mode 100644 index 00000000000..a19e58577bf --- /dev/null +++ b/tests/bundle/fixtures/simple_project/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "simple-project" +version = "1.2.3" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = "README.rst" + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" +foo = "^1.0.0" + +[tool.poetry.scripts] +foo = "foo:bar" +baz = "bar:baz.boom.bim" diff --git a/tests/bundle/fixtures/simple_project/simple_project/__init__.py b/tests/bundle/fixtures/simple_project/simple_project/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/bundle/fixtures/simple_project_with_no_module/README.rst b/tests/bundle/fixtures/simple_project_with_no_module/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/bundle/fixtures/simple_project_with_no_module/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/bundle/fixtures/simple_project_with_no_module/poetry.lock b/tests/bundle/fixtures/simple_project_with_no_module/poetry.lock new file mode 100644 index 00000000000..3b8c78ddcfc --- /dev/null +++ b/tests/bundle/fixtures/simple_project_with_no_module/poetry.lock @@ -0,0 +1,14 @@ +[[package]] +category = "main" +description = "" +name = "foo" +optional = false +python-versions = "*" +version = "1.0.0" + +[metadata] +content-hash = "e97e6b0e10d38d2bbc2549ae152a31c81e1d2ed5e65a2cdf395d4ade26e9bf41" +python-versions = "~2.7 || ^3.4" + +[metadata.files] +foo = [] diff --git a/tests/bundle/fixtures/simple_project_with_no_module/pyproject.toml b/tests/bundle/fixtures/simple_project_with_no_module/pyproject.toml new file mode 100644 index 00000000000..a19e58577bf --- /dev/null +++ b/tests/bundle/fixtures/simple_project_with_no_module/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "simple-project" +version = "1.2.3" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = "README.rst" + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "~2.7 || ^3.4" +foo = "^1.0.0" + +[tool.poetry.scripts] +foo = "foo:bar" +baz = "bar:baz.boom.bim" diff --git a/tests/bundle/test_bundler_manager.py b/tests/bundle/test_bundler_manager.py new file mode 100644 index 00000000000..b6a72e7b705 --- /dev/null +++ b/tests/bundle/test_bundler_manager.py @@ -0,0 +1,51 @@ +import pytest + +from poetry.bundle.bundler import Bundler +from poetry.bundle.bundler_manager import BundlerManager +from poetry.bundle.exceptions import BundlerManagerError + + +class MockBundler(Bundler): + @property + def name(self): # type: () -> str + return "mock" + + +def test_manager_has_default_bundlers(): + manager = BundlerManager() + + assert len(manager.bundlers) > 0 + + +def test_bundler_returns_the_correct_bundler(): + manager = BundlerManager() + + bundler = manager.bundler("venv") + assert isinstance(bundler, Bundler) + assert "venv" == bundler.name + + +def test_bundler_raises_an_error_for_incorrect_bundlers(): + manager = BundlerManager() + + with pytest.raises(BundlerManagerError, match='The bundler "mock" does not exist.'): + manager.bundler("mock") + + +def test_register_bundler_registers_new_bundlers(): + manager = BundlerManager() + manager.register_bundler(MockBundler()) + + bundler = manager.bundler("mock") + assert isinstance(bundler, Bundler) + assert "mock" == bundler.name + + +def test_register_bundler_cannot_register_existing_bundlers(): + manager = BundlerManager() + manager.register_bundler(MockBundler()) + + with pytest.raises( + BundlerManagerError, match='A bundler with the name "mock" already exists.' + ): + manager.register_bundler(MockBundler()) diff --git a/tests/bundle/test_venv_bundler.py b/tests/bundle/test_venv_bundler.py new file mode 100644 index 00000000000..3480bf71cd5 --- /dev/null +++ b/tests/bundle/test_venv_bundler.py @@ -0,0 +1,208 @@ +import shutil +import sys + +from pathlib import Path + +import pytest + +from cleo.formatters.style import Style +from cleo.io.buffered_io import BufferedIO + +from poetry.bundle.venv_bundler import VenvBundler +from poetry.core.packages.package import Package +from poetry.factory import Factory +from poetry.repositories.pool import Pool +from poetry.repositories.repository import Repository + + +@pytest.fixture() +def io(): + io = BufferedIO() + + io.output.formatter.set_style("success", Style("green", options=["dark"])) + io.output.formatter.set_style("warning", Style("yellow", options=["dark"])) + + return io + + +@pytest.fixture() +def poetry(config): + poetry = Factory().create_poetry( + Path(__file__).parent / "fixtures" / "simple_project" + ) + poetry.set_config(config) + + pool = Pool() + repository = Repository() + repository.add_package(Package("foo", "1.0.0")) + pool.add_repository(repository) + poetry.set_pool(pool) + + return poetry + + +def test_bundler_should_build_a_new_venv_with_existing_python( + io, tmp_dir, poetry, mocker +): + shutil.rmtree(tmp_dir) + mocker.patch("poetry.installation.executor.Executor._execute_operation") + + bundler = VenvBundler() + + assert bundler.bundle(poetry, io, Path(tmp_dir)) + + expected = """\ + • Bundling simple-project (1.2.3) into {path} + • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version} + • Bundling simple-project (1.2.3) into {path}: Installing dependencies + • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) + • Bundled simple-project (1.2.3) into {path} +""".format( + path=tmp_dir, python_version=".".join(str(v) for v in sys.version_info[:3]) + ) + assert expected == io.fetch_output() + + +def test_bundler_should_build_a_new_venv_with_given_executable( + io, tmp_dir, poetry, mocker +): + shutil.rmtree(tmp_dir) + mocker.patch("poetry.installation.executor.Executor._execute_operation") + + bundler = VenvBundler() + + assert bundler.bundle(poetry, io, Path(tmp_dir), executable=sys.executable) + + expected = """\ + • Bundling simple-project (1.2.3) into {path} + • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version} + • Bundling simple-project (1.2.3) into {path}: Installing dependencies + • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) + • Bundled simple-project (1.2.3) into {path} +""".format( + path=tmp_dir, python_version=".".join(str(v) for v in sys.version_info[:3]) + ) + assert expected == io.fetch_output() + + +def test_bundler_should_build_a_new_venv_if_existing_venv_is_incompatible( + io, tmp_dir, poetry, mocker +): + mocker.patch("poetry.installation.executor.Executor._execute_operation") + + bundler = VenvBundler() + + assert bundler.bundle(poetry, io, Path(tmp_dir)) + + expected = """\ + • Bundling simple-project (1.2.3) into {path} + • Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment + • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version} + • Bundling simple-project (1.2.3) into {path}: Installing dependencies + • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) + • Bundled simple-project (1.2.3) into {path} +""".format( + path=tmp_dir, python_version=".".join(str(v) for v in sys.version_info[:3]) + ) + assert expected == io.fetch_output() + + +def test_bundler_should_use_an_existing_venv_if_compatible( + io, tmp_venv, poetry, mocker +): + mocker.patch("poetry.installation.executor.Executor._execute_operation") + + bundler = VenvBundler() + + assert bundler.bundle(poetry, io, tmp_venv.path) + + expected = """\ + • Bundling simple-project (1.2.3) into {path} + • Bundling simple-project (1.2.3) into {path}: Using existing virtual environment + • Bundling simple-project (1.2.3) into {path}: Installing dependencies + • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) + • Bundled simple-project (1.2.3) into {path} +""".format( + path=str(tmp_venv.path), + python_version=".".join(str(v) for v in sys.version_info[:3]), + ) + assert expected == io.fetch_output() + + +def test_bundler_should_remove_an_existing_venv_if_forced(io, tmp_venv, poetry, mocker): + mocker.patch("poetry.installation.executor.Executor._execute_operation") + + bundler = VenvBundler() + + assert bundler.bundle(poetry, io, tmp_venv.path, remove=True) + + expected = """\ + • Bundling simple-project (1.2.3) into {path} + • Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment + • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version} + • Bundling simple-project (1.2.3) into {path}: Installing dependencies + • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) + • Bundled simple-project (1.2.3) into {path} +""".format( + path=str(tmp_venv.path), + python_version=".".join(str(v) for v in sys.version_info[:3]), + ) + assert expected == io.fetch_output() + + +def test_bundler_should_fail_when_installation_fails(io, tmp_dir, poetry, mocker): + mocker.patch( + "poetry.installation.executor.Executor._do_execute_operation", + side_effect=Exception(), + ) + + bundler = VenvBundler() + + assert not bundler.bundle(poetry, io, Path(tmp_dir)) + + expected = """\ + • Bundling simple-project (1.2.3) into {path} + • Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment + • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version} + • Bundling simple-project (1.2.3) into {path}: Installing dependencies + • Bundling simple-project (1.2.3) into {path}: Failed at step Installing dependencies +""".format( + path=tmp_dir, + python_version=".".join(str(v) for v in sys.version_info[:3]), + ) + assert expected == io.fetch_output() + + +def test_bundler_should_display_a_warning_for_projects_with_no_module( + io, tmp_venv, mocker, config +): + poetry = Factory().create_poetry( + Path(__file__).parent / "fixtures" / "simple_project_with_no_module" + ) + poetry.set_config(config) + + pool = Pool() + repository = Repository() + repository.add_package(Package("foo", "1.0.0")) + pool.add_repository(repository) + poetry.set_pool(pool) + + mocker.patch("poetry.installation.executor.Executor._execute_operation") + + bundler = VenvBundler() + + assert bundler.bundle(poetry, io, tmp_venv.path, remove=True) + + expected = """\ + • Bundling simple-project (1.2.3) into {path} + • Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment + • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version} + • Bundling simple-project (1.2.3) into {path}: Installing dependencies + • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) + • Bundled simple-project (1.2.3) into {path} + • The root package was not installed because no matching module or package was found. +""".format( + path=str(tmp_venv.path), + python_version=".".join(str(v) for v in sys.version_info[:3]), + ) + assert expected == io.fetch_output() diff --git a/tests/console/commands/bundle/__init__.py b/tests/console/commands/bundle/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/console/commands/bundle/test_venv.py b/tests/console/commands/bundle/test_venv.py new file mode 100644 index 00000000000..eecfe678622 --- /dev/null +++ b/tests/console/commands/bundle/test_venv.py @@ -0,0 +1,28 @@ +from pathlib import Path + + +def test_venv_calls_venv_bundler(app_tester, mocker): + mock = mocker.patch( + "poetry.bundle.venv_bundler.VenvBundler.bundle", side_effect=[True, False] + ) + + app_tester.application.catch_exceptions(False) + assert 0 == app_tester.execute("bundle venv /foo") + assert 1 == app_tester.execute("bundle venv /foo --python python3.8 --clear") + + assert [ + mocker.call( + app_tester.application.poetry, + mocker.ANY, + Path("/foo"), + executable=None, + remove=False, + ), + mocker.call( + app_tester.application.poetry, + mocker.ANY, + Path("/foo"), + executable="python3.8", + remove=True, + ), + ] == mock.call_args_list From 32ed49df5d2ab65fe8b72bc76ce4d2fc5f639fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sat, 13 Feb 2021 13:47:20 +0100 Subject: [PATCH 2/2] Upgrade Cleo --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index c576b9e1eff..add15380747 100644 --- a/poetry.lock +++ b/poetry.lock @@ -95,7 +95,7 @@ python-versions = "*" [[package]] name = "cleo" -version = "1.0.0a1" +version = "1.0.0a2" description = "Cleo allows you to create beautiful and testable command-line interfaces." category = "main" optional = false @@ -668,7 +668,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "3061627bdc17958f2b0d1dfc86e448e167ff3e97639e6f3c8f67bf0b06d9995c" +content-hash = "1f73c9ec5eb9eab75a5d4008f44e7d7bf7f31f5ac395ebb4c59518e3033aa083" [metadata.files] appdirs = [ @@ -742,8 +742,8 @@ chardet = [ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] cleo = [ - {file = "cleo-1.0.0a1-py3-none-any.whl", hash = "sha256:e4a45adc6b56a04d350e7b4893352fdcc07d89d35991e5df16753e05a7c78c2b"}, - {file = "cleo-1.0.0a1.tar.gz", hash = "sha256:45bc5f04278c2f183c7ab77b3ec20f5204711fecb37ae688424c39ea8badf3fe"}, + {file = "cleo-1.0.0a2-py3-none-any.whl", hash = "sha256:fa4c9ed94deba0f286bb9f2efbd3d6b4d0237b99b93a7dae6b55d473e1d8339c"}, + {file = "cleo-1.0.0a2.tar.gz", hash = "sha256:1028cc0585e510e74ceaa73f4b7da8586911b7cc3e4aba03f0540c7d9415701b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, diff --git a/pyproject.toml b/pyproject.toml index 716fd2f27aa..f44a85b446e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ python = "^3.6" poetry-core = "^1.0.2" -cleo = "^1.0.0a1" +cleo = "^1.0.0a2" crashtest = "^0.3.0" requests = "^2.18" cachy = "^0.3.0"