Skip to content

Commit

Permalink
Add a bundle venv command
Browse files Browse the repository at this point in the history
  • Loading branch information
sdispater committed Jul 17, 2020
1 parent d5a99f0 commit e991f59
Show file tree
Hide file tree
Showing 20 changed files with 677 additions and 52 deletions.
42 changes: 42 additions & 0 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,48 @@ associated with a specific project.

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

## bundle

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

### 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 scratched.

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
4 changes: 4 additions & 0 deletions poetry/bundle/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .bundler_manager import BundlerManager


bundler_manager = BundlerManager()
20 changes: 20 additions & 0 deletions poetry/bundle/bundler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import TYPE_CHECKING


if TYPE_CHECKING:
from typing import Optional

from clikit.api.io.io import IO
from poetry.poetry import Poetry
from poetry.utils._compat import Path


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

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

from .bundler import Bundler
from .exceptions import BundlerManagerError
from .venv_bundler import VenvBundler


class BundlerManager(object):
def __init__(self): # type: () -> None
self._bundlers = {} # type: Dict[str, Bundler]

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

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

def bundler(self, name): # type: (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): # type: (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
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
174 changes: 174 additions & 0 deletions poetry/bundle/venv_bundler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import sys

from typing import TYPE_CHECKING

from .bundler import Bundler


if TYPE_CHECKING:
from typing import Optional

from poetry.poetry import Poetry
from clikit.api.io.io import IO
from clikit.api.io.section_output import SectionOutput # noqa
from poetry.utils._compat import Path


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

def bundle(
self, poetry, io, path, executable=None, remove=False
): # type: (Poetry, IO, Path, Optional[str], bool) -> bool
from poetry.core.masonry.builders.wheel import WheelBuilder
from poetry.core.semver.version import Version
from poetry.installation.installer import Installer
from poetry.installation.operations.install import Install
from poetry.io.null_io import NullIO
from poetry.utils._compat import Path
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

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.supports_ansi() and not io.is_debug():
verbosity = io.verbosity
io = io.section()
io.set_verbosity(verbosity)

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:
wheel_name = WheelBuilder.make_in(poetry, directory=Path(directory))
wheel = Path(directory).joinpath(wheel_name)
package = poetry.package.clone()
package.source_type = "file"
package.source_url = wheel
installer.executor.execute([Install(package)])

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

return True

def _get_message(
self, poetry, path, done=False, error=False
): # type: (Poetry, Path, bool) -> 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, message): # type: (IO, str) -> None
from clikit.api.io.section_output import SectionOutput # noqa

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

io.output.clear()
io.write(message)
4 changes: 4 additions & 0 deletions poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .commands.about import AboutCommand
from .commands.add import AddCommand
from .commands.build import BuildCommand
from .commands.bundle import BundleCommand
from .commands.cache.cache import CacheCommand
from .commands.check import CheckCommand
from .commands.config import ConfigCommand
Expand Down Expand Up @@ -75,6 +76,9 @@ def get_default_commands(self): # type: () -> list
VersionCommand(),
]

# Bundle commands
commands += [BundleCommand()]

# Cache commands
commands += [CacheCommand()]

Expand Down
1 change: 1 addition & 0 deletions poetry/console/commands/bundle/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .bundle import BundleCommand
15 changes: 15 additions & 0 deletions poetry/console/commands/bundle/bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Optional

from ..command import Command
from .venv import BundleVenvCommand


class BundleCommand(Command):

name = "bundle"
description = "Bundle the current project."

commands = [BundleVenvCommand()]

def handle(self): # type: () -> Optional[int]
return self.call("help", self._config.name)
55 changes: 55 additions & 0 deletions poetry/console/commands/bundle/venv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from typing import Optional

from cleo import argument
from cleo import option

from poetry.utils._compat import Path

from ..command import Command


class BundleVenvCommand(Command):

name = "venv"
description = "Bundle the current project into a virtual environment"

arguments = [
argument("path", "The path to the virtual environment to bundle into."),
]

options = [
option(
"python",
"p",
"The Python executable to use to create the virtual environment. "
"Defaults to the current Python executable",
flag=False,
value_required=True,
),
option(
"clear",
None,
"Clear the existing virtual environment if it exists. ",
flag=True,
),
]

def handle(self): # type: () -> Optional[int]
from poetry.bundle import bundler_manager

path = Path(self.argument("path"))
executable = self.option("python")

bundler = bundler_manager.bundler("venv")

self.line("")

return int(
not bundler.bundle(
self.poetry,
self._io,
path,
executable=executable,
remove=self.option("clear"),
)
)
Loading

0 comments on commit e991f59

Please sign in to comment.