diff --git a/docs/docs/cli.md b/docs/docs/cli.md
index 63d63fbc990..94b99d34b36 100644
--- a/docs/docs/cli.md
+++ b/docs/docs/cli.md
@@ -325,6 +325,7 @@ It can also build the package if you pass it the `--build` option.
Should match a repository name set by the [`config`](#config) command.
* `--username (-u)`: The username to access the repository.
* `--password (-p)`: The password to access the repository.
+* `--dry-run`: Perform all actions except upload the package.
## config
diff --git a/poetry/console/commands/publish.py b/poetry/console/commands/publish.py
index 7fcbefcf73d..557cd1d7ab7 100644
--- a/poetry/console/commands/publish.py
+++ b/poetry/console/commands/publish.py
@@ -26,6 +26,7 @@ class PublishCommand(Command):
flag=False,
),
option("build", None, "Build the package before publishing."),
+ option("dry-run", None, "Perform all actions except upload the package."),
]
help = """The publish command builds and uploads the package to a remote repository.
@@ -79,4 +80,5 @@ def handle(self):
self.option("password"),
cert,
client_cert,
+ self.option("dry-run"),
)
diff --git a/poetry/publishing/publisher.py b/poetry/publishing/publisher.py
index b62b3addbe6..2ca9164f324 100644
--- a/poetry/publishing/publisher.py
+++ b/poetry/publishing/publisher.py
@@ -26,7 +26,15 @@ def __init__(self, poetry, io):
def files(self):
return self._uploader.files
- def publish(self, repository_name, username, password, cert=None, client_cert=None):
+ def publish(
+ self,
+ repository_name,
+ username,
+ password,
+ cert=None,
+ client_cert=None,
+ dry_run=False,
+ ):
if repository_name:
self._io.write_line(
"Publishing {} ({}) "
@@ -90,4 +98,5 @@ def publish(self, repository_name, username, password, cert=None, client_cert=No
url,
cert=cert or get_cert(self._poetry.config, repository_name),
client_cert=resolved_client_cert,
+ dry_run=dry_run,
)
diff --git a/poetry/publishing/uploader.py b/poetry/publishing/uploader.py
index 5c5ffad912f..133a471b572 100644
--- a/poetry/publishing/uploader.py
+++ b/poetry/publishing/uploader.py
@@ -95,8 +95,8 @@ def is_authenticated(self):
return self._username is not None and self._password is not None
def upload(
- self, url, cert=None, client_cert=None
- ): # type: (str, Optional[Path], Optional[Path]) -> None
+ self, url, cert=None, client_cert=None, dry_run=False
+ ): # type: (str, Optional[Path], Optional[Path], bool) -> None
session = self.make_session()
if cert:
@@ -106,7 +106,7 @@ def upload(
session.cert = str(client_cert)
try:
- self._upload(session, url)
+ self._upload(session, url, dry_run)
finally:
session.close()
@@ -188,9 +188,9 @@ def post_data(self, file):
return data
- def _upload(self, session, url):
+ def _upload(self, session, url, dry_run=False):
try:
- self._do_upload(session, url)
+ self._do_upload(session, url, dry_run)
except HTTPError as e:
if (
e.response.status_code == 400
@@ -203,15 +203,16 @@ def _upload(self, session, url):
raise UploadError(e)
- def _do_upload(self, session, url):
+ def _do_upload(self, session, url, dry_run=False):
for file in self.files:
# TODO: Check existence
- resp = self._upload_file(session, url, file)
+ resp = self._upload_file(session, url, file, dry_run)
- resp.raise_for_status()
+ if not dry_run:
+ resp.raise_for_status()
- def _upload_file(self, session, url, file):
+ def _upload_file(self, session, url, file, dry_run=False):
data = self.post_data(file)
data.update(
{
@@ -238,14 +239,17 @@ def _upload_file(self, session, url, file):
bar.start()
- resp = session.post(
- url,
- data=monitor,
- allow_redirects=False,
- headers={"Content-Type": monitor.content_type},
- )
+ resp = None
+
+ if not dry_run:
+ resp = session.post(
+ url,
+ data=monitor,
+ allow_redirects=False,
+ headers={"Content-Type": monitor.content_type},
+ )
- if resp.ok:
+ if dry_run or resp.ok:
bar.set_format(
" - Uploading {0} %percent%%>".format(
file.name
diff --git a/tests/console/commands/test_publish.py b/tests/console/commands/test_publish.py
index 5415694fa3c..3ae6b3e87ff 100644
--- a/tests/console/commands/test_publish.py
+++ b/tests/console/commands/test_publish.py
@@ -27,7 +27,7 @@ def test_publish_with_cert(app_tester, mocker):
app_tester.execute("publish --cert path/to/ca.pem")
assert [
- (None, None, None, Path("path/to/ca.pem"), None)
+ (None, None, None, Path("path/to/ca.pem"), None, False)
] == publisher_publish.call_args
@@ -36,5 +36,22 @@ def test_publish_with_client_cert(app_tester, mocker):
app_tester.execute("publish --client-cert path/to/client.pem")
assert [
- (None, None, None, None, Path("path/to/client.pem"))
+ (None, None, None, None, Path("path/to/client.pem"), False)
] == publisher_publish.call_args
+
+
+def test_publish_dry_run(app_tester, http):
+ http.register_uri(
+ http.POST, "https://upload.pypi.org/legacy/", status=403, body="Forbidden"
+ )
+
+ exit_code = app_tester.execute("publish --dry-run --username foo --password bar")
+
+ assert 0 == exit_code
+
+ output = app_tester.io.fetch_output()
+ error = app_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/publishing/test_publisher.py b/tests/publishing/test_publisher.py
index 8ec4ac261b9..da376120645 100644
--- a/tests/publishing/test_publisher.py
+++ b/tests/publishing/test_publisher.py
@@ -23,7 +23,7 @@ def test_publish_publishes_to_pypi_by_default(fixture_dir, mocker, config):
assert [("foo", "bar")] == uploader_auth.call_args
assert [
("https://upload.pypi.org/legacy/",),
- {"cert": None, "client_cert": None},
+ {"cert": None, "client_cert": None, "dry_run": False},
] == uploader_upload.call_args
@@ -45,7 +45,7 @@ def test_publish_can_publish_to_given_repository(fixture_dir, mocker, config):
assert [("foo", "bar")] == uploader_auth.call_args
assert [
("http://foo.bar",),
- {"cert": None, "client_cert": None},
+ {"cert": None, "client_cert": None, "dry_run": False},
] == uploader_upload.call_args
@@ -74,7 +74,7 @@ def test_publish_uses_token_if_it_exists(fixture_dir, mocker, config):
assert [("__token__", "my-token")] == uploader_auth.call_args
assert [
("https://upload.pypi.org/legacy/",),
- {"cert": None, "client_cert": None},
+ {"cert": None, "client_cert": None, "dry_run": False},
] == uploader_upload.call_args
@@ -98,7 +98,7 @@ def test_publish_uses_cert(fixture_dir, mocker, config):
assert [("foo", "bar")] == uploader_auth.call_args
assert [
("https://foo.bar",),
- {"cert": Path(cert), "client_cert": None},
+ {"cert": Path(cert), "client_cert": None, "dry_run": False},
] == uploader_upload.call_args
@@ -119,7 +119,7 @@ def test_publish_uses_client_cert(fixture_dir, mocker, config):
assert [
("https://foo.bar",),
- {"cert": None, "client_cert": Path(client_cert)},
+ {"cert": None, "client_cert": Path(client_cert), "dry_run": False},
] == uploader_upload.call_args
@@ -137,5 +137,5 @@ def test_publish_read_from_environment_variable(fixture_dir, environ, mocker, co
assert [("bar", "baz")] == uploader_auth.call_args
assert [
("https://foo.bar",),
- {"cert": None, "client_cert": None},
+ {"cert": None, "client_cert": None, "dry_run": False},
] == uploader_upload.call_args