Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle connection errors when publishing #2285

Merged
merged 1 commit into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions poetry/publishing/publisher.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -34,24 +37,7 @@ def publish(
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"
Expand Down Expand Up @@ -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 <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,
Expand Down
81 changes: 49 additions & 32 deletions poetry/publishing/uploader.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand All @@ -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(
{
Expand Down Expand Up @@ -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 <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.
"""
Expand Down
19 changes: 19 additions & 0 deletions tests/console/commands/test_publish.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"
)
Expand Down