diff --git a/.coveragerc b/.coveragerc index 89e464b4..5ac2d2f4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,13 +10,5 @@ exclude_lines = # Don't complain if non-runnable code isn't run if __name__ == .__main__.: - -# Paths to omit from consideration -omit = - # __main__.py exists only as a very basic wrapper around warehouse.cli - # and exists only to provide setuptools and python -m a place to point - # at. - */twine/__main__.py - [html] show_contexts = True diff --git a/tests/conftest.py b/tests/conftest.py index 630609a8..f497e9d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,7 +128,7 @@ def entered_password(monkeypatch): monkeypatch.setattr(getpass, "getpass", lambda prompt: "entered pw") -@pytest.fixture +@pytest.fixture(scope="session") def sampleproject_dist(tmp_path_factory): checkout = tmp_path_factory.mktemp("sampleproject", numbered=False) subprocess.run( diff --git a/tests/test_integration.py b/tests/test_integration.py index c39734ae..c499ab9c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,7 +1,10 @@ +import re import sys +import colorama import pytest +from twine import __main__ as dunder_main from twine import cli @@ -46,6 +49,31 @@ def test_pypi_upload(sampleproject_dist): cli.dispatch(command) +def test_pypi_error(sampleproject_dist, monkeypatch): + command = [ + "twine", + "upload", + "--repository-url", + "https://test.pypi.org/legacy/", + "--username", + "foo", + "--password", + "bar", + str(sampleproject_dist), + ] + monkeypatch.setattr(sys, "argv", command) + + message = ( + re.escape(colorama.Fore.RED) + + r"HTTPError: 403 Forbidden from https://test\.pypi\.org/legacy/\n" + + r".+?authentication" + ) + + result = dunder_main.main() + + assert re.match(message, result) + + @pytest.mark.xfail( sys.platform == "win32", reason="pytest-services watcher_getter fixture does not support Windows", diff --git a/twine/__main__.py b/twine/__main__.py index 3a19f9d1..fd0133e2 100644 --- a/twine/__main__.py +++ b/twine/__main__.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import http import sys from typing import Any @@ -24,9 +25,19 @@ def main() -> Any: try: - return cli.dispatch(sys.argv[1:]) - except (exceptions.TwineException, requests.HTTPError) as exc: - return _format_error(f"{exc.__class__.__name__}: {exc.args[0]}") + result = cli.dispatch(sys.argv[1:]) + except requests.HTTPError as exc: + status_code = exc.response.status_code + status_phrase = http.HTTPStatus(status_code).phrase + result = ( + f"{exc.__class__.__name__}: {status_code} {status_phrase} " + f"from {exc.response.url}\n" + f"{exc.response.reason}" + ) + except exceptions.TwineException as exc: + result = f"{exc.__class__.__name__}: {exc.args[0]}" + + return _format_error(result) if isinstance(result, str) else result def _format_error(message: str) -> str: