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 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_cert
from poetry.utils.helpers import get_client_cert from poetry.utils.helpers import get_client_cert
from poetry.utils.password_manager import PasswordManager from poetry.utils.password_manager import PasswordManager
...@@ -34,24 +37,7 @@ class Publisher: ...@@ -34,24 +37,7 @@ class Publisher:
cert=None, cert=None,
client_cert=None, client_cert=None,
dry_run=False, dry_run=False,
): ): # type: (Optional[str], Optional[str], Optional[str], Optional[Path], Optional[Path], Optional[bool]) -> None
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
)
)
if not repository_name: if not repository_name:
url = "https://upload.pypi.org/legacy/" url = "https://upload.pypi.org/legacy/"
repository_name = "pypi" repository_name = "pypi"
...@@ -89,12 +75,22 @@ class Publisher: ...@@ -89,12 +75,22 @@ class Publisher:
if username is None: if username is None:
username = self._io.ask("Username:") 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:") password = self._io.ask_hidden("Password:")
self._uploader.auth(username, 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, url,
cert=cert or get_cert(self._poetry.config, repository_name), cert=cert or get_cert(self._poetry.config, repository_name),
client_cert=resolved_client_cert, client_cert=resolved_client_cert,
......
import hashlib import hashlib
import io import io
import math
from typing import Any
from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Union
import requests import requests
from requests import adapters from requests import adapters
from requests.exceptions import ConnectionError
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from requests.packages.urllib3 import util from requests.packages.urllib3 import util
from requests_toolbelt import user_agent from requests_toolbelt import user_agent
...@@ -27,12 +30,19 @@ _has_blake2 = hasattr(hashlib, "blake2b") ...@@ -27,12 +30,19 @@ _has_blake2 = hasattr(hashlib, "blake2b")
class UploadError(Exception): class UploadError(Exception):
def __init__(self, error): # type: (HTTPError) -> None def __init__(self, error): # type: (Union[ConnectionError, HTTPError]) -> None
super(UploadError, self).__init__( if isinstance(error, HTTPError):
"HTTP Error {}: {}".format( message = "HTTP Error {}: {}".format(
error.response.status_code, error.response.reason 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: class Uploader:
...@@ -59,7 +69,7 @@ class Uploader: ...@@ -59,7 +69,7 @@ class Uploader:
return adapters.HTTPAdapter(max_retries=retry) return adapters.HTTPAdapter(max_retries=retry)
@property @property
def files(self): # type: () -> List[str] def files(self): # type: () -> List[Path]
dist = self._poetry.file.parent / "dist" dist = self._poetry.file.parent / "dist"
version = normalize_version(self._package.version.text) version = normalize_version(self._package.version.text)
...@@ -80,7 +90,7 @@ class Uploader: ...@@ -80,7 +90,7 @@ class Uploader:
self._username = username self._username = username
self._password = password self._password = password
def make_session(self): def make_session(self): # type: () -> requests.Session
session = requests.session() session = requests.session()
if self.is_authenticated(): if self.is_authenticated():
session.auth = (self._username, self._password) session.auth = (self._username, self._password)
...@@ -110,7 +120,7 @@ class Uploader: ...@@ -110,7 +120,7 @@ class Uploader:
finally: finally:
session.close() session.close()
def post_data(self, file): def post_data(self, file): # type: (Path) -> Dict[str, Any]
meta = Metadata.from_package(self._package) meta = Metadata.from_package(self._package)
file_type = self._get_type(file) file_type = self._get_type(file)
...@@ -188,7 +198,9 @@ class Uploader: ...@@ -188,7 +198,9 @@ class Uploader:
return data 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: try:
self._do_upload(session, url, dry_run) self._do_upload(session, url, dry_run)
except HTTPError as e: except HTTPError as e:
...@@ -203,7 +215,9 @@ class Uploader: ...@@ -203,7 +215,9 @@ class Uploader:
raise UploadError(e) 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: for file in self.files:
# TODO: Check existence # TODO: Check existence
...@@ -212,7 +226,9 @@ class Uploader: ...@@ -212,7 +226,9 @@ class Uploader:
if not dry_run: if not dry_run:
resp.raise_for_status() 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 = self.post_data(file)
data.update( data.update(
{ {
...@@ -241,6 +257,7 @@ class Uploader: ...@@ -241,6 +257,7 @@ class Uploader:
resp = None resp = None
try:
if not dry_run: if not dry_run:
resp = session.post( resp = session.post(
url, url,
...@@ -248,7 +265,6 @@ class Uploader: ...@@ -248,7 +265,6 @@ class Uploader:
allow_redirects=False, allow_redirects=False,
headers={"Content-Type": monitor.content_type}, headers={"Content-Type": monitor.content_type},
) )
if dry_run or resp.ok: if dry_run or resp.ok:
bar.set_format( bar.set_format(
" - Uploading <c1>{0}</c1> <fg=green>%percent%%</>".format( " - Uploading <c1>{0}</c1> <fg=green>%percent%%</>".format(
...@@ -256,21 +272,22 @@ class Uploader: ...@@ -256,21 +272,22 @@ class Uploader:
) )
) )
bar.finish() bar.finish()
except (requests.ConnectionError, requests.HTTPError) as e:
self._io.write_line("")
else:
if self._io.output.supports_ansi(): if self._io.output.supports_ansi():
self._io.overwrite( self._io.overwrite(
" - Uploading <c1>{0}</c1> <error>{1}%</>".format( " - Uploading <c1>{0}</c1> <error>{1}</>".format(
file.name, int(math.floor(bar._percent * 100)) file.name, "FAILED"
) )
) )
raise UploadError(e)
finally:
self._io.write_line("") self._io.write_line("")
return resp return resp
def _register(self, session, url): def _register(
self, session, url
): # type: (requests.Session, str) -> requests.Response
""" """
Register a package to a repository. Register a package to a repository.
""" """
......
import pytest import pytest
import requests
from poetry.publishing.uploader import UploadError
from poetry.utils._compat import PY36 from poetry.utils._compat import PY36
from poetry.utils._compat import Path from poetry.utils._compat import Path
...@@ -28,6 +30,23 @@ Publishing simple-project (1.2.3) to PyPI ...@@ -28,6 +30,23 @@ Publishing simple-project (1.2.3) to PyPI
assert expected in app_tester.io.fetch_output() 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( @pytest.mark.skipif(
PY36, reason="Improved error rendering is not available on Python <3.6" 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