Skip to content

Commit

Permalink
refactor(cli): split out business logic into separate class
Browse files Browse the repository at this point in the history
Allows easier testing.
  • Loading branch information
finswimmer committed Jan 19, 2025
1 parent 534bd3b commit 32904fb
Showing 1 changed file with 125 additions and 78 deletions.
203 changes: 125 additions & 78 deletions src/poetry/console/commands/build.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import dataclasses

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
Expand All @@ -20,81 +22,25 @@

from build import DistributionType
from cleo.io.inputs.option import Option
from cleo.io.io import IO

from poetry.poetry import Poetry
from poetry.utils.env import Env

class BuildCommand(EnvCommand):
name = "build"
description = "Builds a package, as a tarball and a wheel by default."

options: ClassVar[list[Option]] = [
option("format", "f", "Limit the format to either sdist or wheel.", flag=False),
option(
"clean",
description="Clean output directory before building.",
flag=True,
),
option(
"local-version",
"l",
"Add or replace a local version label to the build. (<warning>Deprecated</warning>)",
flag=False,
),
option(
"output",
"o",
"Set output directory for build artifacts. Default is `dist`.",
default="dist",
flag=False,
),
option(
"config-settings",
description="Provide config settings that should be passed to backend in <key>=<value> format.",
flag=False,
multiple=True,
),
]

loggers: ClassVar[list[str]] = [
"poetry.core.masonry.builders.builder",
"poetry.core.masonry.builders.sdist",
"poetry.core.masonry.builders.wheel",
]

def _requested_formats(self) -> list[DistributionType]:
fmt = self.option("format") or "all"
formats: list[DistributionType]

if fmt in BUILD_FORMATS:
formats = [fmt] # type: ignore[list-item]
elif fmt == "all":
formats = list(BUILD_FORMATS.keys()) # type: ignore[arg-type]
else:
raise ValueError(f"Invalid format: {fmt}")
@dataclasses.dataclass(frozen=True)
class BuildOptions:
clean: bool
formats: list[DistributionType]
output: str
config_settings: dict[str, Any] = dataclasses.field(default_factory=dict)

return formats

def _config_settings(self) -> dict[str, str]:
config_settings = {}

if local_version_label := self.option("local-version"):
self.line_error(
f"<warning>`<fg=yellow;options=bold>--local-version</>` is deprecated."
f" Use `<fg=yellow;options=bold>--config-settings local-version={local_version_label}</>`"
f" instead.</warning>"
)
config_settings["local-version"] = local_version_label

for config_setting in self.option("config-settings"):
if "=" not in config_setting:
raise ValueError(
f"Invalid config setting format: {config_setting}. "
"Config settings must be in the format 'key=value'"
)

key, _, value = config_setting.partition("=")
config_settings[key] = value

return config_settings
class BuildHandler:
def __init__(self, poetry: Poetry, env: Env, io: IO) -> None:
self.poetry = poetry
self.env = env
self.io = io

def _build(
self,
Expand Down Expand Up @@ -167,32 +113,133 @@ def _get_builder(self) -> Callable[..., None]:

return self._build

def handle(self) -> int:
def build(self, options: BuildOptions) -> int:
if not self.poetry.is_package_mode:
self.line_error("Building a package is not possible in non-package mode.")
self.io.write_error_line(
"Building a package is not possible in non-package mode."
)
return 1

dist_dir = Path(self.option("output"))
dist_dir = Path(options.output)
package = self.poetry.package
self.line(
self.io.write_line(
f"Building <c1>{package.pretty_name}</c1> (<c2>{package.version}</c2>)"
)

if not dist_dir.is_absolute():
dist_dir = self.poetry.pyproject_path.parent / dist_dir

if self.option("clean"):
if options.clean:
remove_directory(path=dist_dir, force=True)

build = self._get_builder()

for fmt in self._requested_formats():
self.line(f"Building <info>{fmt}</info>")
for fmt in options.formats:
self.io.write_line(f"Building <info>{fmt}</info>")
build(
fmt,
executable=self.env.python,
target_dir=dist_dir,
config_settings=self._config_settings(),
config_settings=options.config_settings,
)

return 0


class BuildCommand(EnvCommand):
name = "build"
description = "Builds a package, as a tarball and a wheel by default."

options: ClassVar[list[Option]] = [
option("format", "f", "Limit the format to either sdist or wheel.", flag=False),
option(
"clean",
description="Clean output directory before building.",
flag=True,
),
option(
"local-version",
"l",
"Add or replace a local version label to the build. (<warning>Deprecated</warning>)",
flag=False,
),
option(
"output",
"o",
"Set output directory for build artifacts. Default is `dist`.",
default="dist",
flag=False,
),
option(
"config-settings",
description="Provide config settings that should be passed to backend in <key>=<value> format.",
flag=False,
multiple=True,
),
]

loggers: ClassVar[list[str]] = [
"poetry.core.masonry.builders.builder",
"poetry.core.masonry.builders.sdist",
"poetry.core.masonry.builders.wheel",
]

@staticmethod
def _prepare_config_settings(
local_version: str | None, config_settings: list[str] | None, io: IO
) -> dict[str, str]:
config_settings = config_settings or []
result = {}

if local_version:
io.write_error_line(
f"<warning>`<fg=yellow;options=bold>--local-version</>` is deprecated."
f" Use `<fg=yellow;options=bold>--config-settings local-version={local_version}</>`"
f" instead.</warning>"
)
result["local-version"] = local_version

for config_setting in config_settings:
if "=" not in config_setting:
raise ValueError(
f"Invalid config setting format: {config_setting}. "
"Config settings must be in the format 'key=value'"
)

key, _, value = config_setting.partition("=")
result[key] = value

return result

@staticmethod
def _prepare_formats(fmt: str | None) -> list[DistributionType]:
fmt = fmt or "all"
formats: list[DistributionType]

if fmt in BUILD_FORMATS:
formats = [fmt] # type: ignore[list-item]
elif fmt == "all":
formats = list(BUILD_FORMATS.keys()) # type: ignore[arg-type]
else:
raise ValueError(f"Invalid format: {fmt}")

return formats

def handle(self) -> int:
build_handler = BuildHandler(
poetry=self.poetry,
env=self.env,
io=self.io,
)
build_options = BuildOptions(
clean=self.option("clean"),
formats=self._prepare_formats(self.option("format")),
output=self.option("output"),
config_settings=self._prepare_config_settings(
local_version=self.option("local-version"),
config_settings=self.option("config-settings"),
io=self.io,
),
)

return build_handler.build(options=build_options)

0 comments on commit 32904fb

Please sign in to comment.