Commit 7d50a8ed by Arun Babu Neelicattu

Handle connection errors when publishing

This change ensures connection errors are handled explicitly for
cases where there are network issues.

In addition to the above, this includes type hint updates and minor
refactor.
parent 4f5cc841
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 @@ class Publisher:
cert=None,
client_cert=None,
dry_run=False,
):
if repository_name:
self._io.write_line(
"Publishing <c1>{}</c1> (<c2>{}</c2>) "
"to <info>{}</info>".format(
self._package.pretty_name,
self._package.pretty_version,
repository_name,
)
)
else:
self._io.write_line(
"Publishing <c1>{}</c1> (<c2>{}</c2>) "
"to <info>PyPI</info>".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 @@ class Publisher:
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 <c1>{}</c1> (<c2>{}</c2>) "
"to <info>{}</info>".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,
......
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 @@ _has_blake2 = hasattr(hashlib, "blake2b")
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 @@ class Uploader:
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 @@ class Uploader:
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 @@ class Uploader:
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 @@ class Uploader:
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 @@ class Uploader:
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 @@ class Uploader:
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 @@ class Uploader:
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 <c1>{0}</c1> <fg=green>%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 <c1>{0}</c1> <fg=green>%percent%%</>".format(
file.name
)
)
bar.finish()
except (requests.ConnectionError, requests.HTTPError) as e:
if self._io.output.supports_ansi():
self._io.overwrite(
" - Uploading <c1>{0}</c1> <error>{1}%</>".format(
file.name, int(math.floor(bar._percent * 100))
" - Uploading <c1>{0}</c1> <error>{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.
"""
......
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 @@ Publishing simple-project (1.2.3) to PyPI
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"
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment