Commit 90351406 by Sébastien Eustace

Fix error reporting when publishing fails

Adresses #742 and #239
parent 6fe7f54a
......@@ -220,6 +220,17 @@ webencodings = "*"
[[package]]
category = "dev"
description = "HTTP client mock for Python"
name = "httpretty"
optional = false
python-versions = "*"
version = "0.9.6"
[package.dependencies]
six = "*"
[[package]]
category = "dev"
description = "File identification library for Python"
name = "identify"
optional = false
......@@ -790,7 +801,7 @@ python-versions = ">=2.7"
version = "0.3.3"
[metadata]
content-hash = "1f1739517a0fb9f3dfdc2213a6bfc0516d94215cc613ba1e7f2e3361694009ee"
content-hash = "2ba54ce4f8fd1fde6aeb5ea8d6520859b94e530c5a64526d7da2ad1a30c24e0a"
python-versions = "~2.7 || ^3.4"
[metadata.hashes]
......@@ -817,6 +828,7 @@ functools32 = ["89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0
futures = ["51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"]
glob2 = ["f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6"]
html5lib = ["20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", "66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"]
httpretty = ["01b52d45077e702eda491f4fe75328d3468fd886aed5dcc530003e7b2b5939dc"]
identify = ["08826e68e39e7de53cc2ddd8f6228a4e463b4bacb20565e5301c3ec690e68d27", "2364e24a7699fea0dc910e90740adbab43eef3746eeea4e016029c34123ce66d"]
idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"]
importlib-metadata = ["a17ce1a8c7bff1e8674cb12c992375d8d0800c9190177ecf0ad93e0097224095", "b50191ead8c70adfa12495fba19ce6d75f2e0275c14c5a7beb653d6799b512bd"]
......
import hashlib
import io
import math
import re
from typing import List
......@@ -22,6 +23,15 @@ from ..metadata import Metadata
_has_blake2 = hasattr(hashlib, "blake2b")
class UploadError(Exception):
def __init__(self, error): # type: (HTTPError) -> None
super(UploadError, self).__init__(
"HTTP Error {}: {}".format(
error.response.status_code, error.response.reason
)
)
class Uploader:
def __init__(self, poetry, io):
self._poetry = poetry
......@@ -84,7 +94,7 @@ class Uploader:
def is_authenticated(self):
return self._username is not None and self._password is not None
def upload(self, url):
def upload(self, url: str) -> bool:
session = self.make_session()
try:
......@@ -170,23 +180,20 @@ class Uploader:
return data
def _upload(self, session, url):
def _upload(self, session, url) -> bool:
try:
self._do_upload(session, url)
except HTTPError as e:
if (
e.response.status_code not in (403, 400)
or e.response.status_code == 400
and "was ever registered" not in e.response.text
e.response.status_code == 400
and "was ever registered" in e.response.text
):
raise
try:
self._register(session, url)
except HTTPError as e:
raise UploadError(e)
# It may be the first time we publish the package
# We'll try to register it and go from there
try:
self._register(session, url)
except HTTPError:
raise
raise UploadError(e)
def _do_upload(self, session, url):
for file in self.files:
......@@ -235,7 +242,14 @@ class Uploader:
self._io.writeln("")
else:
self._io.overwrite("")
if self._io.output.is_decorated():
self._io.overwrite(
" - Uploading <info>{0}</> <error>{1}%</>".format(
file.name, int(math.floor(bar._percent * 100))
)
)
else:
self._io.writeln("")
return resp
......@@ -263,6 +277,8 @@ class Uploader:
headers={"Content-Type": encoder.content_type},
)
resp.raise_for_status()
return resp
def _prepare_data(self, data):
......
......@@ -60,6 +60,7 @@ black = { version = "^18.3-alpha.0", python = "^3.6" }
pre-commit = "^1.10"
tox = "^3.0"
pytest-sugar = "^0.9.2"
httpretty = "^0.9.6"
[tool.poetry.scripts]
......
import httpretty
from cleo.testers import ApplicationTester
@httpretty.activate
def test_publish_returns_non_zero_code_for_upload_errors(app, app_tester):
httpretty.register_uri(
httpretty.POST,
"https://upload.pypi.org/legacy/",
status=400,
body="Bad Request",
)
exit_code = app_tester.run(
[("command", "publish"), ("--username", "foo"), ("--password", "bar")]
)
assert 1 == exit_code
expected = """
Publishing simple-project (1.2.3) to PyPI
- Uploading simple-project-1.2.3.tar.gz 0%
- Uploading simple-project-1.2.3.tar.gz 100%
- Uploading simple-project-1.2.3.tar.gz 100%
[UploadError]
HTTP Error 400: Bad Request
"""
assert app_tester.get_display(True).startswith(expected)
import io
import os
import pytest
import shutil
......@@ -7,6 +8,9 @@ try:
except ImportError:
import urlparse
from cleo import ApplicationTester as BaseApplicationTester
from cleo.inputs import ListInput
from cleo.outputs import StreamOutput
from tomlkit import document
from poetry.config import Config as BaseConfig
......@@ -17,11 +21,33 @@ from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.utils._compat import Path
from poetry.utils.env import Env
from poetry.utils.env import MockEnv
from poetry.utils.toml_file import TomlFile
class ApplicationTester(BaseApplicationTester):
def run(self, input_, options=None):
options = options or {}
self._input = ListInput(input_)
if self._inputs:
self._input.set_stream(self._create_stream(self._inputs))
if "interactive" in options:
self._input.set_interactive(options["interactive"])
self._output = StreamOutput(io.BytesIO())
if "decorated" in options:
self._output.set_decorated(options["decorated"])
else:
self._output.set_decorated(False)
if "verbosity" in options:
self._output.set_verbosity(options["verbosity"])
return self._application.run(self._input, self._output)
@pytest.fixture()
def installer():
return NoopInstaller()
......@@ -135,6 +161,7 @@ class Poetry(BasePoetry):
self._local_config = local_config
self._locker = Locker(locker.lock.path, locker._local_config)
self._config = Config.create("config.toml")
self._auth_config = Config.create("auth.toml")
# Configure sources
self._pool = Pool()
......@@ -163,4 +190,12 @@ def poetry(repo):
@pytest.fixture
def app(poetry):
return Application(poetry)
app_ = Application(poetry)
app_.set_auto_exit(False)
return app_
@pytest.fixture
def app_tester(app):
return ApplicationTester(app)
import httpretty
import pytest
import shutil
from poetry.masonry.publishing.uploader import UploadError
from poetry.masonry.publishing.uploader import Uploader
from poetry.io import NullIO
from poetry.poetry import Poetry
from poetry.utils._compat import Path
fixtures_dir = Path(__file__).parent / "fixtures"
@pytest.fixture(autouse=True)
def setup():
clear_samples_dist()
yield
clear_samples_dist()
def clear_samples_dist():
for dist in fixtures_dir.glob("**/dist"):
if dist.is_dir():
shutil.rmtree(str(dist))
def project(name):
return Path(__file__).parent / "fixtures" / name
@httpretty.activate
def test_uploader_properly_handles_400_errors():
httpretty.register_uri(
httpretty.POST, "https://foo.com", status=400, body="Bad request"
)
uploader = Uploader(Poetry.create(project("complete")), NullIO())
with pytest.raises(UploadError) as e:
uploader.upload("https://foo.com")
assert "HTTP Error 400: Bad Request" == str(e.value)
@httpretty.activate
def test_uploader_properly_handles_403_errors():
httpretty.register_uri(
httpretty.POST, "https://foo.com", status=403, body="Unauthorized"
)
uploader = Uploader(Poetry.create(project("complete")), NullIO())
with pytest.raises(UploadError) as e:
uploader.upload("https://foo.com")
assert "HTTP Error 403: Forbidden" == str(e.value)
@httpretty.activate
def test_uploader_registers_for_appropriate_400_errors(mocker):
register = mocker.patch("poetry.masonry.publishing.uploader.Uploader._register")
httpretty.register_uri(
httpretty.POST,
"https://foo.com",
status=400,
body="No package was ever registered",
)
uploader = Uploader(Poetry.create(project("complete")), NullIO())
with pytest.raises(UploadError):
uploader.upload("https://foo.com")
register.assert_called_once()
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