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