diff --git a/tests/fixtures/twine-1.5.0-py2.py3-none-any.whl b/tests/fixtures/twine-1.5.0-py2.py3-none-any.whl new file mode 100644 index 00000000..af870cd4 Binary files /dev/null and b/tests/fixtures/twine-1.5.0-py2.py3-none-any.whl differ diff --git a/tests/test_upload.py b/tests/test_upload.py index 5c843db1..b40660f8 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -16,9 +16,14 @@ import os import textwrap +import pretend import pytest from twine.commands import upload +from twine import package + + +WHEEL_FIXTURE = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl' def test_ensure_wheel_files_uploaded_first(): @@ -72,7 +77,7 @@ def test_get_config_old_format(tmpdir): try: upload.upload(dists="foo", repository="pypi", sign=None, identity=None, username=None, password=None, comment=None, - sign_with=None, config_file=pypirc) + sign_with=None, config_file=pypirc, skip_existing=False) except KeyError as err: assert err.args[0] == ( "Missing 'pypi' section from the configuration file.\n" @@ -80,3 +85,27 @@ def test_get_config_old_format(tmpdir): "more info: " "https://docs.python.org/distutils/packageindex.html#pypirc\n" ).format(pypirc) + + +def test_skip_existing_skips_files_already_on_PyPI(monkeypatch): + response = pretend.stub( + status_code=400, + reason='A file named "twine-1.5.0-py2.py3-none-any.whl" already ' + 'exists for twine-1.5.0.') + + pkg = package.PackageFile.from_filename(WHEEL_FIXTURE, None) + assert upload.skip_upload(response=response, + skip_existing=True, + package=pkg) is True + + +def test_skip_upload_respects_skip_existing(monkeypatch): + response = pretend.stub( + status_code=400, + reason='A file named "twine-1.5.0-py2.py3-none-any.whl" already ' + 'exists for twine-1.5.0.') + + pkg = package.PackageFile.from_filename(WHEEL_FIXTURE, None) + assert upload.skip_upload(response=response, + skip_existing=False, + package=pkg) is False diff --git a/twine/commands/upload.py b/twine/commands/upload.py index 03814799..032cc21b 100644 --- a/twine/commands/upload.py +++ b/twine/commands/upload.py @@ -53,8 +53,16 @@ def find_dists(dists): return group_wheel_files_first(uploads) +def skip_upload(response, skip_existing, package): + filename = package.basefilename + msg = 'A file named "{0}" already exists for'.format(filename) + return (response.status_code == 400 and + response.reason.startswith(msg) and + skip_existing) + + def upload(dists, repository, sign, identity, username, password, comment, - sign_with, config_file): + sign_with, config_file, skip_existing): # Check that a nonsensical option wasn't given if not sign and identity: raise ValueError("sign must be given along with identity") @@ -106,9 +114,15 @@ def upload(dists, repository, sign, identity, username, password, comment, if resp.is_redirect: raise exc.RedirectDetected( ('"{0}" attempted to redirect to "{1}" during upload.' - ' Aborting...').format(config["respository"], + ' Aborting...').format(config["repository"], resp.headers["location"])) + # Otherwise, raise an HTTPError based on the status code. + if skip_upload(resp, skip_existing, package): + print(" Skipping {0} because it appears to already exist".format( + package.basefilename)) + continue + resp.raise_for_status() # Bug 28. Try to silence a ResourceWarning by clearing the connection @@ -155,6 +169,12 @@ def main(args): default="~/.pypirc", help="The .pypirc config file to use", ) + parser.add_argument( + "--skip-existing", + default=False, + action="store_true", + help="Continue uploading files if one already exists", + ) parser.add_argument( "dists", nargs="+",