diff --git a/poetry/publishing/publisher.py b/poetry/publishing/publisher.py index 0eb4a4bfe71..d30a9a47119 100644 --- a/poetry/publishing/publisher.py +++ b/poetry/publishing/publisher.py @@ -1,5 +1,8 @@ import logging +from typing import Optional + +from poetry.utils._compat import Path from poetry.utils.helpers import get_cert from poetry.utils.helpers import get_client_cert from poetry.utils.password_manager import PasswordManager @@ -34,24 +37,7 @@ def publish( cert=None, client_cert=None, dry_run=False, - ): - if repository_name: - self._io.write_line( - "Publishing {} ({}) " - "to {}".format( - self._package.pretty_name, - self._package.pretty_version, - repository_name, - ) - ) - else: - self._io.write_line( - "Publishing {} ({}) " - "to PyPI".format( - self._package.pretty_name, self._package.pretty_version - ) - ) - + ): # type: (Optional[str], Optional[str], Optional[str], Optional[Path], Optional[Path], Optional[bool]) -> None if not repository_name: url = "https://upload.pypi.org/legacy/" repository_name = "pypi" @@ -89,12 +75,22 @@ def publish( if username is None: username = self._io.ask("Username:") - if password is None: + # skip password input if no username is provided, assume unauthenticated + if username and password is None: password = self._io.ask_hidden("Password:") self._uploader.auth(username, password) - return self._uploader.upload( + self._io.write_line( + "Publishing {} ({}) " + "to {}".format( + self._package.pretty_name, + self._package.pretty_version, + {"pypi": "PyPI"}.get(repository_name, "PyPI"), + ) + ) + + self._uploader.upload( url, cert=cert or get_cert(self._poetry.config, repository_name), client_cert=resolved_client_cert, diff --git a/poetry/publishing/uploader.py b/poetry/publishing/uploader.py index 133a471b572..b817676f956 100644 --- a/poetry/publishing/uploader.py +++ b/poetry/publishing/uploader.py @@ -1,13 +1,16 @@ import hashlib import io -import math +from typing import Any +from typing import Dict from typing import List from typing import Optional +from typing import Union import requests from requests import adapters +from requests.exceptions import ConnectionError from requests.exceptions import HTTPError from requests.packages.urllib3 import util from requests_toolbelt import user_agent @@ -27,12 +30,19 @@ class UploadError(Exception): - def __init__(self, error): # type: (HTTPError) -> None - super(UploadError, self).__init__( - "HTTP Error {}: {}".format( + def __init__(self, error): # type: (Union[ConnectionError, HTTPError]) -> None + if isinstance(error, HTTPError): + message = "HTTP Error {}: {}".format( error.response.status_code, error.response.reason ) - ) + elif isinstance(error, ConnectionError): + message = ( + "Connection Error: We were unable to connect to the repository, " + "ensure the url is correct and can be reached." + ) + else: + message = str(error) + super(UploadError, self).__init__(message) class Uploader: @@ -59,7 +69,7 @@ def adapter(self): return adapters.HTTPAdapter(max_retries=retry) @property - def files(self): # type: () -> List[str] + def files(self): # type: () -> List[Path] dist = self._poetry.file.parent / "dist" version = normalize_version(self._package.version.text) @@ -80,7 +90,7 @@ def auth(self, username, password): self._username = username self._password = password - def make_session(self): + def make_session(self): # type: () -> requests.Session session = requests.session() if self.is_authenticated(): session.auth = (self._username, self._password) @@ -110,7 +120,7 @@ def upload( finally: session.close() - def post_data(self, file): + def post_data(self, file): # type: (Path) -> Dict[str, Any] meta = Metadata.from_package(self._package) file_type = self._get_type(file) @@ -188,7 +198,9 @@ def post_data(self, file): return data - def _upload(self, session, url, dry_run=False): + def _upload( + self, session, url, dry_run=False + ): # type: (requests.Session, str, Optional[bool]) -> None try: self._do_upload(session, url, dry_run) except HTTPError as e: @@ -203,7 +215,9 @@ def _upload(self, session, url, dry_run=False): raise UploadError(e) - def _do_upload(self, session, url, dry_run=False): + def _do_upload( + self, session, url, dry_run=False + ): # type: (requests.Session, str, Optional[bool]) -> None for file in self.files: # TODO: Check existence @@ -212,7 +226,9 @@ def _do_upload(self, session, url, dry_run=False): if not dry_run: resp.raise_for_status() - def _upload_file(self, session, url, file, dry_run=False): + def _upload_file( + self, session, url, file, dry_run=False + ): # type: (requests.Session, str, Path, Optional[bool]) -> requests.Response data = self.post_data(file) data.update( { @@ -241,36 +257,37 @@ def _upload_file(self, session, url, file, dry_run=False): resp = None - if not dry_run: - resp = session.post( - url, - data=monitor, - allow_redirects=False, - headers={"Content-Type": monitor.content_type}, - ) - - if dry_run or resp.ok: - bar.set_format( - " - Uploading {0} %percent%%".format( - file.name + try: + if not dry_run: + resp = session.post( + url, + data=monitor, + allow_redirects=False, + headers={"Content-Type": monitor.content_type}, ) - ) - bar.finish() - - self._io.write_line("") - else: + if dry_run or resp.ok: + bar.set_format( + " - Uploading {0} %percent%%".format( + file.name + ) + ) + bar.finish() + except (requests.ConnectionError, requests.HTTPError) as e: if self._io.output.supports_ansi(): self._io.overwrite( - " - Uploading {0} {1}%".format( - file.name, int(math.floor(bar._percent * 100)) + " - Uploading {0} {1}".format( + file.name, "FAILED" ) ) - + raise UploadError(e) + finally: self._io.write_line("") return resp - def _register(self, session, url): + def _register( + self, session, url + ): # type: (requests.Session, str) -> requests.Response """ Register a package to a repository. """ diff --git a/tests/console/commands/test_publish.py b/tests/console/commands/test_publish.py index 25e314a5cc1..dccc43ee87d 100644 --- a/tests/console/commands/test_publish.py +++ b/tests/console/commands/test_publish.py @@ -1,5 +1,7 @@ import pytest +import requests +from poetry.publishing.uploader import UploadError from poetry.utils._compat import PY36 from poetry.utils._compat import Path @@ -28,6 +30,23 @@ def test_publish_returns_non_zero_code_for_upload_errors(app, app_tester, http): assert expected in app_tester.io.fetch_output() +def test_publish_returns_non_zero_code_for_connection_errors(app, app_tester, http): + def request_callback(*_, **__): + raise requests.ConnectionError() + + http.register_uri( + http.POST, "https://upload.pypi.org/legacy/", body=request_callback + ) + + exit_code = app_tester.execute("publish --username foo --password bar") + + assert 1 == exit_code + + expected = str(UploadError(error=requests.ConnectionError())) + + assert expected in app_tester.io.fetch_output() + + @pytest.mark.skipif( PY36, reason="Improved error rendering is not available on Python <3.6" )