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

Add helpful error message for incorrect PyPI URL #509

Merged
merged 14 commits into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from 9 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: 36 additions & 0 deletions tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from twine.commands import upload
from twine import package, cli, exceptions
import twine
from twine.exceptions import (
InvalidPyPIUploadURL,
UploadToDeprecatedPyPIDetected,
)

import helpers

Expand Down Expand Up @@ -285,3 +289,35 @@ def none_upload(*args, **settings_kwargs):
assert "pypipassword" == upload_settings.password
assert "pypiuser" == upload_settings.username
assert "/foo/bar.crt" == upload_settings.cacert


@pytest.mark.parametrize('repo_url', [
"https://upload.pypi.org/",
"https://test.pypi.org/",
"https://pypi.org/"
])
def test_check_status_code_for_wrong_repo_url(repo_url, make_settings):
upload_settings = make_settings()

# override defaults to use incorrect URL
upload_settings.repository_config['repository'] = repo_url

with pytest.raises(InvalidPyPIUploadURL):
upload.upload(upload_settings, [
WHEEL_FIXTURE, SDIST_FIXTURE, NEW_SDIST_FIXTURE, NEW_WHEEL_FIXTURE
])


@pytest.mark.parametrize('repo_url', [
"https://pypi.python.org",
"https://testpypi.python.org"
])
def test_check_status_code_for_deprecated_pypi_url(repo_url):
response = pretend.stub(
status_code=410,
url=repo_url
)

# value of Verbose doesn't matter for this check
with pytest.raises(UploadToDeprecatedPyPIDetected):
upload.check_status_code(response, False)
40 changes: 38 additions & 2 deletions twine/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
from twine.package import PackageFile
from twine import exceptions
from twine import settings
from twine import utils
from twine.utils import DEFAULT_REPOSITORY, TEST_REPOSITORY

from requests.exceptions import HTTPError


def skip_upload(response, skip_existing, package):
Expand Down Expand Up @@ -48,6 +50,40 @@ def skip_upload(response, skip_existing, package):
(response.status_code == 403 and msg_403 in response.text)))


def check_status_code(response, verbose):
"""
Additional safety net to catch response code 410 in case the
UploadToDeprecatedPyPIDetected exception breaks.
Also includes a check for response code 405 and prints helpful error
message guiding users to the right repository endpoints.
"""
if response.status_code == 410 and "pypi.python.org" in response.url:
raise exceptions.UploadToDeprecatedPyPIDetected(
f"It appears you're uploading to pypi.python.org (or "
f"testpypi.python.org). You've received a 410 error response. "
f"Uploading to those sites is deprecated. The new sites are "
f"pypi.org and test.pypi.org. Try using {DEFAULT_REPOSITORY} (or "
f"{TEST_REPOSITORY}) to upload your packages instead. These are "
f"the default URLs for Twine now. More at "
f"https://packaging.python.org/guides/migrating-to-pypi-org/.")
elif response.status_code == 405 and "pypi.org" in response.url:
raise exceptions.InvalidPyPIUploadURL(
f"It appears you're trying to upload to pypi.org but have an "
f"invalid URL. You probably want one of these two URLs: "
f"{DEFAULT_REPOSITORY} or {TEST_REPOSITORY}. Check your "
f"--repository-url value.")
try:
response.raise_for_status()
except HTTPError as err:
if response.text:
if verbose:
print('Content received from server:\n{}'.format(
response.text))
else:
print('NOTE: Try --verbose to see response content.')
raise err


def upload(upload_settings, dists):
dists = _find_dists(dists)

Expand Down Expand Up @@ -99,7 +135,7 @@ def upload(upload_settings, dists):
print(skip_message)
continue

utils.check_status_code(resp, upload_settings.verbose)
check_status_code(resp, upload_settings.verbose)

uploaded_packages.append(package)

Expand Down
9 changes: 9 additions & 0 deletions twine/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,12 @@ class InvalidDistribution(TwineException):
"""Raised when a distribution is invalid."""

pass


class InvalidPyPIUploadURL(TwineException):
"""Repository configuration tries to use PyPI with an incorrect URL.

For example, https://pypi.org instead of https://upload.pypi.org/legacy.
"""

pass
29 changes: 0 additions & 29 deletions twine/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import configparser
from urllib.parse import urlparse, urlunparse

from requests.exceptions import HTTPError

try:
import keyring # noqa
Expand Down Expand Up @@ -129,34 +128,6 @@ def normalize_repository_url(url):
return urlunparse(parsed)


def check_status_code(response, verbose):
"""
Shouldn't happen, thanks to the UploadToDeprecatedPyPIDetected
exception, but this is in case that breaks and it does.
"""
if (response.status_code == 410 and
response.url.startswith(("https://pypi.python.org",
"https://testpypi.python.org"))):
print("It appears you're uploading to pypi.python.org (or "
"testpypi.python.org). You've received a 410 error response. "
"Uploading to those sites is deprecated. The new sites are "
"pypi.org and test.pypi.org. Try using "
"https://upload.pypi.org/legacy/ "
"(or https://test.pypi.org/legacy/) to upload your packages "
"instead. These are the default URLs for Twine now. More at "
"https://packaging.python.org/guides/migrating-to-pypi-org/ ")
try:
response.raise_for_status()
except HTTPError as err:
if response.text:
if verbose:
print('Content received from server:\n{}'.format(
response.text))
else:
print('NOTE: Try --verbose to see response content.')
raise err


def get_userpass_value(cli_value, config, key, prompt_strategy=None):
"""Gets the username / password from config.

Expand Down