From bfeb859d7bb6a177f960aef3de48e18edd717422 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Thu, 19 Mar 2020 02:33:30 +0100 Subject: [PATCH] Add --dry-run option for publish command This change introduces `--dry-run` option for the publish command. When used, will perform all actions required for publishing except for uploading build artifacts. Resolves: #2181 --- docs/docs/cli.md | 1 + poetry/console/commands/publish.py | 2 ++ poetry/masonry/publishing/publisher.py | 11 ++++++- poetry/masonry/publishing/uploader.py | 36 ++++++++++++---------- tests/console/commands/test_publish.py | 21 +++++++++++-- tests/masonry/publishing/test_publisher.py | 10 +++--- 6 files changed, 57 insertions(+), 24 deletions(-) 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 edbdadb140d..e5205d4c082 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/masonry/publishing/publisher.py b/poetry/masonry/publishing/publisher.py index 3ff39a7c840..17b1a749cb7 100644 --- a/poetry/masonry/publishing/publisher.py +++ b/poetry/masonry/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 {} ({}) " @@ -94,4 +102,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/masonry/publishing/uploader.py b/poetry/masonry/publishing/uploader.py index 7e534cc30ae..afbc58635fb 100644 --- a/poetry/masonry/publishing/uploader.py +++ b/poetry/masonry/publishing/uploader.py @@ -96,8 +96,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: @@ -107,7 +107,7 @@ def upload( session.cert = str(client_cert) try: - self._upload(session, url) + self._upload(session, url, dry_run) finally: session.close() @@ -189,9 +189,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 @@ -204,15 +204,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( { @@ -239,14 +240,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 856878a57a4..c1e4594ef1d 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/masonry/publishing/test_publisher.py b/tests/masonry/publishing/test_publisher.py index 4058e255ae4..1bfdcfcb978 100644 --- a/tests/masonry/publishing/test_publisher.py +++ b/tests/masonry/publishing/test_publisher.py @@ -21,7 +21,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 @@ -43,7 +43,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 @@ -72,7 +72,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 @@ -96,7 +96,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 @@ -117,5 +117,5 @@ 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