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

Add output option to build and dist-dir option to publish command #8828

Merged
merged 11 commits into from
Feb 10, 2024
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
Secrus marked this conversation as resolved.
Show resolved Hide resolved


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`.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally, we don't use backticks here (only in the docs), but I do not have strong feelings and did not found a reference for something else so 🤷

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))
radoering marked this conversation as resolved.
Show resolved Hide resolved

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