diff --git a/docs/cli.md b/docs/cli.md
index f77d2f851e7..5ea0b16051f 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -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
@@ -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.
diff --git a/src/poetry/console/commands/build.py b/src/poetry/console/commands/build.py
index 0d1bd15af45..86626fcf64a 100644
--- a/src/poetry/console/commands/build.py
+++ b/src/poetry/console/commands/build.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from pathlib import Path
from cleo.helpers import option
@@ -8,16 +8,19 @@
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 = [
@@ -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 {package.pretty_name} ({package.version})"
)
- 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
diff --git a/src/poetry/console/commands/publish.py b/src/poetry/console/commands/publish.py
index a53df8208dd..18d4cb0f456 100644
--- a/src/poetry/console/commands/publish.py
+++ b/src/poetry/console/commands/publish.py
@@ -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(
@@ -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"):
@@ -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:
diff --git a/src/poetry/publishing/publisher.py b/src/poetry/publishing/publisher.py
index a5335258041..a009e7ab9af 100644
--- a/src/poetry/publishing/publisher.py
+++ b/src/poetry/publishing/publisher.py
@@ -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
diff --git a/src/poetry/publishing/uploader.py b/src/poetry/publishing/uploader.py
index e9b22561b92..972e376e0bf 100644
--- a/src/poetry/publishing/uploader.py
+++ b/src/poetry/publishing/uploader.py
@@ -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
@@ -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)
@@ -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"
diff --git a/tests/console/commands/test_build.py b/tests/console/commands/test_build.py
index 3b378d1e4e5..023e855ee59 100644
--- a/tests/console/commands/test_build.py
+++ b/tests/console/commands/test_build.py
@@ -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"
@@ -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)
diff --git a/tests/console/commands/test_publish.py b/tests/console/commands/test_publish.py
index 5f230c01ff3..e123a082c9a 100644
--- a/tests/console/commands/test_publish.py
+++ b/tests/console/commands/test_publish.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import shutil
+
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
@@ -7,6 +9,7 @@
import pytest
import requests
+from poetry.factory import Factory
from poetry.publishing.uploader import UploadError
@@ -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(
@@ -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
diff --git a/tests/fixtures/with_multiple_dist_dir/README.rst b/tests/fixtures/with_multiple_dist_dir/README.rst
new file mode 100644
index 00000000000..f7fe15470f9
--- /dev/null
+++ b/tests/fixtures/with_multiple_dist_dir/README.rst
@@ -0,0 +1,2 @@
+My Package
+==========
diff --git a/tests/fixtures/with_multiple_dist_dir/dist/simple_project-1.2.3-py2.py3-none-any.whl b/tests/fixtures/with_multiple_dist_dir/dist/simple_project-1.2.3-py2.py3-none-any.whl
new file mode 100644
index 00000000000..fcdeda31338
Binary files /dev/null and b/tests/fixtures/with_multiple_dist_dir/dist/simple_project-1.2.3-py2.py3-none-any.whl differ
diff --git a/tests/fixtures/with_multiple_dist_dir/dist/simple_project-1.2.3.tar.gz b/tests/fixtures/with_multiple_dist_dir/dist/simple_project-1.2.3.tar.gz
new file mode 100644
index 00000000000..149aa9527c5
Binary files /dev/null and b/tests/fixtures/with_multiple_dist_dir/dist/simple_project-1.2.3.tar.gz differ
diff --git a/tests/fixtures/with_multiple_dist_dir/other_dist/dist/simple_project-1.2.3-py2.py3-none-any.whl b/tests/fixtures/with_multiple_dist_dir/other_dist/dist/simple_project-1.2.3-py2.py3-none-any.whl
new file mode 100644
index 00000000000..fcdeda31338
Binary files /dev/null and b/tests/fixtures/with_multiple_dist_dir/other_dist/dist/simple_project-1.2.3-py2.py3-none-any.whl differ
diff --git a/tests/fixtures/with_multiple_dist_dir/other_dist/dist/simple_project-1.2.3.tar.gz b/tests/fixtures/with_multiple_dist_dir/other_dist/dist/simple_project-1.2.3.tar.gz
new file mode 100644
index 00000000000..149aa9527c5
Binary files /dev/null and b/tests/fixtures/with_multiple_dist_dir/other_dist/dist/simple_project-1.2.3.tar.gz differ
diff --git a/tests/fixtures/with_multiple_dist_dir/pyproject.toml b/tests/fixtures/with_multiple_dist_dir/pyproject.toml
new file mode 100644
index 00000000000..45a61d43cad
--- /dev/null
+++ b/tests/fixtures/with_multiple_dist_dir/pyproject.toml
@@ -0,0 +1,35 @@
+[tool.poetry]
+name = "simple-project"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Sébastien Eustace "
+]
+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"
diff --git a/tests/fixtures/with_multiple_dist_dir/simple_project/__init__.py b/tests/fixtures/with_multiple_dist_dir/simple_project/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d