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"
)