Commit 90351406 by Sébastien Eustace

Fix error reporting when publishing fails

Adresses #742 and #239
parent 6fe7f54a
...@@ -220,6 +220,17 @@ webencodings = "*" ...@@ -220,6 +220,17 @@ webencodings = "*"
[[package]] [[package]]
category = "dev" 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" description = "File identification library for Python"
name = "identify" name = "identify"
optional = false optional = false
...@@ -790,7 +801,7 @@ python-versions = ">=2.7" ...@@ -790,7 +801,7 @@ python-versions = ">=2.7"
version = "0.3.3" version = "0.3.3"
[metadata] [metadata]
content-hash = "1f1739517a0fb9f3dfdc2213a6bfc0516d94215cc613ba1e7f2e3361694009ee" content-hash = "2ba54ce4f8fd1fde6aeb5ea8d6520859b94e530c5a64526d7da2ad1a30c24e0a"
python-versions = "~2.7 || ^3.4" python-versions = "~2.7 || ^3.4"
[metadata.hashes] [metadata.hashes]
...@@ -817,6 +828,7 @@ functools32 = ["89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0 ...@@ -817,6 +828,7 @@ functools32 = ["89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0
futures = ["51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"] futures = ["51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"]
glob2 = ["f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6"] glob2 = ["f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6"]
html5lib = ["20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", "66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"] html5lib = ["20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", "66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"]
httpretty = ["01b52d45077e702eda491f4fe75328d3468fd886aed5dcc530003e7b2b5939dc"]
identify = ["08826e68e39e7de53cc2ddd8f6228a4e463b4bacb20565e5301c3ec690e68d27", "2364e24a7699fea0dc910e90740adbab43eef3746eeea4e016029c34123ce66d"] identify = ["08826e68e39e7de53cc2ddd8f6228a4e463b4bacb20565e5301c3ec690e68d27", "2364e24a7699fea0dc910e90740adbab43eef3746eeea4e016029c34123ce66d"]
idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"]
importlib-metadata = ["a17ce1a8c7bff1e8674cb12c992375d8d0800c9190177ecf0ad93e0097224095", "b50191ead8c70adfa12495fba19ce6d75f2e0275c14c5a7beb653d6799b512bd"] importlib-metadata = ["a17ce1a8c7bff1e8674cb12c992375d8d0800c9190177ecf0ad93e0097224095", "b50191ead8c70adfa12495fba19ce6d75f2e0275c14c5a7beb653d6799b512bd"]
......
import hashlib import hashlib
import io import io
import math
import re import re
from typing import List from typing import List
...@@ -22,6 +23,15 @@ from ..metadata import Metadata ...@@ -22,6 +23,15 @@ from ..metadata import Metadata
_has_blake2 = hasattr(hashlib, "blake2b") _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: class Uploader:
def __init__(self, poetry, io): def __init__(self, poetry, io):
self._poetry = poetry self._poetry = poetry
...@@ -84,7 +94,7 @@ class Uploader: ...@@ -84,7 +94,7 @@ class Uploader:
def is_authenticated(self): def is_authenticated(self):
return self._username is not None and self._password is not None 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() session = self.make_session()
try: try:
...@@ -170,23 +180,20 @@ class Uploader: ...@@ -170,23 +180,20 @@ class Uploader:
return data return data
def _upload(self, session, url): def _upload(self, session, url) -> bool:
try: try:
self._do_upload(session, url) self._do_upload(session, url)
except HTTPError as e: except HTTPError as e:
if ( if (
e.response.status_code not in (403, 400) e.response.status_code == 400
or e.response.status_code == 400 and "was ever registered" in e.response.text
and "was ever registered" not in e.response.text
): ):
raise
# It may be the first time we publish the package
# We'll try to register it and go from there
try: try:
self._register(session, url) self._register(session, url)
except HTTPError: except HTTPError as e:
raise raise UploadError(e)
raise UploadError(e)
def _do_upload(self, session, url): def _do_upload(self, session, url):
for file in self.files: for file in self.files:
...@@ -235,7 +242,14 @@ class Uploader: ...@@ -235,7 +242,14 @@ class Uploader:
self._io.writeln("") self._io.writeln("")
else: 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 return resp
...@@ -263,6 +277,8 @@ class Uploader: ...@@ -263,6 +277,8 @@ class Uploader:
headers={"Content-Type": encoder.content_type}, headers={"Content-Type": encoder.content_type},
) )
resp.raise_for_status()
return resp return resp
def _prepare_data(self, data): def _prepare_data(self, data):
......
...@@ -60,6 +60,7 @@ black = { version = "^18.3-alpha.0", python = "^3.6" } ...@@ -60,6 +60,7 @@ black = { version = "^18.3-alpha.0", python = "^3.6" }
pre-commit = "^1.10" pre-commit = "^1.10"
tox = "^3.0" tox = "^3.0"
pytest-sugar = "^0.9.2" pytest-sugar = "^0.9.2"
httpretty = "^0.9.6"
[tool.poetry.scripts] [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 os
import pytest import pytest
import shutil import shutil
...@@ -7,6 +8,9 @@ try: ...@@ -7,6 +8,9 @@ try:
except ImportError: except ImportError:
import urlparse import urlparse
from cleo import ApplicationTester as BaseApplicationTester
from cleo.inputs import ListInput
from cleo.outputs import StreamOutput
from tomlkit import document from tomlkit import document
from poetry.config import Config as BaseConfig from poetry.config import Config as BaseConfig
...@@ -17,11 +21,33 @@ from poetry.packages import Locker as BaseLocker ...@@ -17,11 +21,33 @@ from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import Env
from poetry.utils.env import MockEnv from poetry.utils.env import MockEnv
from poetry.utils.toml_file import TomlFile 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() @pytest.fixture()
def installer(): def installer():
return NoopInstaller() return NoopInstaller()
...@@ -135,6 +161,7 @@ class Poetry(BasePoetry): ...@@ -135,6 +161,7 @@ class Poetry(BasePoetry):
self._local_config = local_config self._local_config = local_config
self._locker = Locker(locker.lock.path, locker._local_config) self._locker = Locker(locker.lock.path, locker._local_config)
self._config = Config.create("config.toml") self._config = Config.create("config.toml")
self._auth_config = Config.create("auth.toml")
# Configure sources # Configure sources
self._pool = Pool() self._pool = Pool()
...@@ -163,4 +190,12 @@ def poetry(repo): ...@@ -163,4 +190,12 @@ def poetry(repo):
@pytest.fixture @pytest.fixture
def app(poetry): 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