Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bundle command implementation #2682

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,48 @@ associated with a specific project.

See [Managing environments](/docs/managing-environments/) for more information about these commands.

## bundle

The `bundle` namespace regroups commands to bundle the current project
and its dependencies into various formats. These commands are particularly useful to deploy
Poetry-managed applications.

### bundle venv

The `bundle venv` command bundles the project and its dependencies into a virtual environment.

The following command

```bash
poetry bundle venv /path/to/environment
```

will bundle the project in the `/path/to/environment` directory by creating the virtual environment,
installing the dependencies and the current project inside it. If the directory does not exist,
it will be created automatically.

By default, the command uses the current Python executable to build the virtual environment.
If you want to use a different one, you can specify it with the `--python/-p` option:

```bash
poetry bundle venv /path/to/environment --python /full/path/to/python
poetry bundle venv /path/to/environment -p python3.8
poetry bundle venv /path/to/environment -p 3.8
```

!!!note

If the virtual environment already exists, two things can happen:

- **The python version of the virtual environment is the same as the main one**: the dependencies will be synced (updated or removed).
- **The python version of the virtual environment is different**: the virtual environment will be recreated from scratch.

You can also ensure that the virtual environment is recreated by using the `--clear` option:

```bash
poetry bundle venv /path/to/environment --clear
```

## cache

The `cache` command regroups sub commands to interact with Poetry's cache.
Expand Down
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added poetry/bundle/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions poetry/bundle/bundler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import TYPE_CHECKING
from typing import Optional


if TYPE_CHECKING:
from pathlib import Path
from typing import Optional

from clikit.api.io.io import IO

from poetry.poetry import Poetry


class Bundler(object):
@property
def name(self) -> str:
raise NotImplementedError()

def bundle(
self, poetry: "Poetry", io: "IO", path: "Path", executable: Optional[str] = None
) -> None:
raise NotImplementedError()
39 changes: 39 additions & 0 deletions poetry/bundle/bundler_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import TYPE_CHECKING
from typing import Dict
from typing import List

from .exceptions import BundlerManagerError


if TYPE_CHECKING:
from .bundler import Bundler


class BundlerManager(object):
def __init__(self) -> None:
from .venv_bundler import VenvBundler

self._bundlers: Dict[str, "Bundler"] = {}

# Register default bundlers
self.register_bundler(VenvBundler())

@property
def bundlers(self) -> List["Bundler"]:
return list(self._bundlers.values())

def bundler(self, name: str) -> "Bundler":
if name.lower() not in self._bundlers:
raise BundlerManagerError('The bundler "{}" does not exist.'.format(name))

return self._bundlers[name.lower()]

def register_bundler(self, bundler: "Bundler") -> "BundlerManager":
if bundler.name.lower() in self._bundlers:
raise BundlerManagerError(
'A bundler with the name "{}" already exists.'.format(bundler.name)
)

self._bundlers[bundler.name.lower()] = bundler

return self
3 changes: 3 additions & 0 deletions poetry/bundle/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class BundlerManagerError(Exception):

pass
198 changes: 198 additions & 0 deletions poetry/bundle/venv_bundler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import sys

from typing import TYPE_CHECKING
from typing import Optional
from typing import cast

from .bundler import Bundler


if TYPE_CHECKING:
from pathlib import Path
from typing import Optional

from cleo.io.io import IO
from cleo.io.outputs.section_output import SectionOutput # noqa

from poetry.poetry import Poetry


class VenvBundler(Bundler):
@property
def name(self): # type: () -> str
return "venv"

def bundle(
self,
poetry: "Poetry",
io: "IO",
path: "Path",
executable: Optional[str] = None,
remove: bool = False,
) -> bool:
from pathlib import Path

from cleo.io.null_io import NullIO

from poetry.core.masonry.builders.wheel import WheelBuilder
from poetry.core.masonry.utils.module import ModuleOrPackageNotFound
from poetry.core.packages.package import Package
from poetry.core.semver.version import Version
from poetry.installation.installer import Installer
from poetry.installation.operations.install import Install
from poetry.utils.env import EnvManager
from poetry.utils.env import SystemEnv
from poetry.utils.env import VirtualEnv
from poetry.utils.helpers import temporary_directory

warnings = []

manager = EnvManager(poetry)
if executable is not None:
executable, python_version = manager.get_executable_info(executable)
else:
version_info = SystemEnv(Path(sys.prefix)).get_version_info()
python_version = Version(*version_info[:3])

message = self._get_message(poetry, path)
if io.is_decorated() and not io.is_debug():
io = io.section()

io.write_line(message)

if path.exists():
env = VirtualEnv(path)
env_python_version = Version(*env.version_info[:3])
if not env.is_sane() or env_python_version != python_version or remove:
self._write(
io, message + ": <info>Removing existing virtual environment</info>"
)

manager.remove_venv(str(path))

self._write(
io,
message
+ ": <info>Creating a virtual environment using Python <b>{}</b></info>".format(
python_version
),
)

manager.build_venv(str(path), executable=executable)
else:
self._write(
io,
message
+ ": <info>Using existing virtual environment</info>".format(
python_version
),
)
else:
self._write(
io,
message
+ ": <info>Creating a virtual environment using Python <b>{}</b></info>".format(
python_version
),
)

manager.build_venv(str(path), executable=executable)

env = VirtualEnv(path)

self._write(
io,
message + ": <info>Installing dependencies</info>".format(python_version),
)

installer = Installer(
NullIO() if not io.is_debug() else io,
env,
poetry.package,
poetry.locker,
poetry.pool,
poetry.config,
)
installer.remove_untracked()
installer.use_executor(poetry.config.get("experimental.new-installer", False))

return_code = installer.run()
if return_code:
self._write(
io,
self._get_message(poetry, path, error=True)
+ ": <error>Failed</> at step <b>Installing dependencies</b>".format(
python_version
),
)
return False

self._write(
io,
message
+ ": <info>Installing <c1>{}</c1> (<b>{}</b>)</info>".format(
poetry.package.pretty_name, poetry.package.pretty_version
),
)

# Build a wheel of the project in a temporary directory
# and install it in the newly create virtual environment
with temporary_directory() as directory:
try:
wheel_name = WheelBuilder.make_in(poetry, directory=Path(directory))
wheel = Path(directory).joinpath(wheel_name)
package = Package(
poetry.package.name,
poetry.package.version,
source_type="file",
source_url=wheel,
)
installer.executor.execute([Install(package)])
except ModuleOrPackageNotFound:
warnings.append(
"The root package was not installed because no matching module or package was found."
)

self._write(io, self._get_message(poetry, path, done=True))

if warnings:
for warning in warnings:
io.write_line(
" <fg=yellow;options=bold>•</> <warning>{}</warning>".format(
warning
)
)

return True

def _get_message(
self, poetry: "Poetry", path: "Path", done: bool = False, error: bool = False
) -> str:
operation_color = "blue"

if error:
operation_color = "red"
elif done:
operation_color = "green"

verb = "Bundling"
if done:
verb = "<success>Bundled</success>"

return " <fg={};options=bold>•</> {} <c1>{}</c1> (<b>{}</b>) into <c2>{}</c2>".format(
operation_color,
verb,
poetry.package.pretty_name,
poetry.package.pretty_version,
path,
)

def _write(self, io: "IO", message: str) -> None:
from cleo.io.outputs.section_output import SectionOutput # noqa

if io.is_debug() or not io.is_decorated() or not isinstance(io, SectionOutput):
io.write_line(message)
return

io = cast(SectionOutput, io)
io.overwrite(message)
20 changes: 20 additions & 0 deletions poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def _load() -> Type[Command]:
"show",
"update",
"version",
# Bundle commands
"bundle venv",
# Cache commands
"cache clear",
"cache list",
Expand Down Expand Up @@ -89,6 +91,7 @@ def __init__(self) -> None:
dispatcher.add_listener(COMMAND, self.register_command_loggers)
dispatcher.add_listener(COMMAND, self.set_env)
dispatcher.add_listener(COMMAND, self.set_installer)
dispatcher.add_listener(COMMAND, self.configure_bundle_commands)
self.set_event_dispatcher(dispatcher)

command_loader = FactoryCommandLoader(
Expand Down Expand Up @@ -272,6 +275,23 @@ def set_installer(
installer.use_executor(poetry.config.get("experimental.new-installer", False))
command.set_installer(installer)

def configure_bundle_commands(
self, event: ConsoleCommandEvent, event_name: str, _: Any
) -> None:
from .commands.bundle.bundle_command import BundleCommand

command: BundleCommand = cast(BundleCommand, event.command)
if not isinstance(command, BundleCommand):
return

# If the command already has a bundler manager, do nothing
if command.bundler_manager is not None:
return

from poetry.bundle.bundler_manager import BundlerManager

command.set_bundler_manager(BundlerManager())


def main() -> int:
return Application().run()
Expand Down
Empty file.
Loading