├── tests ├── __init__.py ├── utils │ ├── __init__.py │ └── test_platforms.py ├── bundlers │ ├── __init__.py │ ├── test_bundler_manager.py │ └── test_venv_bundler.py ├── console │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ └── bundle │ │ │ ├── __init__.py │ │ │ └── test_venv.py │ └── conftest.py ├── fixtures │ ├── project_with_binary_wheel │ │ ├── README.md │ │ ├── project_with_binary_wheel │ │ │ └── __init__.py │ │ ├── pyproject.toml │ │ └── poetry.lock │ ├── simple_project │ │ ├── simple_project │ │ │ └── __init__.py │ │ ├── README.rst │ │ ├── poetry.lock │ │ └── pyproject.toml │ ├── simple_project_with_editable_dep │ │ ├── bar │ │ │ ├── bar │ │ │ │ └── __init__.py │ │ │ └── pyproject.toml │ │ ├── simple_project │ │ │ └── __init__.py │ │ ├── README.rst │ │ ├── poetry.lock │ │ └── pyproject.toml │ ├── simple_project_with_dev_dep │ │ ├── simple_project │ │ │ └── __init__.py │ │ ├── README.rst │ │ ├── poetry.lock │ │ └── pyproject.toml │ ├── simple_project_with_no_module │ │ ├── README.rst │ │ ├── poetry.lock │ │ └── pyproject.toml │ └── non_package_mode │ │ ├── poetry.lock │ │ └── pyproject.toml ├── helpers.py └── conftest.py ├── src └── poetry_plugin_bundle │ ├── __init__.py │ ├── utils │ ├── __init__.py │ └── platforms.py │ ├── bundlers │ ├── __init__.py │ ├── bundler.py │ ├── bundler_manager.py │ └── venv_bundler.py │ ├── console │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── bundle │ │ ├── __init__.py │ │ ├── bundle_command.py │ │ └── venv.py │ ├── exceptions.py │ └── plugin.py ├── .gitignore ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── release.yaml │ └── main.yaml ├── pyproject.toml ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/bundlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/console/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/console/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/console/commands/bundle/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/bundlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/console/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/console/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_binary_wheel/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project/simple_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/console/commands/bundle/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_editable_dep/bar/bar/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project/README.rst: -------------------------------------------------------------------------------- 1 | My Package 2 | ========== 3 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_dev_dep/simple_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_binary_wheel/project_with_binary_wheel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_editable_dep/simple_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_dev_dep/README.rst: -------------------------------------------------------------------------------- 1 | My Package 2 | ========== 3 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_editable_dep/README.rst: -------------------------------------------------------------------------------- 1 | My Package 2 | ========== 3 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_no_module/README.rst: -------------------------------------------------------------------------------- 1 | My Package 2 | ========== 3 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class BundlerManagerError(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/fixtures/non_package_mode/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | package = [] 3 | 4 | [metadata] 5 | lock-version = "2.0" 6 | python-versions = "~2.7 || ^3.4" 7 | content-hash = "ea8246031ba3d40ce95b77e8f186836ce4a26fbba7b2be93f6a2f70047c04b66" 8 | 9 | 10 | [metadata.files] 11 | foo = [] 12 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/bundlers/bundler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | 6 | if TYPE_CHECKING: 7 | from cleo.io.io import IO 8 | from poetry.poetry import Poetry 9 | 10 | 11 | class Bundler: 12 | name: str 13 | 14 | def bundle(self, poetry: Poetry, io: IO) -> bool: 15 | raise NotImplementedError() 16 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "" 4 | name = "foo" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.0.0" 8 | 9 | [metadata] 10 | lock-version = "1.0" 11 | content-hash = "940146b262cc643ca9ab2e882e530cc737484f7a10fa7623439fa401c1b3af72" 12 | python-versions = "~2.7 || ^3.4" 13 | 14 | [metadata.files] 15 | foo = [] 16 | -------------------------------------------------------------------------------- /tests/fixtures/non_package_mode/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "simple-project-non-package-mode" 3 | version = "1.2.3" 4 | package-mode = false 5 | 6 | # Requirements 7 | [tool.poetry.dependencies] 8 | python = "~2.7 || ^3.4" 9 | 10 | 11 | [tool.poetry.group.dev] 12 | optional = true 13 | dependencies = {} 14 | 15 | [tool.poetry.scripts] 16 | foo = "foo:bar" 17 | baz = "bar:baz.boom.bim" 18 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_dev_dep/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "" 4 | name = "foo" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.0.0" 8 | 9 | [metadata] 10 | lock-version = "1.0" 11 | content-hash = "84d6d2a62597932428c63eb32553bbd5b803651a1878365bd33d0df15f741c44" 12 | python-versions = "~2.7 || ^3.4" 13 | 14 | [metadata.files] 15 | foo = [] 16 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_no_module/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "" 4 | name = "foo" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.0.0" 8 | 9 | [metadata] 10 | lock-version = "1.0" 11 | content-hash = "e97e6b0e10d38d2bbc2549ae152a31c81e1d2ed5e65a2cdf395d4ade26e9bf41" 12 | python-versions = "~2.7 || ^3.4" 13 | 14 | [metadata.files] 15 | foo = [] 16 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_binary_wheel/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "project-with-binary-wheel" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["BrandonLWhite "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | cryptography = "^43.0.1" 11 | 12 | 13 | [build-system] 14 | requires = ["poetry-core"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_editable_dep/poetry.lock: -------------------------------------------------------------------------------- 1 | [metadata] 2 | lock-version = "1.0" 3 | content-hash = "4bf28231e477206338f6afc67df322e7b3601fdbf1d704521e1676a33603768b" 4 | python-versions = "~2.7 || ^3.4" 5 | 6 | [[package]] 7 | name = "bar" 8 | version = "0.1.0" 9 | description = "" 10 | optional = false 11 | python-versions = "*" 12 | files = [] 13 | develop = true 14 | 15 | [package.source] 16 | type = "directory" 17 | url = "bar" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | # Packages 4 | *.egg 5 | !/tests/**/*.egg 6 | /*.egg-info 7 | /dist/* 8 | build 9 | _build 10 | .cache 11 | *.so 12 | 13 | # Installer logs 14 | pip-log.txt 15 | 16 | # Unit test / coverage reports 17 | .coverage 18 | .tox 19 | .pytest_cache 20 | 21 | .DS_Store 22 | .idea/* 23 | .python-version 24 | .vscode/* 25 | 26 | /test.py 27 | /test_*.* 28 | 29 | /setup.cfg 30 | MANIFEST.in 31 | /setup.py 32 | /docs/site/* 33 | /tests/fixtures/simple_project/setup.py 34 | .mypy_cache 35 | 36 | .venv 37 | /releases/* 38 | pip-wheel-metadata 39 | /poetry.toml 40 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_editable_dep/bar/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "bar" 3 | version = "1.2.3" 4 | description = "Some description." 5 | authors = [ 6 | "Sébastien Eustace " 7 | ] 8 | license = "MIT" 9 | 10 | readme = "README.rst" 11 | 12 | homepage = "https://python-poetry.org" 13 | repository = "https://github.com/python-poetry/poetry" 14 | documentation = "https://python-poetry.org/docs" 15 | 16 | keywords = ["packaging", "dependency", "poetry"] 17 | 18 | classifiers = [ 19 | "Topic :: Software Development :: Build Tools", 20 | "Topic :: Software Development :: Libraries :: Python Modules" 21 | ] 22 | 23 | # Requirements 24 | [tool.poetry.dependencies] 25 | python = "~2.7 || ^3.4" 26 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_no_module/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "simple-project" 3 | version = "1.2.3" 4 | description = "Some description." 5 | authors = [ 6 | "Sébastien Eustace " 7 | ] 8 | license = "MIT" 9 | 10 | readme = "README.rst" 11 | 12 | homepage = "https://python-poetry.org" 13 | repository = "https://github.com/python-poetry/poetry" 14 | documentation = "https://python-poetry.org/docs" 15 | 16 | keywords = ["packaging", "dependency", "poetry"] 17 | 18 | classifiers = [ 19 | "Topic :: Software Development :: Build Tools", 20 | "Topic :: Software Development :: Libraries :: Python Modules" 21 | ] 22 | 23 | # Requirements 24 | [tool.poetry.dependencies] 25 | python = "~2.7 || ^3.4" 26 | foo = "^1.0.0" 27 | 28 | [tool.poetry.scripts] 29 | foo = "foo:bar" 30 | baz = "bar:baz.boom.bim" 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v6.0.0 7 | hooks: 8 | - id: trailing-whitespace 9 | - id: end-of-file-fixer 10 | - id: check-merge-conflict 11 | - id: check-case-conflict 12 | - id: check-json 13 | - id: check-toml 14 | - id: check-yaml 15 | - id: pretty-format-json 16 | args: [--autofix, --no-ensure-ascii, --no-sort-keys] 17 | - id: check-ast 18 | - id: debug-statements 19 | - id: check-docstring-first 20 | 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: v0.14.9 23 | hooks: 24 | - id: ruff-check 25 | - id: ruff-format 26 | 27 | - repo: https://github.com/woodruffw/zizmor-pre-commit 28 | rev: v1.18.0 29 | hooks: 30 | - id: zizmor 31 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_editable_dep/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "simple-project" 3 | version = "1.2.3" 4 | description = "Some description." 5 | authors = [ 6 | "Sébastien Eustace " 7 | ] 8 | license = "MIT" 9 | 10 | readme = "README.rst" 11 | 12 | homepage = "https://python-poetry.org" 13 | repository = "https://github.com/python-poetry/poetry" 14 | documentation = "https://python-poetry.org/docs" 15 | 16 | keywords = ["packaging", "dependency", "poetry"] 17 | 18 | classifiers = [ 19 | "Topic :: Software Development :: Build Tools", 20 | "Topic :: Software Development :: Libraries :: Python Modules" 21 | ] 22 | 23 | # Requirements 24 | [tool.poetry.dependencies] 25 | python = "~2.7 || ^3.4" 26 | bar = { path = "bar", develop = true } 27 | 28 | [tool.poetry.scripts] 29 | foo = "foo:bar" 30 | baz = "bar:baz.boom.bim" 31 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project_with_dev_dep/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "simple-project" 3 | version = "1.2.3" 4 | description = "Some description." 5 | authors = [ 6 | "Sébastien Eustace " 7 | ] 8 | license = "MIT" 9 | 10 | readme = "README.rst" 11 | 12 | homepage = "https://python-poetry.org" 13 | repository = "https://github.com/python-poetry/poetry" 14 | documentation = "https://python-poetry.org/docs" 15 | 16 | keywords = ["packaging", "dependency", "poetry"] 17 | 18 | classifiers = [ 19 | "Topic :: Software Development :: Build Tools", 20 | "Topic :: Software Development :: Libraries :: Python Modules" 21 | ] 22 | 23 | # Requirements 24 | [tool.poetry.dependencies] 25 | python = "~2.7 || ^3.4" 26 | foo = "^1.0.0" 27 | 28 | [tool.poetry.dev-dependencies] 29 | bar = "^1.0.0" 30 | 31 | [tool.poetry.scripts] 32 | foo = "foo:bar" 33 | baz = "bar:baz.boom.bim" 34 | -------------------------------------------------------------------------------- /tests/fixtures/simple_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "simple-project" 3 | version = "1.2.3" 4 | description = "Some description." 5 | authors = [ 6 | "Sébastien Eustace " 7 | ] 8 | license = "MIT" 9 | 10 | readme = "README.rst" 11 | 12 | homepage = "https://python-poetry.org" 13 | repository = "https://github.com/python-poetry/poetry" 14 | documentation = "https://python-poetry.org/docs" 15 | 16 | keywords = ["packaging", "dependency", "poetry"] 17 | 18 | classifiers = [ 19 | "Topic :: Software Development :: Build Tools", 20 | "Topic :: Software Development :: Libraries :: Python Modules" 21 | ] 22 | 23 | # Requirements 24 | [tool.poetry.dependencies] 25 | python = "~2.7 || ^3.4" 26 | foo = "^1.0.0" 27 | 28 | [tool.poetry.group.dev] 29 | optional = true 30 | dependencies = {} 31 | 32 | [tool.poetry.scripts] 33 | foo = "foo:bar" 34 | baz = "bar:baz.boom.bim" 35 | -------------------------------------------------------------------------------- /tests/console/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import TYPE_CHECKING 5 | 6 | import pytest 7 | 8 | from cleo.testers.application_tester import ApplicationTester 9 | from poetry.factory import Factory 10 | 11 | from tests.helpers import TestApplication 12 | from tests.helpers import TestLocker 13 | 14 | 15 | if TYPE_CHECKING: 16 | from poetry.config.config import Config 17 | from poetry.poetry import Poetry 18 | 19 | 20 | @pytest.fixture 21 | def project_directory() -> str: 22 | return "simple_project" 23 | 24 | 25 | @pytest.fixture 26 | def poetry(project_directory: str, config: Config) -> Poetry: 27 | p = Factory().create_poetry( 28 | Path(__file__).parent.parent / "fixtures" / project_directory 29 | ) 30 | p.set_locker(TestLocker(p.locker.lock, p.locker._pyproject_data)) 31 | 32 | return p 33 | 34 | 35 | @pytest.fixture 36 | def app(poetry: Poetry) -> TestApplication: 37 | app_ = TestApplication(poetry) 38 | 39 | return app_ 40 | 41 | 42 | @pytest.fixture 43 | def app_tester(app: TestApplication) -> ApplicationTester: 44 | return ApplicationTester(app) 45 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/bundlers/bundler_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from poetry_plugin_bundle.exceptions import BundlerManagerError 6 | 7 | 8 | if TYPE_CHECKING: 9 | from poetry_plugin_bundle.bundlers.bundler import Bundler 10 | 11 | 12 | class BundlerManager: 13 | def __init__(self) -> None: 14 | from poetry_plugin_bundle.bundlers.venv_bundler import VenvBundler 15 | 16 | self._bundler_classes: dict[str, type[Bundler]] = {} 17 | 18 | # Register default bundlers 19 | self.register_bundler_class(VenvBundler) 20 | 21 | def bundler(self, name: str) -> Bundler: 22 | if name.lower() not in self._bundler_classes: 23 | raise BundlerManagerError(f'The bundler class "{name}" does not exist.') 24 | 25 | return self._bundler_classes[name.lower()]() 26 | 27 | def register_bundler_class(self, bundler_class: type[Bundler]) -> BundlerManager: 28 | if not bundler_class.name: 29 | raise BundlerManagerError("A bundler class must have a name") 30 | 31 | if bundler_class.name.lower() in self._bundler_classes: 32 | raise BundlerManagerError( 33 | f'A bundler class with the name "{bundler_class.name}" already exists.' 34 | ) 35 | 36 | self._bundler_classes[bundler_class.name.lower()] = bundler_class 37 | 38 | return self 39 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/console/commands/bundle/bundle_command.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from poetry.console.commands.group_command import GroupCommand 6 | 7 | 8 | if TYPE_CHECKING: 9 | from poetry_plugin_bundle.bundlers.bundler import Bundler 10 | from poetry_plugin_bundle.bundlers.bundler_manager import BundlerManager 11 | 12 | 13 | class BundleCommand(GroupCommand): 14 | """ 15 | Base class for all bundle commands. 16 | """ 17 | 18 | bundler_name: str 19 | 20 | def __init__(self) -> None: 21 | self._bundler_manager: BundlerManager | None = None 22 | 23 | super().__init__() 24 | 25 | @property 26 | def bundler_manager(self) -> BundlerManager | None: 27 | return self._bundler_manager 28 | 29 | def set_bundler_manager(self, bundler_manager: BundlerManager) -> None: 30 | self._bundler_manager = bundler_manager 31 | 32 | def configure_bundler(self, bundler: Bundler) -> None: 33 | """ 34 | Configure the given bundler based on command specific options and arguments. 35 | """ 36 | 37 | def handle(self) -> int: 38 | self.line("") 39 | 40 | assert self._bundler_manager is not None 41 | bundler = self._bundler_manager.bundler(self.bundler_name) 42 | 43 | self.configure_bundler(bundler) 44 | 45 | return int(not bundler.bundle(self.poetry, self._io)) 46 | -------------------------------------------------------------------------------- /tests/bundlers/test_bundler_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | 5 | import pytest 6 | 7 | from poetry_plugin_bundle.bundlers.bundler import Bundler 8 | from poetry_plugin_bundle.bundlers.bundler_manager import BundlerManager 9 | from poetry_plugin_bundle.exceptions import BundlerManagerError 10 | 11 | 12 | class MockBundler(Bundler): 13 | name = "mock" 14 | 15 | 16 | def test_bundler_returns_an_instance_of_the_correct_bundler_class() -> None: 17 | manager = BundlerManager() 18 | 19 | bundler = manager.bundler("venv") 20 | assert isinstance(bundler, Bundler) 21 | assert bundler.name == "venv" 22 | 23 | 24 | def test_bundler_raises_an_error_for_incorrect_bundler_classes() -> None: 25 | manager = BundlerManager() 26 | 27 | with pytest.raises( 28 | BundlerManagerError, match=re.escape('The bundler class "mock" does not exist.') 29 | ): 30 | manager.bundler("mock") 31 | 32 | 33 | def test_register_bundler_class_registers_new_bundler_classes() -> None: 34 | manager = BundlerManager() 35 | manager.register_bundler_class(MockBundler) 36 | 37 | bundler = manager.bundler("mock") 38 | assert isinstance(bundler, Bundler) 39 | assert bundler.name == "mock" 40 | 41 | 42 | def test_register_bundler_class_cannot_register_existing_bundler_classes() -> None: 43 | manager = BundlerManager() 44 | manager.register_bundler_class(MockBundler) 45 | 46 | with pytest.raises( 47 | BundlerManagerError, 48 | match=re.escape('A bundler class with the name "mock" already exists.'), 49 | ): 50 | manager.register_bundler_class(MockBundler) 51 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | from typing import Any 5 | from typing import cast 6 | 7 | from cleo.events.console_events import COMMAND 8 | from poetry.plugins.application_plugin import ApplicationPlugin 9 | 10 | from poetry_plugin_bundle.console.commands.bundle.venv import BundleVenvCommand 11 | 12 | 13 | if TYPE_CHECKING: 14 | from cleo.events.console_command_event import ConsoleCommandEvent 15 | from poetry.console.application import Application 16 | from poetry.console.commands.command import Command 17 | 18 | 19 | class BundleApplicationPlugin(ApplicationPlugin): 20 | @property 21 | def commands(self) -> list[type[Command]]: 22 | return [BundleVenvCommand] 23 | 24 | def activate(self, application: Application) -> None: 25 | assert application.event_dispatcher 26 | application.event_dispatcher.add_listener( 27 | COMMAND, 28 | self.configure_bundle_commands, # type: ignore[arg-type] 29 | ) 30 | super().activate(application=application) 31 | 32 | def configure_bundle_commands( 33 | self, event: ConsoleCommandEvent, event_name: str, _: Any 34 | ) -> None: 35 | from poetry_plugin_bundle.console.commands.bundle.bundle_command import ( 36 | BundleCommand, 37 | ) 38 | 39 | command: BundleCommand = cast("BundleCommand", event.command) 40 | if not isinstance(command, BundleCommand): 41 | return 42 | 43 | # If the command already has a bundler manager, do nothing 44 | if command.bundler_manager is not None: 45 | return 46 | 47 | from poetry_plugin_bundle.bundlers.bundler_manager import BundlerManager 48 | 49 | command.set_bundler_manager(BundlerManager()) 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 15 | with: 16 | persist-credentials: false 17 | 18 | - run: pipx run build 19 | 20 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 21 | with: 22 | name: distfiles 23 | path: dist/ 24 | if-no-files-found: error 25 | 26 | upload-github: 27 | name: Upload (GitHub) 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | needs: build 32 | steps: 33 | # We need to be in a git repo for gh to work. 34 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 35 | with: 36 | persist-credentials: false 37 | 38 | - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 39 | with: 40 | name: distfiles 41 | path: dist/ 42 | 43 | - run: gh release upload "${TAG_NAME}" dist/*.{tar.gz,whl} 44 | env: 45 | GH_TOKEN: ${{ github.token }} 46 | TAG_NAME: ${{ github.event.release.tag_name }} 47 | 48 | upload-pypi: 49 | name: Upload (PyPI) 50 | runs-on: ubuntu-latest 51 | environment: 52 | name: pypi 53 | url: https://pypi.org/project/poetry-plugin-bundle/ 54 | permissions: 55 | id-token: write 56 | needs: build 57 | steps: 58 | - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 59 | with: 60 | name: distfiles 61 | path: dist/ 62 | 63 | - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 64 | with: 65 | print-hash: true 66 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | from typing import Any 5 | 6 | from poetry.console.application import Application 7 | from poetry.factory import Factory 8 | from poetry.packages import Locker 9 | 10 | 11 | if TYPE_CHECKING: 12 | from pathlib import Path 13 | 14 | from poetry.poetry import Poetry 15 | from tomlkit.toml_document import TOMLDocument 16 | 17 | 18 | class TestApplication(Application): 19 | def __init__(self, poetry: Poetry) -> None: 20 | super().__init__() 21 | self._poetry = poetry 22 | 23 | def reset_poetry(self) -> None: 24 | poetry = self._poetry 25 | assert poetry is not None 26 | self._poetry = Factory().create_poetry(poetry.file.path.parent) 27 | self._poetry.set_pool(poetry.pool) 28 | self._poetry.set_config(poetry.config) 29 | self._poetry.set_locker( 30 | TestLocker(poetry.locker.lock, self._poetry.local_config) 31 | ) 32 | 33 | 34 | class TestLocker(Locker): 35 | def __init__(self, lock: Path, local_config: dict[str, Any]) -> None: 36 | super().__init__(lock, local_config) 37 | self._locked = False 38 | self._write = False 39 | 40 | def write(self, write: bool = True) -> None: 41 | self._write = write 42 | 43 | def is_locked(self) -> bool: 44 | return self._locked 45 | 46 | def locked(self, is_locked: bool = True) -> TestLocker: 47 | self._locked = is_locked 48 | 49 | return self 50 | 51 | def mock_lock_data(self, data: TOMLDocument) -> None: 52 | self.locked() 53 | 54 | self._lock_data = data 55 | 56 | def is_fresh(self) -> bool: 57 | return True 58 | 59 | def _write_lock_data(self, data: TOMLDocument) -> None: 60 | if self._write: 61 | super()._write_lock_data(data) 62 | self._locked = True 63 | return 64 | 65 | self._lock_data = data 66 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import shutil 4 | 5 | from typing import TYPE_CHECKING 6 | from typing import Iterator 7 | 8 | import pytest 9 | 10 | from poetry.config.config import Config 11 | from poetry.config.dict_config_source import DictConfigSource 12 | from poetry.utils.env import EnvManager 13 | from poetry.utils.env import VirtualEnv 14 | 15 | 16 | if TYPE_CHECKING: 17 | from pathlib import Path 18 | 19 | from pytest_mock import MockerFixture 20 | 21 | 22 | @pytest.fixture 23 | def config_cache_dir(tmp_path: Path) -> Path: 24 | path = tmp_path / ".cache" / "pypoetry" 25 | path.mkdir(parents=True) 26 | return path 27 | 28 | 29 | @pytest.fixture 30 | def config_virtualenvs_path(config_cache_dir: Path) -> Path: 31 | return config_cache_dir / "virtualenvs" 32 | 33 | 34 | @pytest.fixture 35 | def config_source(config_cache_dir: Path) -> DictConfigSource: 36 | source = DictConfigSource() 37 | source.add_property("cache-dir", str(config_cache_dir)) 38 | 39 | return source 40 | 41 | 42 | @pytest.fixture 43 | def auth_config_source() -> DictConfigSource: 44 | source = DictConfigSource() 45 | 46 | return source 47 | 48 | 49 | @pytest.fixture 50 | def config( 51 | config_source: DictConfigSource, 52 | auth_config_source: DictConfigSource, 53 | mocker: MockerFixture, 54 | ) -> Config: 55 | import keyring 56 | 57 | from keyring.backends.fail import Keyring 58 | 59 | keyring.set_keyring(Keyring()) # type: ignore[no-untyped-call] 60 | 61 | c = Config() 62 | c.merge(config_source.config) 63 | c.set_config_source(config_source) 64 | c.set_auth_config_source(auth_config_source) 65 | 66 | mocker.patch("poetry.config.config.Config.create", return_value=c) 67 | mocker.patch("poetry.config.config.Config.set_config_source") 68 | 69 | return c 70 | 71 | 72 | @pytest.fixture 73 | def tmp_venv(tmp_path: Path) -> Iterator[VirtualEnv]: 74 | venv_path = tmp_path / "venv" 75 | 76 | EnvManager.build_venv(venv_path) 77 | 78 | venv = VirtualEnv(venv_path) 79 | yield venv 80 | 81 | shutil.rmtree(str(venv.path)) 82 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/console/commands/bundle/venv.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import TYPE_CHECKING 5 | 6 | from cleo.helpers import argument 7 | from cleo.helpers import option 8 | 9 | from poetry_plugin_bundle.console.commands.bundle.bundle_command import BundleCommand 10 | 11 | 12 | if TYPE_CHECKING: 13 | from poetry_plugin_bundle.bundlers.venv_bundler import VenvBundler 14 | 15 | 16 | class BundleVenvCommand(BundleCommand): 17 | name = "bundle venv" 18 | description = "Bundle the current project into a virtual environment" 19 | 20 | arguments = [ # noqa: RUF012 21 | argument("path", "The path to the virtual environment to bundle into.") 22 | ] 23 | 24 | options = [ # noqa: RUF012 25 | *BundleCommand._group_dependency_options(), 26 | option( 27 | "python", 28 | "p", 29 | "The Python executable to use to create the virtual environment. " 30 | "Defaults to the current Python executable", 31 | flag=False, 32 | value_required=True, 33 | ), 34 | option( 35 | "clear", 36 | None, 37 | "Clear the existing virtual environment if it exists. ", 38 | flag=True, 39 | ), 40 | option( 41 | "compile", 42 | None, 43 | "Compile Python source files to bytecode." 44 | " (This option has no effect if modern-installation is disabled" 45 | " because the old installer always compiles.)", 46 | flag=True, 47 | ), 48 | option( 49 | "platform", 50 | None, 51 | ( 52 | "Only use wheels compatible with the specified platform." 53 | " Otherwise the default behavior uses the platform" 54 | " of the running system. (Experimental)" 55 | ), 56 | flag=False, 57 | value_required=True, 58 | ), 59 | ] 60 | 61 | bundler_name = "venv" 62 | 63 | def configure_bundler(self, bundler: VenvBundler) -> None: # type: ignore[override] 64 | bundler.set_path(Path(self.argument("path"))) 65 | bundler.set_executable(self.option("python")) 66 | bundler.set_remove(self.option("clear")) 67 | bundler.set_compile(self.option("compile")) 68 | bundler.set_platform(self.option("platform")) 69 | bundler.set_activated_groups(self.activated_groups) 70 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: tests-${{ github.head_ref || github.ref }} 11 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | tests: 17 | name: ${{ matrix.os }} / ${{ matrix.python-version }} ${{ matrix.suffix }} 18 | runs-on: ${{ matrix.image }} 19 | strategy: 20 | matrix: 21 | os: [Ubuntu, macOS, Windows] 22 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 23 | include: 24 | - os: Ubuntu 25 | image: ubuntu-latest 26 | - os: Windows 27 | image: windows-2022 28 | - os: macOS 29 | image: macos-14 30 | fail-fast: false 31 | defaults: 32 | run: 33 | shell: bash 34 | steps: 35 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 36 | with: 37 | persist-credentials: false 38 | 39 | - name: Set up Python ${{ matrix.python-version }} 40 | uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | allow-prereleases: true 44 | 45 | - name: Get full Python version 46 | id: full-python-version 47 | run: echo "version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))")" >> $GITHUB_OUTPUT 48 | 49 | - name: Bootstrap poetry 50 | run: | 51 | curl -sL https://install.python-poetry.org | python - -y ${{ matrix.bootstrap-args }} 52 | 53 | - name: Update PATH 54 | if: ${{ matrix.os != 'Windows' }} 55 | run: echo "$HOME/.local/bin" >> $GITHUB_PATH 56 | 57 | - name: Update Path for Windows 58 | if: ${{ matrix.os == 'Windows' }} 59 | run: echo "$APPDATA\Python\Scripts" >> $GITHUB_PATH 60 | 61 | - name: Configure poetry 62 | run: poetry config virtualenvs.in-project true 63 | 64 | - name: Set up cache 65 | uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 66 | id: cache 67 | with: 68 | path: .venv 69 | key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} 70 | 71 | - name: Ensure cache is healthy 72 | if: steps.cache.outputs.cache-hit == 'true' 73 | run: timeout 10s poetry run pip --version || rm -rf .venv 74 | 75 | - name: Install dependencies 76 | run: poetry install --with github-actions 77 | 78 | - name: Run mypy 79 | run: poetry run mypy 80 | 81 | - name: Run pytest 82 | run: poetry run pytest -v 83 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "poetry-plugin-bundle" 3 | version = "1.7.0" 4 | description = "Poetry plugin to bundle projects into various formats" 5 | authors = [{ name = "Sébastien Eustace", email = "sebastien@eustace.io" }] 6 | license = "MIT" 7 | readme = "README.md" 8 | requires-python = ">=3.9,<4.0" 9 | dependencies = ["poetry>=2.1.0,<3.0.0"] 10 | dynamic = ["classifiers"] 11 | 12 | [project.entry-points."poetry.application.plugin"] 13 | export = "poetry_plugin_bundle.plugin:BundleApplicationPlugin" 14 | 15 | [tool.poetry] 16 | packages = [ 17 | { include = "poetry_plugin_bundle", from = "src" } 18 | ] 19 | include = [ 20 | { path = "tests", format = "sdist" } 21 | ] 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | pre-commit = ">=2.6" 25 | # add setuptools for PyCharm 26 | # see https://youtrack.jetbrains.com/issue/PY-48909 27 | # and https://github.com/JetBrains/intellij-community/commit/3f37786ec0bf5066e4527690d1068b5a00680ea3 28 | setuptools = { version = ">=60", python = "<3.10" } 29 | 30 | [tool.poetry.group.test.dependencies] 31 | coverage = ">=7.2.0" 32 | pytest = ">=7.1.2" 33 | pytest-mock = ">=3.6.1" 34 | 35 | [tool.poetry.group.typing.dependencies] 36 | mypy = ">=1.1.1" 37 | 38 | # only used in github actions 39 | [tool.poetry.group.github-actions] 40 | optional = true 41 | [tool.poetry.group.github-actions.dependencies] 42 | pytest-github-actions-annotate-failures = "^0.1.7" 43 | 44 | 45 | [tool.ruff] 46 | fix = true 47 | line-length = 88 48 | src = ["src"] 49 | target-version = "py38" 50 | 51 | [tool.ruff.lint] 52 | extend-select = [ 53 | "B", # flake8-bugbear 54 | "C4", # flake8-comprehensions 55 | "ERA", # flake8-eradicate/eradicate 56 | "I", # isort 57 | "N", # pep8-naming 58 | "PIE", # flake8-pie 59 | "PGH", # pygrep 60 | "RUF", # ruff checks 61 | "SIM", # flake8-simplify 62 | "T20", # flake8-print 63 | "TCH", # flake8-type-checking 64 | "TID", # flake8-tidy-imports 65 | "UP", # pyupgrade 66 | ] 67 | extend-safe-fixes = [ 68 | "TCH", # move import from and to TYPE_CHECKING blocks 69 | ] 70 | unfixable = [ 71 | "ERA", # do not autoremove commented out code 72 | ] 73 | 74 | [tool.ruff.lint.flake8-tidy-imports] 75 | ban-relative-imports = "all" 76 | 77 | [tool.ruff.lint.isort] 78 | force-single-line = true 79 | lines-between-types = 1 80 | lines-after-imports = 2 81 | known-first-party = ["poetry_plugin_bundle"] 82 | required-imports = ["from __future__ import annotations"] 83 | 84 | 85 | [tool.mypy] 86 | enable_error_code = [ 87 | "ignore-without-code", 88 | "redundant-expr", 89 | "truthy-bool", 90 | ] 91 | explicit_package_bases = true 92 | files = ["src", "tests"] 93 | mypy_path = "src" 94 | namespace_packages = true 95 | show_error_codes = true 96 | strict = true 97 | 98 | [[tool.mypy.overrides]] 99 | module = [ 100 | 'cleo.*', 101 | ] 102 | ignore_missing_imports = true 103 | 104 | 105 | [tool.pytest.ini_options] 106 | testpaths = [ 107 | "tests" 108 | ] 109 | 110 | 111 | [build-system] 112 | requires = ["poetry-core>=2.0"] 113 | build-backend = "poetry.core.masonry.api" 114 | -------------------------------------------------------------------------------- /tests/console/commands/bundle/test_venv.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import TYPE_CHECKING 5 | 6 | from poetry.console.application import Application 7 | 8 | from poetry_plugin_bundle.bundlers.venv_bundler import VenvBundler 9 | 10 | 11 | if TYPE_CHECKING: 12 | from cleo.testers.application_tester import ApplicationTester 13 | from pytest_mock import MockerFixture 14 | 15 | 16 | def test_venv_calls_venv_bundler( 17 | app_tester: ApplicationTester, mocker: MockerFixture 18 | ) -> None: 19 | mock = mocker.patch( 20 | "poetry_plugin_bundle.bundlers.venv_bundler.VenvBundler.bundle", 21 | side_effect=[True, False, False, False, False], 22 | ) 23 | set_path = mocker.spy(VenvBundler, "set_path") 24 | set_executable = mocker.spy(VenvBundler, "set_executable") 25 | set_remove = mocker.spy(VenvBundler, "set_remove") 26 | set_activated_groups = mocker.spy(VenvBundler, "set_activated_groups") 27 | set_compile = mocker.spy(VenvBundler, "set_compile") 28 | 29 | app_tester.application.catch_exceptions(False) 30 | assert app_tester.execute("bundle venv /foo") == 0 31 | assert ( 32 | app_tester.execute("bundle venv /foo --python python3.8 --clear --with dev") 33 | == 1 34 | ) 35 | assert app_tester.execute("bundle venv /foo --only dev") == 1 36 | assert app_tester.execute("bundle venv /foo --without main --with dev") == 1 37 | assert app_tester.execute("bundle venv /foo --compile") == 1 38 | 39 | assert isinstance(app_tester.application, Application) 40 | assert [ 41 | mocker.call(app_tester.application.poetry, mocker.ANY), 42 | mocker.call(app_tester.application.poetry, mocker.ANY), 43 | mocker.call(app_tester.application.poetry, mocker.ANY), 44 | mocker.call(app_tester.application.poetry, mocker.ANY), 45 | mocker.call(app_tester.application.poetry, mocker.ANY), 46 | ] == mock.call_args_list 47 | 48 | assert set_path.call_args_list == [ 49 | mocker.call(mocker.ANY, Path("/foo")), 50 | mocker.call(mocker.ANY, Path("/foo")), 51 | mocker.call(mocker.ANY, Path("/foo")), 52 | mocker.call(mocker.ANY, Path("/foo")), 53 | mocker.call(mocker.ANY, Path("/foo")), 54 | ] 55 | assert set_executable.call_args_list == [ 56 | mocker.call(mocker.ANY, None), 57 | mocker.call(mocker.ANY, "python3.8"), 58 | mocker.call(mocker.ANY, None), 59 | mocker.call(mocker.ANY, None), 60 | mocker.call(mocker.ANY, None), 61 | ] 62 | assert set_remove.call_args_list == [ 63 | mocker.call(mocker.ANY, False), 64 | mocker.call(mocker.ANY, True), 65 | mocker.call(mocker.ANY, False), 66 | mocker.call(mocker.ANY, False), 67 | mocker.call(mocker.ANY, False), 68 | ] 69 | assert set_activated_groups.call_args_list == [ 70 | mocker.call(mocker.ANY, {"main"}), 71 | mocker.call(mocker.ANY, {"main", "dev"}), 72 | mocker.call(mocker.ANY, {"dev"}), 73 | mocker.call(mocker.ANY, {"dev"}), 74 | mocker.call(mocker.ANY, {"main"}), 75 | ] 76 | 77 | assert set_compile.call_args_list == [ 78 | mocker.call(mocker.ANY, False), 79 | mocker.call(mocker.ANY, False), 80 | mocker.call(mocker.ANY, False), 81 | mocker.call(mocker.ANY, False), 82 | mocker.call(mocker.ANY, True), 83 | ] 84 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | ## [1.7.0] - 2025-03-10 5 | 6 | ### Added 7 | 8 | - Add experimental `--platform` option to facilitate installing wheels for platforms other than the host system ([#123](https://github.com/python-poetry/poetry-plugin-bundle/pull/123)). 9 | 10 | 11 | ## [1.6.0] - 2025-02-16 12 | 13 | ### Added 14 | 15 | - Ensure compatibility with Poetry 2.1 ([#137](https://github.com/python-poetry/poetry-plugin-bundle/pull/137)). 16 | 17 | ### Changed 18 | 19 | - Drop support for older Poetry versions ([#137](https://github.com/python-poetry/poetry-plugin-bundle/pull/137)). 20 | 21 | 22 | ## [1.5.0] - 2025-01-05 23 | 24 | ### Added 25 | 26 | - Ensure compatibility with Poetry 2.0 ([#128](https://github.com/python-poetry/poetry-plugin-bundle/pull/128)). 27 | - Add support for projects with `package-mode = false` ([#119](https://github.com/python-poetry/poetry-plugin-bundle/pull/119)). 28 | 29 | ### Changed 30 | 31 | - Drop support for Python 3.8 ([#127](https://github.com/python-poetry/poetry-plugin-bundle/pull/127)). 32 | 33 | 34 | ## [1.4.1] - 2024-08-15 35 | 36 | ### Fixed 37 | 38 | - Fix an issue where `path/to/venv` was ignored and an existing venv was used instead ([#114](https://github.com/python-poetry/poetry-plugin-bundle/pull/114)). 39 | 40 | 41 | ## [1.4.0] - 2024-07-26 42 | 43 | ### Added 44 | 45 | - Add a `--compile` option analogous to `poetry install` ([#88](https://github.com/python-poetry/poetry-plugin-bundle/pull/88)). 46 | 47 | ### Changed 48 | 49 | - Drop support for Python 3.7 ([#66](https://github.com/python-poetry/poetry-plugin-bundle/pull/66)). 50 | - Install all dependencies as non-editable ([#106](https://github.com/python-poetry/poetry-plugin-bundle/pull/106)). 51 | - Use same logic as `poetry install` to determine the Python version if not provided explicitly ([#103](https://github.com/python-poetry/poetry-plugin-bundle/pull/103)). 52 | 53 | 54 | ## [1.3.0] - 2023-05-29 55 | 56 | ### Added 57 | 58 | - Ensure compatibility with poetry 1.5 ([#61](https://github.com/python-poetry/poetry-plugin-bundle/pull/61)). 59 | 60 | 61 | ## [1.2.0] - 2023-03-19 62 | 63 | ### Added 64 | 65 | - Ensure compatibility with poetry 1.4 ([#48](https://github.com/python-poetry/poetry-plugin-bundle/pull/48)). 66 | 67 | ### Changed 68 | 69 | - Drop some compatibility code and bump minimum required poetry version to 1.4.0 ([#48](https://github.com/python-poetry/poetry-plugin-bundle/pull/48)). 70 | 71 | 72 | ## [1.1.0] - 2022-11-04 73 | 74 | ### Added 75 | 76 | - Add support for dependency groups ([#26](https://github.com/python-poetry/poetry-plugin-bundle/pull/26)). 77 | 78 | 79 | ## [1.0.0] - 2022-08-24 80 | 81 | Initial version. 82 | 83 | 84 | [Unreleased]: https://github.com/python-poetry/poetry-plugin-bundle/compare/1.7.0...main 85 | [1.7.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.7.0 86 | [1.6.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.6.0 87 | [1.5.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.5.0 88 | [1.4.1]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.4.1 89 | [1.4.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.4.0 90 | [1.3.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.3.0 91 | [1.2.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.2.0 92 | [1.1.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.1.0 93 | [1.0.0]: https://github.com/python-poetry/poetry-plugin-bundle/releases/tag/1.0.0 94 | -------------------------------------------------------------------------------- /tests/utils/test_platforms.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from poetry.utils.env import MockEnv 6 | 7 | import poetry_plugin_bundle.utils.platforms as platforms 8 | 9 | 10 | def _get_supported_tags_set( 11 | platform: str, python_version_info: tuple[int, int, int] 12 | ) -> set[str]: 13 | env = MockEnv(version_info=python_version_info) 14 | result = platforms.create_supported_tags(platform, env) 15 | return {str(tag) for tag in result} 16 | 17 | 18 | def _test_create_supported_tags( 19 | platform: str, 20 | python_version_info: tuple[int, int, int], 21 | expected_tags: set[str], 22 | unexpected_tags: set[str], 23 | ) -> None: 24 | result_set = _get_supported_tags_set(platform, python_version_info) 25 | 26 | assert result_set.issuperset(expected_tags) 27 | assert not result_set.intersection(unexpected_tags) 28 | 29 | 30 | def test_create_supported_tags_manylinux() -> None: 31 | _test_create_supported_tags( 32 | platform="manylinux_2_12_x86_64", 33 | python_version_info=(3, 12, 1), 34 | expected_tags={ 35 | "cp312-cp312-manylinux_2_12_x86_64", 36 | "cp312-none-manylinux_2_12_x86_64", 37 | "cp312-abi3-manylinux_2_12_x86_64", 38 | "cp312-cp312-manylinux_2_1_x86_64", 39 | "cp311-abi3-manylinux_2_12_x86_64", 40 | "py312-none-manylinux_2_12_x86_64", 41 | "py312-none-any", 42 | "cp312-none-any", 43 | }, 44 | unexpected_tags={ 45 | "cp313-cp313-manylinux_2_12_x86_64", 46 | "cp312-cp312-manylinux_2_13_x86_64", 47 | }, 48 | ) 49 | 50 | 51 | def test_create_supported_tags_legacy_manylinux_aliases() -> None: 52 | _test_create_supported_tags( 53 | platform="manylinux1_x86_64", 54 | python_version_info=(3, 10, 2), 55 | expected_tags={ 56 | "cp310-cp310-manylinux_2_5_x86_64", 57 | "cp310-cp310-manylinux_2_1_x86_64", 58 | }, 59 | unexpected_tags={ 60 | "cp310-cp310-manylinux_2_6_x86_64", 61 | }, 62 | ) 63 | 64 | _test_create_supported_tags( 65 | platform="manylinux2010_x86_64", 66 | python_version_info=(3, 10, 2), 67 | expected_tags={ 68 | "cp310-cp310-manylinux_2_12_x86_64", 69 | "cp310-cp310-manylinux_2_1_x86_64", 70 | }, 71 | unexpected_tags={ 72 | "cp310-cp310-manylinux_2_13_x86_64", 73 | }, 74 | ) 75 | 76 | _test_create_supported_tags( 77 | platform="manylinux2014_x86_64", 78 | python_version_info=(3, 11, 9), 79 | expected_tags={ 80 | "cp311-cp311-manylinux_2_17_x86_64", 81 | "cp311-cp311-manylinux_2_1_x86_64", 82 | }, 83 | unexpected_tags={ 84 | "cp311-cp311-manylinux_2_24_x86_64", 85 | }, 86 | ) 87 | 88 | 89 | def test_create_supported_tags_macosx() -> None: 90 | _test_create_supported_tags( 91 | platform="macosx_11_0_arm64", 92 | python_version_info=(3, 11, 8), 93 | expected_tags={ 94 | "cp311-abi3-macosx_11_0_arm64", 95 | "cp311-abi3-macosx_10_12_universal2", 96 | "cp311-none-macosx_11_0_universal2", 97 | "py311-none-any", 98 | }, 99 | unexpected_tags={ 100 | "cp311-none-macosx_11_1_universal2", 101 | }, 102 | ) 103 | 104 | _test_create_supported_tags( 105 | platform="macosx_10_9_x86_64", 106 | python_version_info=(3, 11, 8), 107 | expected_tags={ 108 | "cp311-abi3-macosx_10_9_universal2", 109 | "cp311-abi3-macosx_10_9_x86_64", 110 | "cp311-abi3-macosx_10_9_intel", 111 | "cp311-none-macosx_10_7_universal2", 112 | "py311-none-any", 113 | }, 114 | unexpected_tags={ 115 | "cp311-none-macosx_11_1_universal2", 116 | "cp311-abi3-macosx_11_0_arm64", 117 | }, 118 | ) 119 | 120 | 121 | def test_create_supported_tags_musllinux() -> None: 122 | _test_create_supported_tags( 123 | platform="musllinux_1_1_x86_64", 124 | python_version_info=(3, 13, 1), 125 | expected_tags={ 126 | "cp313-cp313-musllinux_1_1_x86_64", 127 | "cp313-abi3-musllinux_1_1_x86_64", 128 | "py312-none-any", 129 | "cp313-none-any", 130 | }, 131 | unexpected_tags={ 132 | "cp313-cp313-musllinux_1_2_x86_64", 133 | }, 134 | ) 135 | 136 | 137 | def test_create_supported_tags_unsupported_platform() -> None: 138 | env = MockEnv(version_info=(3, 12, 1)) 139 | 140 | unsupported_platforms = [ 141 | "win32", 142 | "linux", 143 | "foobar", 144 | ] 145 | for platform in unsupported_platforms: 146 | with pytest.raises(NotImplementedError): 147 | platforms.create_supported_tags(platform, env) 148 | 149 | 150 | def test_create_supported_tags_malformed_platforms() -> None: 151 | env = MockEnv(version_info=(3, 12, 1)) 152 | 153 | malformed_platforms = [ 154 | "macosx_11_blah_arm64", 155 | "macosx", 156 | "manylinux_blammo_12_x86_64", 157 | "manylinux_x86_64", 158 | "manylinux", 159 | "musllinux_?_1_x86_64", 160 | "musllinux", 161 | ] 162 | for platform in malformed_platforms: 163 | with pytest.raises(ValueError): 164 | platforms.create_supported_tags(platform, env) 165 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/utils/platforms.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING 5 | 6 | 7 | if TYPE_CHECKING: 8 | from packaging.tags import Tag 9 | from poetry.utils.env import Env 10 | 11 | 12 | @dataclass 13 | class PlatformTagParseResult: 14 | platform: str 15 | version_major: int 16 | version_minor: int 17 | arch: str 18 | 19 | @staticmethod 20 | def parse(tag: str) -> PlatformTagParseResult: 21 | import re 22 | 23 | match = re.match("([a-z]+)_([0-9]+)_([0-9]+)_(.*)", tag) 24 | if not match: 25 | raise ValueError(f"Invalid platform tag: {tag}") 26 | platform, version_major_str, version_minor_str, arch = match.groups() 27 | return PlatformTagParseResult( 28 | platform=platform, 29 | version_major=int(version_major_str), 30 | version_minor=int(version_minor_str), 31 | arch=arch, 32 | ) 33 | 34 | def to_tag(self) -> str: 35 | return "_".join( 36 | [self.platform, str(self.version_major), str(self.version_minor), self.arch] 37 | ) 38 | 39 | 40 | def create_supported_tags(platform: str, env: Env) -> list[Tag]: 41 | """ 42 | Given a platform specifier string, generate a list of compatible tags 43 | for the argument environment's interpreter. 44 | 45 | Refer to: 46 | https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#platform-tag 47 | https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-platform 48 | """ 49 | from packaging.tags import INTERPRETER_SHORT_NAMES 50 | from packaging.tags import compatible_tags 51 | from packaging.tags import cpython_tags 52 | from packaging.tags import generic_tags 53 | 54 | if platform.startswith("manylinux"): 55 | supported_platforms = create_supported_manylinux_platforms(platform) 56 | elif platform.startswith("musllinux"): 57 | supported_platforms = create_supported_musllinux_platforms(platform) 58 | elif platform.startswith("macosx"): 59 | supported_platforms = create_supported_macosx_platforms(platform) 60 | else: 61 | raise NotImplementedError(f"Platform {platform} not supported") 62 | 63 | python_implementation = env.python_implementation.lower() 64 | python_version = env.version_info[:2] 65 | interpreter_name = INTERPRETER_SHORT_NAMES.get( 66 | python_implementation, python_implementation 67 | ) 68 | interpreter = None 69 | 70 | if interpreter_name == "cp": 71 | tags = list( 72 | cpython_tags(python_version=python_version, platforms=supported_platforms) 73 | ) 74 | interpreter = f"{interpreter_name}{python_version[0]}{python_version[1]}" 75 | else: 76 | tags = list( 77 | generic_tags( 78 | interpreter=interpreter, abis=[], platforms=supported_platforms 79 | ) 80 | ) 81 | 82 | tags.extend( 83 | compatible_tags( 84 | interpreter=interpreter, 85 | python_version=python_version, 86 | platforms=supported_platforms, 87 | ) 88 | ) 89 | 90 | return tags 91 | 92 | 93 | def create_supported_manylinux_platforms(platform: str) -> list[str]: 94 | """ 95 | https://peps.python.org/pep-0600/ 96 | manylinux_${GLIBCMAJOR}_${GLIBCMINOR}_${ARCH} 97 | 98 | For now, only GLIBCMAJOR "2" is supported. It is unclear if there will be a need to support a future major 99 | version like "3" and if specified, how generate the compatible 2.x version tags. 100 | """ 101 | # Implementation based on https://peps.python.org/pep-0600/#package-installers 102 | 103 | tag = normalize_legacy_manylinux_alias(platform) 104 | 105 | parsed = PlatformTagParseResult.parse(tag) 106 | return [ 107 | f"{parsed.platform}_{parsed.version_major}_{tag_minor}_{parsed.arch}" 108 | for tag_minor in range(parsed.version_minor, -1, -1) 109 | ] 110 | 111 | 112 | LEGACY_MANYLINUX_ALIASES = { 113 | "manylinux1": "manylinux_2_5", 114 | "manylinux2010": "manylinux_2_12", 115 | "manylinux2014": "manylinux_2_17", 116 | } 117 | 118 | 119 | def normalize_legacy_manylinux_alias(tag: str) -> str: 120 | tag_os_index_end = tag.index("_") 121 | tag_os = tag[:tag_os_index_end] 122 | tag_arch_suffix = tag[tag_os_index_end:] 123 | os_replacement = LEGACY_MANYLINUX_ALIASES.get(tag_os) 124 | if not os_replacement: 125 | return tag 126 | 127 | return os_replacement + tag_arch_suffix 128 | 129 | 130 | def create_supported_macosx_platforms(platform: str) -> list[str]: 131 | import re 132 | 133 | from packaging.tags import mac_platforms 134 | 135 | match = re.match("macosx_([0-9]+)_([0-9]+)_(.*)", platform) 136 | if not match: 137 | raise ValueError(f"Invalid macosx tag: {platform}") 138 | tag_major_str, tag_minor_str, tag_arch = match.groups() 139 | tag_major_max = int(tag_major_str) 140 | tag_minor_max = int(tag_minor_str) 141 | 142 | return list(mac_platforms(version=(tag_major_max, tag_minor_max), arch=tag_arch)) 143 | 144 | 145 | def create_supported_musllinux_platforms(platform: str) -> list[str]: 146 | import re 147 | 148 | match = re.match("musllinux_([0-9]+)_([0-9]+)_(.*)", platform) 149 | if not match: 150 | raise ValueError(f"Invalid musllinux tag: {platform}") 151 | tag_major_str, tag_minor_str, tag_arch = match.groups() 152 | tag_major_max = int(tag_major_str) 153 | tag_minor_max = int(tag_minor_str) 154 | 155 | return [ 156 | f"musllinux_{tag_major_max}_{minor}_{tag_arch}" 157 | for minor in range(tag_minor_max, -1, -1) 158 | ] 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poetry Plugin: Bundle 2 | 3 | [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/) 4 | 5 | This package is a plugin that allows the bundling of Poetry projects into various formats. 6 | 7 | ## Installation 8 | 9 | The easiest way to install the `bundle` plugin is via the `self add` command of Poetry. 10 | 11 | ```bash 12 | poetry self add poetry-plugin-bundle 13 | ``` 14 | 15 | If you used `pipx` to install Poetry you can add the plugin via the `pipx inject` command. 16 | 17 | ```bash 18 | pipx inject poetry poetry-plugin-bundle 19 | ``` 20 | 21 | Otherwise, if you used `pip` to install Poetry you can add the plugin packages via the `pip install` command. 22 | 23 | ```bash 24 | pip install poetry-plugin-bundle 25 | ``` 26 | 27 | ## Usage 28 | 29 | The plugin introduces a `bundle` command namespace that regroups commands to bundle the current project 30 | and its dependencies into various formats. These commands are particularly useful to deploy 31 | Poetry-managed applications. 32 | 33 | ### bundle venv 34 | 35 | The `bundle venv` command bundles the project and its dependencies into a virtual environment. 36 | 37 | The following command 38 | 39 | ```bash 40 | poetry bundle venv /path/to/environment 41 | ``` 42 | 43 | will bundle the project in the `/path/to/environment` directory by creating the virtual environment, 44 | installing the dependencies and the current project inside it. If the directory does not exist, 45 | it will be created automatically. 46 | 47 | By default, the command uses the same Python executable that Poetry would use 48 | when running `poetry install` to build the virtual environment. 49 | If you want to use a different one, you can specify it with the `--python/-p` option: 50 | 51 | ```bash 52 | poetry bundle venv /path/to/environment --python /full/path/to/python 53 | poetry bundle venv /path/to/environment -p python3.8 54 | poetry bundle venv /path/to/environment -p 3.8 55 | ``` 56 | 57 | **Note** 58 | 59 | If the virtual environment already exists, two things can happen: 60 | 61 | - **The python version of the virtual environment is the same as the main one**: the dependencies will be synced (updated or removed). 62 | - **The python version of the virtual environment is different**: the virtual environment will be recreated from scratch. 63 | 64 | You can also ensure that the virtual environment is recreated by using the `--clear` option: 65 | 66 | ```bash 67 | poetry bundle venv /path/to/environment --clear 68 | ``` 69 | 70 | #### --platform option (Experimental) 71 | This option allows you to specify a target platform for binary wheel selection, allowing you to install wheels for 72 | architectures/platforms other than the host system. 73 | 74 | The primary use case is in CI/CD operations to produce a deployable asset, such as a ZIP file for AWS Lambda and other 75 | such cloud providers. It is common for the runtimes of these target environments to be different enough from the CI/CD's 76 | runner host such that the binary wheels selected using the host's criteria are not compatible with the target system's. 77 | 78 | #### Supported platform values 79 | The `--platform` option requires a value that conforms to the [Python Packaging Platform Tag format]( 80 | https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#platform-tag). Only the following 81 | "families" are supported at this time: 82 | - `manylinux` 83 | - `musllinux` 84 | - `macosx` 85 | 86 | #### Examples of valid platform tags 87 | This is not a comprehensive list, but illustrates typical examples. 88 | 89 | `manylinux_2_28_x86_64`, `manylinux1_x86_64`, `manylinux2010_x86_64`, `manylinux2014_x86_64` 90 | 91 | `musllinux_1_2_x86_64` 92 | 93 | `macosx_10_9_x86_64`, `macosx_10_9_intel`, `macosx_11_1_universal2`, `macosx_11_0_arm64` 94 | 95 | #### Example use case for AWS Lambda 96 | As an example of one motivating use case for this option, consider the AWS Lambda "serverless" execution environment. 97 | Depending upon which Python version you configure for your runtime, you may get different versions of the Linux system 98 | runtime. When dealing with pre-compiled binary wheels, these runtime differences can matter. If a shared library from 99 | a wheel is packaged in your deployment artifact that is incompatible with the runtime provided environment, then your 100 | Python "function" will error at execution time in the serverless environment. 101 | The issue arises when the build system that is producing the deployment artifact has a materially different platform 102 | from the selected serverless Lambda runtime. 103 | 104 | For example, the Python 3.11 Lambda runtime is Amazon Linux 2, which includes the Glibc 2.26 library. If a Python 105 | application is packaged and deployed in this environment that contains wheels built for a more recent version of Glibc, 106 | then a runtime error will result. This is likely to occur even if the build system is the same CPU architecture 107 | (e.g. x86_64) and core platform (e.g. Linux) and there is a package dependency that provides multiple precompiled 108 | wheels for various Glibc (or other system library) versions. The "best" wheel in the context of the build system can 109 | differ from that of the target execution environment. 110 | 111 | 112 | #### Limitations 113 | **This is not an actual cross-compiler**. Nor is it a containerized compilation/build environment. It simply allows 114 | controlling which **prebuilt** binaries are selected. It is not a replacement for cross-compilation or containerized 115 | builds for use cases requiring that. 116 | 117 | If there is not a binary wheel distribution compatible with the specified platform, then the package's source 118 | distribution is selected. If there are compile/build steps for "extensions" that need to run for the source 119 | distribution, then these operations will execute in the context of the host CI/build system. 120 | **This means that the `--platform` option 121 | has no impact on any extension compile/build operations that must occur during package installation.** 122 | This feature is only for 123 | **selecting** prebuilt wheels, and **not for compiling** them from source. 124 | 125 | Arguably, in a vast number of use cases, prebuilt wheel binaries are available for your packages and simply selecting 126 | them based on a platform other than the host CI/build system is much faster and simpler than heavier build-from-source 127 | alternatives. 128 | -------------------------------------------------------------------------------- /src/poetry_plugin_bundle/bundlers/venv_bundler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from poetry_plugin_bundle.bundlers.bundler import Bundler 6 | 7 | 8 | if TYPE_CHECKING: 9 | from pathlib import Path 10 | 11 | from cleo.io.io import IO 12 | from cleo.io.outputs.section_output import SectionOutput 13 | from packaging.utils import NormalizedName 14 | from poetry.poetry import Poetry 15 | from poetry.repositories.lockfile_repository import LockfileRepository 16 | from poetry.utils.env import Env 17 | 18 | 19 | class VenvBundler(Bundler): 20 | name = "venv" 21 | 22 | def __init__(self) -> None: 23 | self._path: Path 24 | self._executable: str | None = None 25 | self._remove: bool = False 26 | self._activated_groups: set[NormalizedName] | None = None 27 | self._compile: bool = False 28 | self._platform: str | None = None 29 | 30 | def set_path(self, path: Path) -> VenvBundler: 31 | self._path = path 32 | 33 | return self 34 | 35 | def set_executable(self, executable: str | None) -> VenvBundler: 36 | self._executable = executable 37 | 38 | return self 39 | 40 | def set_activated_groups( 41 | self, activated_groups: set[NormalizedName] 42 | ) -> VenvBundler: 43 | self._activated_groups = activated_groups 44 | 45 | return self 46 | 47 | def set_remove(self, remove: bool = True) -> VenvBundler: 48 | self._remove = remove 49 | 50 | return self 51 | 52 | def set_compile(self, compile: bool = False) -> VenvBundler: 53 | self._compile = compile 54 | 55 | return self 56 | 57 | def set_platform(self, platform: str | None) -> VenvBundler: 58 | self._platform = platform 59 | 60 | return self 61 | 62 | def bundle(self, poetry: Poetry, io: IO) -> bool: 63 | from pathlib import Path 64 | from tempfile import TemporaryDirectory 65 | 66 | from cleo.io.null_io import NullIO 67 | from poetry.core.masonry.builders.wheel import WheelBuilder 68 | from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError 69 | from poetry.core.packages.package import Package 70 | from poetry.installation.installer import Installer 71 | from poetry.installation.operations.install import Install 72 | from poetry.packages.locker import Locker 73 | from poetry.utils.env import EnvManager 74 | from poetry.utils.env.python import Python 75 | from poetry.utils.env.python.exceptions import InvalidCurrentPythonVersionError 76 | 77 | class CustomEnvManager(EnvManager): 78 | """ 79 | This class is used as an adapter for allowing us to use 80 | Poetry's EnvManager.create_venv but with a custom path. 81 | It works by hijacking the "in_project_venv" concept so that 82 | we can get that behavior, but with a custom path. 83 | """ 84 | 85 | @property 86 | def in_project_venv(self) -> Path: 87 | return self._path 88 | 89 | def use_in_project_venv(self) -> bool: 90 | return True 91 | 92 | def in_project_venv_exists(self) -> bool: 93 | """ 94 | Coerce this call to always return True so that we avoid the path in the base 95 | EnvManager.get that detects an existing env residing at the centralized Poetry 96 | virtualenvs_path location. 97 | """ 98 | return True 99 | 100 | def create_venv_at_path( 101 | self, 102 | path: Path, 103 | python: Python | None, 104 | force: bool, 105 | ) -> Env: 106 | self._path = path 107 | return self.create_venv(name=None, python=python, force=force) 108 | 109 | warnings = [] 110 | 111 | manager = CustomEnvManager(poetry) 112 | executable = Path(self._executable) if self._executable else None 113 | python = Python(executable) if executable else None 114 | 115 | message = self._get_message(poetry, self._path) 116 | if io.is_decorated() and not io.is_debug(): 117 | io = io.section() # type: ignore[assignment] 118 | 119 | io.write_line(message) 120 | 121 | if executable: 122 | self._write( 123 | io, 124 | f"{message}: Creating a virtual environment using Python" 125 | f" {executable}", 126 | ) 127 | else: 128 | self._write( 129 | io, 130 | f"{message}: Creating a virtual environment" 131 | " using Poetry-determined Python", 132 | ) 133 | 134 | try: 135 | env = manager.create_venv_at_path( 136 | self._path, python=python, force=self._remove 137 | ) 138 | except InvalidCurrentPythonVersionError: 139 | self._write( 140 | io, 141 | f"{message}: Replacing existing virtual environment" 142 | " due to incompatible Python version", 143 | ) 144 | env = manager.create_venv_at_path(self._path, python=python, force=True) 145 | 146 | if self._platform: 147 | self._constrain_env_platform(env, self._platform) 148 | 149 | self._write(io, f"{message}: Installing dependencies") 150 | 151 | class CustomLocker(Locker): 152 | def locked_repository(self) -> LockfileRepository: 153 | repo = super().locked_repository() 154 | for package in repo.packages: 155 | package.develop = False 156 | return repo 157 | 158 | custom_locker = CustomLocker(poetry.locker.lock, poetry.locker._pyproject_data) 159 | 160 | installer = Installer( 161 | NullIO() if not io.is_debug() else io, 162 | env, 163 | poetry.package, 164 | custom_locker, 165 | poetry.pool, 166 | poetry.config, 167 | ) 168 | if self._activated_groups is not None: 169 | installer.only_groups(self._activated_groups) 170 | installer.requires_synchronization() 171 | 172 | installer.executor.enable_bytecode_compilation(self._compile) 173 | 174 | return_code = installer.run() 175 | if return_code: 176 | self._write( 177 | io, 178 | self._get_message(poetry, self._path, error=True) 179 | + ": Failed at step Installing dependencies", 180 | ) 181 | return False 182 | 183 | # Skip building the wheel if is_package_mode exists and is set to false 184 | if hasattr(poetry, "is_package_mode") and not poetry.is_package_mode: 185 | self._write( 186 | io, 187 | f"{message}: Skipping installation for non package project" 188 | f" {poetry.package.pretty_name}", 189 | ) 190 | else: 191 | self._write( 192 | io, 193 | f"{message}: Installing {poetry.package.pretty_name}" 194 | f" ({poetry.package.pretty_version})", 195 | ) 196 | 197 | # Build a wheel of the project in a temporary directory 198 | # and install it in the newly create virtual environment 199 | with TemporaryDirectory() as directory: 200 | try: 201 | wheel_name = WheelBuilder.make_in(poetry, directory=Path(directory)) 202 | wheel = Path(directory).joinpath(wheel_name) 203 | package = Package( 204 | poetry.package.name, 205 | poetry.package.version, 206 | source_type="file", 207 | source_url=str(wheel), 208 | ) 209 | installer.executor.execute([Install(package)]) 210 | except ModuleOrPackageNotFoundError: 211 | warnings.append( 212 | "The root package was not installed because no matching module or" 213 | " package was found." 214 | ) 215 | 216 | self._write(io, self._get_message(poetry, self._path, done=True)) 217 | 218 | if warnings: 219 | for warning in warnings: 220 | io.write_line( 221 | f" {warning}" 222 | ) 223 | 224 | return True 225 | 226 | def _get_message( 227 | self, poetry: Poetry, path: Path, done: bool = False, error: bool = False 228 | ) -> str: 229 | operation_color = "blue" 230 | 231 | if error: 232 | operation_color = "red" 233 | elif done: 234 | operation_color = "green" 235 | 236 | verb = "Bundling" 237 | if done: 238 | verb = "Bundled" 239 | 240 | return ( 241 | f" •" 242 | f" {verb} {poetry.package.pretty_name}" 243 | f" ({poetry.package.pretty_version}) into {path}" 244 | ) 245 | 246 | def _write(self, io: IO | SectionOutput, message: str) -> None: 247 | from cleo.io.outputs.section_output import SectionOutput 248 | 249 | if io.is_debug() or not io.is_decorated() or not isinstance(io, SectionOutput): 250 | io.write_line(message) 251 | return 252 | 253 | io.overwrite(message) 254 | 255 | def _constrain_env_platform(self, env: Env, platform: str) -> None: 256 | """ 257 | Set the argument environment's supported tags 258 | based on the configured platform override. 259 | """ 260 | from poetry_plugin_bundle.utils.platforms import create_supported_tags 261 | 262 | env._supported_tags = create_supported_tags(platform, env) 263 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_binary_wheel/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "cffi" 5 | version = "2.0.0" 6 | description = "Foreign Function Interface for Python calling C code." 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["main"] 10 | markers = "platform_python_implementation != \"PyPy\"" 11 | files = [ 12 | {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, 13 | {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, 14 | {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, 15 | {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, 16 | {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, 17 | {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, 18 | {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, 19 | {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, 20 | {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, 21 | {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, 22 | {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, 23 | {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, 24 | {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, 25 | {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, 26 | {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, 27 | {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, 28 | {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, 29 | {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, 30 | {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, 31 | {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, 32 | {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, 33 | {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, 34 | {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, 35 | {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, 36 | {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, 37 | {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, 38 | {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, 39 | {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, 40 | {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, 41 | {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, 42 | {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, 43 | {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, 44 | {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, 45 | {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, 46 | {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, 47 | {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, 48 | {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, 49 | {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, 50 | {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, 51 | {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, 52 | {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, 53 | {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, 54 | {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, 55 | {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, 56 | {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, 57 | {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, 58 | {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, 59 | {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, 60 | {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, 61 | {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, 62 | {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, 63 | {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, 64 | {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, 65 | {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, 66 | {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, 67 | {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, 68 | {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, 69 | {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, 70 | {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, 71 | {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, 72 | {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, 73 | {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, 74 | {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, 75 | {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, 76 | {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, 77 | {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, 78 | {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, 79 | {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, 80 | {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, 81 | {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, 82 | {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, 83 | {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, 84 | {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, 85 | {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, 86 | {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, 87 | {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, 88 | {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, 89 | {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, 90 | {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, 91 | {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, 92 | {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, 93 | {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, 94 | {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, 95 | {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, 96 | ] 97 | 98 | [package.dependencies] 99 | pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} 100 | 101 | [[package]] 102 | name = "cryptography" 103 | version = "43.0.3" 104 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 105 | optional = false 106 | python-versions = ">=3.7" 107 | groups = ["main"] 108 | files = [ 109 | {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, 110 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, 111 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, 112 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, 113 | {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, 114 | {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, 115 | {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, 116 | {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, 117 | {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, 118 | {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, 119 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, 120 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, 121 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, 122 | {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, 123 | {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, 124 | {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, 125 | {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, 126 | {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, 127 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, 128 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, 129 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, 130 | {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, 131 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, 132 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, 133 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, 134 | {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, 135 | {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, 136 | ] 137 | 138 | [package.dependencies] 139 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 140 | 141 | [package.extras] 142 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 143 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 144 | nox = ["nox"] 145 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 146 | sdist = ["build"] 147 | ssh = ["bcrypt (>=3.1.5)"] 148 | test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 149 | test-randomorder = ["pytest-randomly"] 150 | 151 | [[package]] 152 | name = "pycparser" 153 | version = "2.23" 154 | description = "C parser in Python" 155 | optional = false 156 | python-versions = ">=3.8" 157 | groups = ["main"] 158 | markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" 159 | files = [ 160 | {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, 161 | {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, 162 | ] 163 | 164 | [metadata] 165 | lock-version = "2.1" 166 | python-versions = "^3.9" 167 | content-hash = "7bbfface00dfb65cff50e48484916beeeb0db466b8206d8fdcd5388b606c3be9" 168 | -------------------------------------------------------------------------------- /tests/bundlers/test_venv_bundler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import shutil 4 | import sys 5 | 6 | from pathlib import Path 7 | from typing import TYPE_CHECKING 8 | 9 | import pytest 10 | 11 | from cleo.formatters.style import Style 12 | from cleo.io.buffered_io import BufferedIO 13 | from poetry.core.packages.dependency_group import MAIN_GROUP 14 | from poetry.core.packages.package import Package 15 | from poetry.core.packages.utils.link import Link 16 | from poetry.factory import Factory 17 | from poetry.installation.operations.install import Install 18 | from poetry.puzzle.exceptions import SolverProblemError 19 | from poetry.repositories.repository import Repository 20 | from poetry.repositories.repository_pool import RepositoryPool 21 | from poetry.utils.env import EnvManager 22 | from poetry.utils.env import MockEnv 23 | from poetry.utils.env import VirtualEnv 24 | 25 | from poetry_plugin_bundle.bundlers.venv_bundler import VenvBundler 26 | 27 | 28 | if TYPE_CHECKING: 29 | from poetry.config.config import Config 30 | from poetry.poetry import Poetry 31 | from pytest_mock import MockerFixture 32 | 33 | 34 | @pytest.fixture() 35 | def io() -> BufferedIO: 36 | io = BufferedIO() 37 | 38 | io.output.formatter.set_style("success", Style("green", options=["dark"])) 39 | io.output.formatter.set_style("warning", Style("yellow", options=["dark"])) 40 | 41 | return io 42 | 43 | 44 | @pytest.fixture() 45 | def poetry(config: Config) -> Poetry: 46 | poetry = Factory().create_poetry( 47 | Path(__file__).parent.parent / "fixtures" / "simple_project" 48 | ) 49 | poetry.set_config(config) 50 | 51 | pool = RepositoryPool() 52 | repository = Repository("repo") 53 | repository.add_package(Package("foo", "1.0.0")) 54 | pool.add_repository(repository) 55 | poetry.set_pool(pool) 56 | 57 | return poetry 58 | 59 | 60 | def _create_venv_marker_file(tempdir: str | Path) -> Path: 61 | marker_file = Path(tempdir) / "existing-venv-marker.txt" 62 | marker_file.write_text("This file should get deleted as part of venv recreation.") 63 | return marker_file 64 | 65 | 66 | def test_bundler_should_build_a_new_venv_with_existing_python( 67 | io: BufferedIO, tmpdir: str, poetry: Poetry, mocker: MockerFixture 68 | ) -> None: 69 | shutil.rmtree(tmpdir) 70 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 71 | 72 | bundler = VenvBundler() 73 | bundler.set_path(Path(tmpdir)) 74 | 75 | assert bundler.bundle(poetry, io) 76 | 77 | expected = f"""\ 78 | • Bundling simple-project (1.2.3) into {tmpdir} 79 | • Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Poetry-determined Python 80 | • Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies 81 | • Bundling simple-project (1.2.3) into {tmpdir}: Installing simple-project (1.2.3) 82 | • Bundled simple-project (1.2.3) into {tmpdir} 83 | """ 84 | assert expected == io.fetch_output() 85 | 86 | 87 | def test_bundler_should_build_a_new_venv_with_given_executable( 88 | io: BufferedIO, tmpdir: str, poetry: Poetry, mocker: MockerFixture 89 | ) -> None: 90 | shutil.rmtree(tmpdir) 91 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 92 | 93 | bundler = VenvBundler() 94 | bundler.set_path(Path(tmpdir)) 95 | bundler.set_executable(sys.executable) 96 | 97 | assert bundler.bundle(poetry, io) 98 | 99 | expected = f"""\ 100 | • Bundling simple-project (1.2.3) into {tmpdir} 101 | • Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Python {sys.executable} 102 | • Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies 103 | • Bundling simple-project (1.2.3) into {tmpdir}: Installing simple-project (1.2.3) 104 | • Bundled simple-project (1.2.3) into {tmpdir} 105 | """ 106 | assert expected == io.fetch_output() 107 | 108 | 109 | def test_bundler_should_build_a_new_venv_if_existing_venv_is_incompatible( 110 | io: BufferedIO, tmpdir: str, poetry: Poetry, mocker: MockerFixture 111 | ) -> None: 112 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 113 | 114 | mock_env = MockEnv(path=Path(tmpdir), is_venv=True, version_info=(1, 2, 3)) 115 | mocker.patch("poetry.utils.env.EnvManager.get", return_value=mock_env) 116 | 117 | bundler = VenvBundler() 118 | bundler.set_path(Path(tmpdir)) 119 | 120 | marker_file = _create_venv_marker_file(tmpdir) 121 | 122 | assert marker_file.exists() 123 | assert bundler.bundle(poetry, io) 124 | assert not marker_file.exists() 125 | 126 | expected = f"""\ 127 | • Bundling simple-project (1.2.3) into {tmpdir} 128 | • Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Poetry-determined Python 129 | • Bundling simple-project (1.2.3) into {tmpdir}: Replacing existing virtual environment due to incompatible Python version 130 | • Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies 131 | • Bundling simple-project (1.2.3) into {tmpdir}: Installing simple-project (1.2.3) 132 | • Bundled simple-project (1.2.3) into {tmpdir} 133 | """ 134 | assert expected == io.fetch_output() 135 | 136 | 137 | def test_bundler_should_use_an_existing_venv_if_compatible( 138 | io: BufferedIO, tmp_venv: VirtualEnv, poetry: Poetry, mocker: MockerFixture 139 | ) -> None: 140 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 141 | 142 | bundler = VenvBundler() 143 | bundler.set_path(tmp_venv.path) 144 | 145 | marker_file = _create_venv_marker_file(tmp_venv.path) 146 | 147 | assert marker_file.exists() 148 | assert bundler.bundle(poetry, io) 149 | assert marker_file.exists() 150 | 151 | path = str(tmp_venv.path) 152 | expected = f"""\ 153 | • Bundling simple-project (1.2.3) into {path} 154 | • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 155 | • Bundling simple-project (1.2.3) into {path}: Installing dependencies 156 | • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) 157 | • Bundled simple-project (1.2.3) into {path} 158 | """ 159 | assert expected == io.fetch_output() 160 | 161 | 162 | def test_bundler_should_remove_an_existing_venv_if_forced( 163 | io: BufferedIO, tmp_venv: VirtualEnv, poetry: Poetry, mocker: MockerFixture 164 | ) -> None: 165 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 166 | 167 | bundler = VenvBundler() 168 | bundler.set_path(tmp_venv.path) 169 | bundler.set_remove(True) 170 | 171 | marker_file = _create_venv_marker_file(tmp_venv.path) 172 | 173 | assert marker_file.exists() 174 | assert bundler.bundle(poetry, io) 175 | assert not marker_file.exists() 176 | 177 | path = str(tmp_venv.path) 178 | expected = f"""\ 179 | • Bundling simple-project (1.2.3) into {path} 180 | • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 181 | • Bundling simple-project (1.2.3) into {path}: Installing dependencies 182 | • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) 183 | • Bundled simple-project (1.2.3) into {path} 184 | """ 185 | assert expected == io.fetch_output() 186 | 187 | 188 | def test_bundler_should_fail_when_installation_fails( 189 | io: BufferedIO, tmpdir: str, poetry: Poetry, mocker: MockerFixture 190 | ) -> None: 191 | mocker.patch( 192 | "poetry.installation.executor.Executor._do_execute_operation", 193 | side_effect=Exception(), 194 | ) 195 | 196 | bundler = VenvBundler() 197 | bundler.set_path(Path(tmpdir)) 198 | 199 | assert not bundler.bundle(poetry, io) 200 | 201 | expected = f"""\ 202 | • Bundling simple-project (1.2.3) into {tmpdir} 203 | • Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Poetry-determined Python 204 | • Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies 205 | • Bundling simple-project (1.2.3) into {tmpdir}: Failed at step Installing dependencies 206 | """ 207 | assert expected == io.fetch_output() 208 | 209 | 210 | def test_bundler_should_display_a_warning_for_projects_with_no_module( 211 | io: BufferedIO, tmp_venv: VirtualEnv, mocker: MockerFixture, config: Config 212 | ) -> None: 213 | poetry = Factory().create_poetry( 214 | Path(__file__).parent.parent / "fixtures" / "simple_project_with_no_module" 215 | ) 216 | poetry.set_config(config) 217 | 218 | pool = RepositoryPool() 219 | repository = Repository("repo") 220 | repository.add_package(Package("foo", "1.0.0")) 221 | pool.add_repository(repository) 222 | poetry.set_pool(pool) 223 | 224 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 225 | 226 | bundler = VenvBundler() 227 | bundler.set_path(tmp_venv.path) 228 | bundler.set_remove(True) 229 | 230 | assert bundler.bundle(poetry, io) 231 | 232 | path = str(tmp_venv.path) 233 | expected = f"""\ 234 | • Bundling simple-project (1.2.3) into {path} 235 | • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 236 | • Bundling simple-project (1.2.3) into {path}: Installing dependencies 237 | • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) 238 | • Bundled simple-project (1.2.3) into {path} 239 | • The root package was not installed because no matching module or package was found. 240 | """ 241 | assert expected == io.fetch_output() 242 | 243 | 244 | def test_bundler_can_filter_dependency_groups( 245 | io: BufferedIO, tmpdir: str, poetry: Poetry, mocker: MockerFixture, config: Config 246 | ) -> None: 247 | poetry = Factory().create_poetry( 248 | Path(__file__).parent.parent / "fixtures" / "simple_project_with_dev_dep" 249 | ) 250 | poetry.set_config(config) 251 | 252 | # foo is in the main dependency group 253 | # bar is a dev dependency 254 | # add a repository for foo but not bar 255 | pool = RepositoryPool() 256 | repository = Repository("repo") 257 | repository.add_package(Package("foo", "1.0.0")) 258 | pool.add_repository(repository) 259 | poetry.set_pool(pool) 260 | 261 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 262 | 263 | bundler = VenvBundler() 264 | bundler.set_path(Path(tmpdir)) 265 | bundler.set_remove(True) 266 | 267 | # This should fail because there is not repo for bar 268 | with pytest.raises(SolverProblemError): 269 | assert not bundler.bundle(poetry, io) 270 | 271 | bundler.set_activated_groups({MAIN_GROUP}) 272 | io.clear_output() 273 | 274 | # This succeeds because the dev dependency group is filtered out 275 | assert bundler.bundle(poetry, io) 276 | 277 | path = tmpdir 278 | expected = f"""\ 279 | • Bundling simple-project (1.2.3) into {path} 280 | • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 281 | • Bundling simple-project (1.2.3) into {path}: Installing dependencies 282 | • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) 283 | • Bundled simple-project (1.2.3) into {path} 284 | """ 285 | assert expected == io.fetch_output() 286 | 287 | 288 | @pytest.mark.parametrize("compile", [True, False]) 289 | def test_bundler_passes_compile_flag( 290 | io: BufferedIO, 291 | tmp_venv: VirtualEnv, 292 | poetry: Poetry, 293 | mocker: MockerFixture, 294 | compile: bool, 295 | ) -> None: 296 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 297 | 298 | bundler = VenvBundler() 299 | bundler.set_path(tmp_venv.path) 300 | bundler.set_remove(True) 301 | bundler.set_compile(compile) 302 | 303 | # bundle passes the flag from set_compile to enable_bytecode_compilation method 304 | mocker = mocker.patch( 305 | "poetry.installation.executor.Executor.enable_bytecode_compilation" 306 | ) 307 | 308 | assert bundler.bundle(poetry, io) 309 | 310 | mocker.assert_called_once_with(compile) 311 | 312 | path = str(tmp_venv.path) 313 | expected = f"""\ 314 | • Bundling simple-project (1.2.3) into {path} 315 | • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 316 | • Bundling simple-project (1.2.3) into {path}: Installing dependencies 317 | • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) 318 | • Bundled simple-project (1.2.3) into {path} 319 | """ 320 | assert expected == io.fetch_output() 321 | 322 | 323 | def test_bundler_editable_deps( 324 | io: BufferedIO, tmpdir: str, poetry: Poetry, mocker: MockerFixture, config: Config 325 | ) -> None: 326 | poetry = Factory().create_poetry( 327 | Path(__file__).parent.parent / "fixtures" / "simple_project_with_editable_dep" 328 | ) 329 | poetry.set_config(config) 330 | 331 | install_spy = mocker.spy(Install, "__init__") 332 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 333 | 334 | bundler = VenvBundler() 335 | bundler.set_path(Path(tmpdir)) 336 | 337 | io.clear_output() 338 | 339 | bundler.bundle(poetry, io) 340 | 341 | path = tmpdir 342 | expected = f"""\ 343 | • Bundling simple-project (1.2.3) into {path} 344 | • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 345 | • Bundling simple-project (1.2.3) into {path}: Installing dependencies 346 | • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) 347 | • Bundled simple-project (1.2.3) into {path} 348 | """ 349 | assert expected == io.fetch_output() 350 | 351 | installed_packages = [call.args[1] for call in install_spy.call_args_list] 352 | dep_installs = list( 353 | filter(lambda package: package.name == "bar", installed_packages) 354 | ) 355 | assert len(dep_installs) > 0 356 | 357 | editable_installs = list(filter(lambda package: package.develop, dep_installs)) 358 | assert len(editable_installs) == 0 359 | 360 | 361 | def test_bundler_should_build_a_venv_at_specified_path_if_centralized_venv_exists( 362 | io: BufferedIO, 363 | tmpdir: str, 364 | tmp_venv: VirtualEnv, 365 | poetry: Poetry, 366 | mocker: MockerFixture, 367 | ) -> None: 368 | """ 369 | Test coverage for [Issue #112](https://github.com/python-poetry/poetry-plugin-bundle/issues/112), which involves 370 | a pre-existing "centralized" venv at the path specified in the Poetry configuration. 371 | The test is intended to verify that the VenvBundler will build a new venv at the specified path if a centralized 372 | venv already exists. 373 | """ 374 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 375 | 376 | poetry.config.config["virtualenvs"]["in-project"] = False 377 | poetry.config.config["virtualenvs"]["path"] = tmp_venv.path 378 | 379 | env_manager = EnvManager(poetry) 380 | env_manager.activate(sys.executable) 381 | 382 | bundler_venv_path = Path(tmpdir) / "bundler" 383 | bundler = VenvBundler() 384 | bundler.set_path(bundler_venv_path) 385 | 386 | assert bundler.bundle(poetry, io) 387 | 388 | bundler_venv = VirtualEnv(bundler_venv_path) 389 | assert bundler_venv.is_sane() 390 | 391 | path = bundler_venv_path 392 | expected = f"""\ 393 | • Bundling simple-project (1.2.3) into {path} 394 | • Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 395 | • Bundling simple-project (1.2.3) into {path}: Installing dependencies 396 | • Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3) 397 | • Bundled simple-project (1.2.3) into {path} 398 | """ 399 | assert expected == io.fetch_output() 400 | 401 | 402 | def test_bundler_non_package_mode( 403 | io: BufferedIO, tmp_venv: VirtualEnv, mocker: MockerFixture, config: Config 404 | ) -> None: 405 | poetry = Factory().create_poetry( 406 | Path(__file__).parent.parent / "fixtures" / "non_package_mode" 407 | ) 408 | poetry.set_config(config) 409 | 410 | mocker.patch("poetry.installation.executor.Executor._execute_operation") 411 | 412 | bundler = VenvBundler() 413 | bundler.set_path(tmp_venv.path) 414 | bundler.set_remove(True) 415 | 416 | assert bundler.bundle(poetry, io) 417 | 418 | path = str(tmp_venv.path) 419 | expected = f"""\ 420 | • Bundling simple-project-non-package-mode (1.2.3) into {path} 421 | • Bundling simple-project-non-package-mode (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python 422 | • Bundling simple-project-non-package-mode (1.2.3) into {path}: Installing dependencies 423 | • Bundling simple-project-non-package-mode (1.2.3) into {path}: Skipping installation for non package project simple-project-non-package-mode 424 | • Bundled simple-project-non-package-mode (1.2.3) into {path} 425 | """ 426 | assert expected == io.fetch_output() 427 | 428 | 429 | def test_bundler_platform_override( 430 | io: BufferedIO, tmpdir: str, mocker: MockerFixture, config: Config 431 | ) -> None: 432 | poetry = Factory().create_poetry( 433 | Path(__file__).parent.parent / "fixtures" / "project_with_binary_wheel" 434 | ) 435 | poetry.set_config(config) 436 | 437 | def get_links_fake(package: Package) -> list[Link]: 438 | return [Link(f"https://example.com/{file['file']}") for file in package.files] 439 | 440 | mocker.patch( 441 | "poetry.installation.chooser.Chooser._get_links", side_effect=get_links_fake 442 | ) 443 | mocker.patch("poetry.installation.executor.Executor._execute_uninstall") 444 | mocker.patch("poetry.installation.executor.Executor._execute_update") 445 | mock_download_link = mocker.patch( 446 | "poetry.installation.executor.Executor._download_link" 447 | ) 448 | mocker.patch("poetry.installation.wheel_installer.WheelInstaller.install") 449 | 450 | def get_installed_links() -> dict[str, str]: 451 | return { 452 | call[0][0].package.name: call[0][1].filename 453 | for call in mock_download_link.call_args_list 454 | } 455 | 456 | bundler = VenvBundler() 457 | bundler.set_path(Path(tmpdir)) 458 | bundler.set_remove(True) 459 | 460 | bundler.set_platform("manylinux_2_28_x86_64") 461 | bundler.bundle(poetry, io) 462 | installed_link_by_package = get_installed_links() 463 | assert "manylinux_2_28_x86_64" in installed_link_by_package["cryptography"] 464 | assert "manylinux_2_17_x86_64" in installed_link_by_package["cffi"] 465 | assert "py3-none-any.whl" in installed_link_by_package["pycparser"] 466 | 467 | bundler.set_platform("manylinux2014_x86_64") 468 | bundler.bundle(poetry, io) 469 | installed_link_by_package = get_installed_links() 470 | assert "manylinux2014_x86_64" in installed_link_by_package["cryptography"] 471 | assert "manylinux_2_17_x86_64" in installed_link_by_package["cffi"] 472 | assert "py3-none-any.whl" in installed_link_by_package["pycparser"] 473 | 474 | bundler.set_platform("macosx_10_9_x86_64") 475 | bundler.bundle(poetry, io) 476 | installed_link_by_package = get_installed_links() 477 | assert "macosx_10_9_universal2" in installed_link_by_package["cryptography"] 478 | assert "cffi-2.0.0.tar.gz" in installed_link_by_package["cffi"] 479 | assert "py3-none-any.whl" in installed_link_by_package["pycparser"] 480 | 481 | bundler.set_platform("macosx_11_0_arm64") 482 | bundler.bundle(poetry, io) 483 | installed_link_by_package = get_installed_links() 484 | assert "macosx_10_9_universal2" in installed_link_by_package["cryptography"] 485 | assert "macosx_11_0_arm64" in installed_link_by_package["cffi"] 486 | assert "py3-none-any.whl" in installed_link_by_package["pycparser"] 487 | 488 | bundler.set_platform("musllinux_1_2_aarch64") 489 | bundler.bundle(poetry, io) 490 | installed_link_by_package = get_installed_links() 491 | assert "musllinux_1_2_aarch64" in installed_link_by_package["cryptography"] 492 | assert "musllinux_1_2_aarch64" in installed_link_by_package["cffi"] 493 | assert "py3-none-any.whl" in installed_link_by_package["pycparser"] 494 | --------------------------------------------------------------------------------