Skip to content

Commit

Permalink
Add output option to build and dist-dir option to publish com…
Browse files Browse the repository at this point in the history
…mand (#8828)
  • Loading branch information
benjamin-callonnec committed Feb 10, 2024
1 parent 3e98fd8 commit b0f46c4
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 15 deletions.
2 changes: 2 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ Note that, at the moment, only pure python wheels are supported.
### Options

* `--format (-f)`: Limit the format to either `wheel` or `sdist`.
* `--output (-o)`: Set output directory for build artifacts. Default is `dist`.

## publish

Expand All @@ -560,6 +561,7 @@ Should match a repository name set by the [`config`](#config) command.
* `--password (-p)`: The password to access the repository.
* `--cert`: Certificate authority to access the repository.
* `--client-cert`: Client certificate to access the repository.
* `--dist-dir`: Dist directory where built artifact are stored. Default is `dist`.
* `--build`: Build the package before publishing.
* `--dry-run`: Perform all actions except upload the package.
* `--skip-existing`: Ignore errors from files already existing in the repository.
Expand Down
20 changes: 13 additions & 7 deletions src/poetry/console/commands/build.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from pathlib import Path

from cleo.helpers import option

from poetry.console.commands.env_command import EnvCommand
from poetry.utils.env import build_environment


if TYPE_CHECKING:
from pathlib import Path


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

options = [
option("format", "f", "Limit the format to either sdist or wheel.", flag=False)
option("format", "f", "Limit the format to either sdist or wheel.", flag=False),
option(
"output",
"o",
"Set output directory for build artifacts. Default is `dist`.",
default="dist",
flag=False,
),
]

loggers = [
Expand Down Expand Up @@ -48,11 +51,14 @@ def _build(
def handle(self) -> int:
with build_environment(poetry=self.poetry, env=self.env, io=self.io) as env:
fmt = self.option("format") or "all"
dist_dir = Path(self.option("output"))
package = self.poetry.package
self.line(
f"Building <c1>{package.pretty_name}</c1> (<c2>{package.version}</c2>)"
)

self._build(fmt, executable=env.python)
if not dist_dir.is_absolute():
dist_dir = self.poetry.pyproject_path.parent / dist_dir
self._build(fmt, executable=env.python, target_dir=dist_dir)

return 0
13 changes: 11 additions & 2 deletions src/poetry/console/commands/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ class PublishCommand(Command):
"Client certificate to access the repository.",
flag=False,
),
option(
"dist-dir",
None,
"Dist directory where built artifact are stored. Default is `dist`.",
default="dist",
flag=False,
),
option("build", None, "Build the package before publishing."),
option("dry-run", None, "Perform all actions except upload the package."),
option(
Expand All @@ -49,7 +56,9 @@ class PublishCommand(Command):
def handle(self) -> int:
from poetry.publishing.publisher import Publisher

publisher = Publisher(self.poetry, self.io)
dist_dir = self.option("dist-dir")

publisher = Publisher(self.poetry, self.io, Path(dist_dir))

# Building package first, if told
if self.option("build"):
Expand All @@ -61,7 +70,7 @@ def handle(self) -> int:

return 1

self.call("build")
self.call("build", args=f"--output {dist_dir}")

files = publisher.files
if not files:
Expand Down
4 changes: 2 additions & 2 deletions src/poetry/publishing/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ class Publisher:
Registers and publishes packages to remote repositories.
"""

def __init__(self, poetry: Poetry, io: IO) -> None:
def __init__(self, poetry: Poetry, io: IO, dist_dir: Path | None = None) -> None:
self._poetry = poetry
self._package = poetry.package
self._io = io
self._uploader = Uploader(poetry, io)
self._uploader = Uploader(poetry, io, dist_dir)
self._authenticator = Authenticator(poetry.config, self._io)

@property
Expand Down
18 changes: 15 additions & 3 deletions src/poetry/publishing/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ def __init__(self, error: ConnectionError | HTTPError | str) -> None:


class Uploader:
def __init__(self, poetry: Poetry, io: IO) -> None:
def __init__(self, poetry: Poetry, io: IO, dist_dir: Path | None = None) -> None:
self._poetry = poetry
self._package = poetry.package
self._io = io
self._dist_dir = dist_dir or self.default_dist_dir
self._username: str | None = None
self._password: str | None = None

Expand All @@ -61,9 +62,20 @@ def user_agent(self) -> str:
agent: str = user_agent("poetry", __version__)
return agent

@property
def default_dist_dir(self) -> Path:
return self._poetry.file.path.parent / "dist"

@property
def dist_dir(self) -> Path:
if not self._dist_dir.is_absolute():
return self._poetry.file.path.parent / self._dist_dir

return self._dist_dir

@property
def files(self) -> list[Path]:
dist = self._poetry.file.path.parent / "dist"
dist = self.dist_dir
version = self._package.version.to_string()
escaped_name = distribution_name(self._package.name)

Expand Down Expand Up @@ -275,7 +287,7 @@ def _register(self, session: requests.Session, url: str) -> requests.Response:
"""
Register a package to a repository.
"""
dist = self._poetry.file.path.parent / "dist"
dist = self.dist_dir
escaped_name = distribution_name(self._package.name)
file = dist / f"{escaped_name}-{self._package.version.to_string()}.tar.gz"

Expand Down
25 changes: 24 additions & 1 deletion tests/console/commands/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ def test_build_with_multiple_readme_files(

poetry = Factory().create_poetry(target_dir)
tester = command_tester_factory("build", poetry, environment=tmp_venv)

tester.execute()

build_dir = target_dir / "dist"
Expand All @@ -93,3 +92,27 @@ def test_build_with_multiple_readme_files(

assert "my_package-0.1/README-1.rst" in sdist_content
assert "my_package-0.1/README-2.rst" in sdist_content


@pytest.mark.parametrize(
"output_dir", [None, "dist", "test/dir", "../dist", "absolute"]
)
def test_build_output_option(
tmp_tester: CommandTester,
tmp_project_path: Path,
tmp_poetry: Poetry,
output_dir: str,
) -> None:
if output_dir is None:
tmp_tester.execute()
build_dir = tmp_project_path / "dist"
elif output_dir == "absolute":
tmp_tester.execute(f"--output {tmp_project_path / 'tmp/dist'}")
build_dir = tmp_project_path / "tmp/dist"
else:
tmp_tester.execute(f"--output {output_dir}")
build_dir = tmp_project_path / output_dir

build_artifacts = tuple(build_dir.glob(get_package_glob(tmp_poetry)))
assert len(build_artifacts) > 0
assert all(archive.exists() for archive in build_artifacts)
83 changes: 83 additions & 0 deletions tests/console/commands/test_publish.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import annotations

import shutil

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any

import pytest
import requests

from poetry.factory import Factory
from poetry.publishing.uploader import UploadError


Expand All @@ -16,7 +19,10 @@
from cleo.testers.application_tester import ApplicationTester
from pytest_mock import MockerFixture

from poetry.utils.env import VirtualEnv
from tests.helpers import PoetryTestApplication
from tests.types import CommandTesterFactory
from tests.types import FixtureDirGetter


def test_publish_returns_non_zero_code_for_upload_errors(
Expand Down Expand Up @@ -130,3 +136,80 @@ def test_skip_existing_output(

error = app_tester.io.fetch_error()
assert "- Uploading simple_project-1.2.3.tar.gz File exists. Skipping" in error


@pytest.mark.parametrize("dist_dir", [None, "dist", "other_dist/dist", "absolute"])
def test_publish_dist_dir_option(
http: type[httpretty.httpretty],
fixture_dir: FixtureDirGetter,
tmp_path: Path,
tmp_venv: VirtualEnv,
command_tester_factory: CommandTesterFactory,
dist_dir: str | None,
) -> None:
source_dir = fixture_dir("with_multiple_dist_dir")
target_dir = tmp_path / "project"
shutil.copytree(str(source_dir), str(target_dir))

http.register_uri(
http.POST, "https://upload.pypi.org/legacy/", status=409, body="Conflict"
)

poetry = Factory().create_poetry(target_dir)
tester = command_tester_factory("publish", poetry, environment=tmp_venv)

if dist_dir is None:
exit_code = tester.execute("--dry-run")
elif dist_dir == "absolute":
exit_code = tester.execute(f"--dist-dir {target_dir / 'dist'} --dry-run")
else:
exit_code = tester.execute(f"--dist-dir {dist_dir} --dry-run")

assert exit_code == 0

output = tester.io.fetch_output()
error = tester.io.fetch_error()

assert "Publishing simple-project (1.2.3) to PyPI" in output
assert "- Uploading simple_project-1.2.3.tar.gz" in error
assert "- Uploading simple_project-1.2.3-py2.py3-none-any.whl" in error


@pytest.mark.parametrize("dist_dir", ["../dist", "tmp/dist", "absolute"])
def test_publish_dist_dir_and_build_options(
http: type[httpretty.httpretty],
fixture_dir: FixtureDirGetter,
tmp_path: Path,
tmp_venv: VirtualEnv,
command_tester_factory: CommandTesterFactory,
dist_dir: str | None,
) -> None:
source_dir = fixture_dir("simple_project")
target_dir = tmp_path / "project"
shutil.copytree(str(source_dir), str(target_dir))

# Remove dist dir because as it will be built again
shutil.rmtree(target_dir / "dist")

http.register_uri(
http.POST, "https://upload.pypi.org/legacy/", status=409, body="Conflict"
)

poetry = Factory().create_poetry(target_dir)
tester = command_tester_factory("publish", poetry, environment=tmp_venv)

if dist_dir == "absolute":
exit_code = tester.execute(
f"--dist-dir {target_dir / 'test/dist'} --dry-run --build"
)
else:
exit_code = tester.execute(f"--dist-dir {dist_dir} --dry-run --build")

assert exit_code == 0

output = tester.io.fetch_output()
error = tester.io.fetch_error()

assert "Publishing simple-project (1.2.3) to PyPI" in output
assert "- Uploading simple_project-1.2.3.tar.gz" in error
assert "- Uploading simple_project-1.2.3-py2.py3-none-any.whl" in error
2 changes: 2 additions & 0 deletions tests/fixtures/with_multiple_dist_dir/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
My Package
==========
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35 changes: 35 additions & 0 deletions tests/fixtures/with_multiple_dist_dir/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[tool.poetry]
name = "simple-project"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"

readme = ["README.rst"]

homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"

keywords = ["packaging", "dependency", "poetry"]

classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]

# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.4"

[tool.poetry.scripts]
foo = "foo:bar"
baz = "bar:baz.boom.bim"
fox = "fuz.foo:bar.baz"


[build-system]
requires = ["poetry-core>=1.1.0a7"]
build-backend = "poetry.core.masonry.api"
Empty file.

0 comments on commit b0f46c4

Please sign in to comment.