Hello community, here is the log from the commit of package python-twine for openSUSE:Factory checked in at 2019-09-23 12:03:37 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-twine (Old) and /work/SRC/openSUSE:Factory/.python-twine.new.7948 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-twine" Mon Sep 23 12:03:37 2019 rev:7 rq:729748 version:1.14.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-twine/python-twine.changes 2019-03-18 10:37:45.383499576 +0100 +++ /work/SRC/openSUSE:Factory/.python-twine.new.7948/python-twine.changes 2019-09-23 12:03:42.965941969 +0200 @@ -1,0 +2,7 @@ +Tue Sep 10 09:42:43 UTC 2019 - Tomáš Chvátal <tchva...@suse.com> + +- Update to 1.14.0: + * Better error handling and gpg2 fallback if gpg not available. + * Fixes for python 3.8 + +------------------------------------------------------------------- Old: ---- twine-1.13.0.tar.gz New: ---- twine-1.14.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-twine.spec ++++++ --- /var/tmp/diff_new_pack.9dOPWp/_old 2019-09-23 12:03:43.909941813 +0200 +++ /var/tmp/diff_new_pack.9dOPWp/_new 2019-09-23 12:03:43.909941813 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-twine -Version: 1.13.0 +Version: 1.14.0 Release: 0 Summary: Collection of utilities for interacting with PyPI License: Apache-2.0 @@ -28,7 +28,7 @@ BuildRequires: %{python_module pkginfo >= 1.4.2} BuildRequires: %{python_module pretend} BuildRequires: %{python_module pytest} -BuildRequires: %{python_module readme_renderer >= 24.0} +BuildRequires: %{python_module readme_renderer >= 21.0} BuildRequires: %{python_module requests >= 2.17.0} BuildRequires: %{python_module requests-toolbelt >= 0.8.0} BuildRequires: %{python_module setuptools >= 0.7.0} @@ -36,7 +36,7 @@ BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-pkginfo >= 1.4.2 -Requires: python-readme_renderer >= 24.0 +Requires: python-readme_renderer >= 21.0 Requires: python-requests >= 2.17.0 Requires: python-requests-toolbelt >= 0.8.0 Requires: python-setuptools >= 0.7.0 @@ -71,7 +71,7 @@ %python_expand %fdupes %{buildroot}%{$python_sitelib} %check -%python_expand PYTHONPATH=%{buildroot}%{$python_sitelib} py.test-%{$python_version} +%pytest %post %python_install_alternative twine ++++++ twine-1.13.0.tar.gz -> twine-1.14.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/AUTHORS new/twine-1.14.0/AUTHORS --- old/twine-1.13.0/AUTHORS 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/AUTHORS 2019-09-07 20:50:56.000000000 +0200 @@ -23,4 +23,6 @@ Frances Hocutt <frances.hoc...@gmail.com> Tathagata Dasgupta <tathagat...@gmail.com> Wasim Thabraze <wa...@thabraze.me> -Varun Kamath <varunkamat...@gmail.com> \ No newline at end of file +Varun Kamath <varunkamat...@gmail.com> +Brian Rutledge <bhrutle...@gmail.com> +Peter Stensmyr <peter.stens...@gmail.com> (http://www.peterstensmyr.com) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/MANIFEST.in new/twine-1.14.0/MANIFEST.in --- old/twine-1.13.0/MANIFEST.in 2018-09-21 19:32:47.000000000 +0200 +++ new/twine-1.14.0/MANIFEST.in 2019-09-07 20:50:56.000000000 +0200 @@ -3,7 +3,7 @@ include AUTHORS include .coveragerc -recursive-include tests *.py *.whl deprecated-pypirc +recursive-include tests *.py *.whl *.gz deprecated-pypirc recursive-include docs *.bat *.empty *.py *.rst Makefile *.txt prune docs/_build diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/PKG-INFO new/twine-1.14.0/PKG-INFO --- old/twine-1.13.0/PKG-INFO 2019-02-13 22:38:54.000000000 +0100 +++ new/twine-1.14.0/PKG-INFO 2019-09-07 23:30:47.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: twine -Version: 1.13.0 +Version: 1.14.0 Summary: Collection of utilities for publishing packages on PyPI Home-page: https://twine.readthedocs.io/ Author: Donald Stufft and individual contributors @@ -369,5 +369,5 @@ Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* -Provides-Extra: with-blake2 Provides-Extra: keyring +Provides-Extra: with-blake2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/docs/changelog.rst new/twine-1.14.0/docs/changelog.rst --- old/twine-1.13.0/docs/changelog.rst 2019-02-13 22:37:01.000000000 +0100 +++ new/twine-1.14.0/docs/changelog.rst 2019-09-07 23:21:42.000000000 +0200 @@ -3,32 +3,39 @@ ========= Changelog ========= +* :release:`1.14.0 <2019-09-06>` +* :feature:`456` Better error handling and gpg2 fallback if gpg not available. +* :bug:`341` Fail more gracefully when encountering bad metadata +* :feature:`459` Show Warehouse URL after uploading a package +* :feature:`310` Now provide a more meaningful error on redirect during upload. * :release:`1.13.0 <2019-02-13>` -* :bug:`452` Restore prompts while retaining support for suppressing prompts. -* :bug:`447` Avoid requests-toolbelt to 0.9.0 to prevent attempting to use +* :bug:`452 major` Restore prompts while retaining support for suppressing + prompts. +* :bug:`447 major` Avoid requests-toolbelt to 0.9.0 to prevent attempting to + use openssl when it isn't available. * :feature:`427` Add disable_progress_bar option to disable tqdm. * :feature:`426` Allow defining an empty username and password in .pypirc. -* :bug:`441` Only install pyblake2 if needed. -* :bug:`444` Use io.StringIO instead of StringIO. -* :bug:`436` Use modern Python language features. +* :bug:`441 major` Only install pyblake2 if needed. +* :bug:`444 major` Use io.StringIO instead of StringIO. +* :bug:`436 major` Use modern Python language features. * :support:`439` Refactor tox env and travis config. -* :bug:`435` Specify python_requires in setup.py -* :bug:`432` Use https URLs everywhere. -* :bug:`428` Fix --skip-existing for Nexus Repos. +* :bug:`435 major` Specify python_requires in setup.py +* :bug:`432 major` Use https URLs everywhere. +* :bug:`428 major` Fix --skip-existing for Nexus Repos. * :feature:`419` Support keyring.get_credential. * :feature:`418` Support keyring.get_username_and_password. -* :bug:`421` Remove unnecessary usage of readme_render.markdown. -* :feature:`` Add Python 3.7 to classifiers. -* :bug:`412` Don't crash if there's no package description. -* :bug:`408` Fix keyring support. +* :bug:`421 major` Remove unnecessary usage of readme_render.markdown. +* :feature:`416` Add Python 3.7 to classifiers. +* :bug:`412 major` Don't crash if there's no package description. +* :bug:`408 major` Fix keyring support. * :release:`1.12.1 <2018-09-24>` * :bug:`404` Fix regression with upload exit code * :release:`1.12.0 <2018-09-24>` * :feature:`395 major` Add ``twine check`` command to check long description * :feature:`392 major` Drop support for Python 3.3 * :feature:`363` Empower ``--skip-existing`` for Artifactory repositories -* :bug:`367` Avoid MD5 when Python is compiled in FIPS mode +* :bug:`367 major` Avoid MD5 when Python is compiled in FIPS mode * :release:`1.11.0 <2018-03-19>` * :bug:`269 major` Avoid uploading to PyPI when given alternate repository URL, and require ``http://`` or ``https://`` in @@ -134,7 +141,7 @@ ConnectionErrors * :release:`1.6.4 <2015-10-27>` * :bug:`145` Paths with hyphens in them break the Wheel regular expression. -* :bug:`146` Exception while accessing the ``respository`` key (sic) +* :bug:`146` Exception while accessing the ``repository`` key (sic) when raising a redirect exception. * :release:`1.6.3 <2015-10-05>` * :bug:`137`, :bug:`140` Uploading signatures was broken due to the pull diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/docs/requirements.txt new/twine-1.14.0/docs/requirements.txt --- old/twine-1.13.0/docs/requirements.txt 2018-09-21 19:32:47.000000000 +0200 +++ new/twine-1.14.0/docs/requirements.txt 2019-09-07 21:40:56.000000000 +0200 @@ -5,3 +5,6 @@ sphinx_rtd_theme>=0.2.4 tox>=2.9.1 twine>=1.10.0 + +# workaround for #492 +semantic-version<2.7 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/tests/conftest.py new/twine-1.14.0/tests/conftest.py --- old/twine-1.13.0/tests/conftest.py 1970-01-01 01:00:00.000000000 +0100 +++ new/twine-1.14.0/tests/conftest.py 2019-06-08 22:45:39.000000000 +0200 @@ -0,0 +1,31 @@ +import textwrap + +import pytest + +from twine import settings + + +@pytest.fixture() +def pypirc(tmpdir): + return tmpdir / ".pypirc" + + +@pytest.fixture() +def make_settings(pypirc): + """Returns a factory function for settings.Settings with defaults.""" + + default_pypirc = """ + [pypi] + username:foo + password:bar + """ + + def _settings(pypirc_text=default_pypirc, **settings_kwargs): + pypirc.write(textwrap.dedent(pypirc_text)) + + settings_kwargs.setdefault('sign_with', None) + settings_kwargs.setdefault('config_file', str(pypirc)) + + return settings.Settings(**settings_kwargs) + + return _settings Binary files old/twine-1.13.0/tests/fixtures/twine-1.5.0.tar.gz and new/twine-1.14.0/tests/fixtures/twine-1.5.0.tar.gz differ Binary files old/twine-1.13.0/tests/fixtures/twine-1.6.5-py2.py3-none-any.whl and new/twine-1.14.0/tests/fixtures/twine-1.6.5-py2.py3-none-any.whl differ Binary files old/twine-1.13.0/tests/fixtures/twine-1.6.5.tar.gz and new/twine-1.14.0/tests/fixtures/twine-1.6.5.tar.gz differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/tests/test_cli.py new/twine-1.14.0/tests/test_cli.py --- old/twine-1.13.0/tests/test_cli.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/tests/test_cli.py 2019-09-07 20:50:56.000000000 +0200 @@ -32,4 +32,4 @@ def test_catches_enoent(): with pytest.raises(SystemExit): - cli.dispatch(["non-existant-command"]) + cli.dispatch(["non-existent-command"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/tests/test_package.py new/twine-1.14.0/tests/test_package.py --- old/twine-1.13.0/tests/test_package.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/tests/test_package.py 2019-09-07 20:50:56.000000000 +0200 @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import unicode_literals import platform -from twine import package +from twine import package, exceptions import pretend import pytest @@ -59,6 +59,48 @@ assert replaced_check_call.calls == [pretend.call(args)] +def test_run_gpg_raises_exception_if_no_gpgs(monkeypatch): + replaced_check_call = pretend.raiser( + package.FileNotFoundError('not found') + ) + monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call) + gpg_args = ('gpg', '--detach-sign', '-a', 'pypircfile') + + with pytest.raises(exceptions.InvalidSigningExecutable) as err: + package.PackageFile.run_gpg(gpg_args) + + assert 'executables not available' in err.value.args[0] + + +def test_run_gpg_raises_exception_if_not_using_gpg(monkeypatch): + replaced_check_call = pretend.raiser( + package.FileNotFoundError('not found') + ) + monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call) + gpg_args = ('not_gpg', '--detach-sign', '-a', 'pypircfile') + + with pytest.raises(exceptions.InvalidSigningExecutable) as err: + package.PackageFile.run_gpg(gpg_args) + + assert 'not_gpg executable not available' in err.value.args[0] + + +def test_run_gpg_falls_back_to_gpg2(monkeypatch): + + def check_call(arg_list): + if arg_list[0] == 'gpg': + raise package.FileNotFoundError('gpg not found') + + replaced_check_call = pretend.call_recorder(check_call) + monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call) + gpg_args = ('gpg', '--detach-sign', '-a', 'pypircfile') + + package.PackageFile.run_gpg(gpg_args) + + gpg2_args = replaced_check_call.calls[1].args + assert gpg2_args[0][0] == 'gpg2' + + def test_package_signed_name_is_correct(): filename = 'tests/fixtures/deprecated-pypirc' @@ -208,3 +250,22 @@ hasher.hash() hashes = TWINE_1_5_0_WHEEL_HEXDIGEST._replace(blake2=None) assert hasher.hexdigest() == hashes + + +def test_pkginfo_returns_no_metadata(monkeypatch): + """ + Fail gracefully if pkginfo can't interpret the metadata (possibly due to + seeing a version number it doesn't support yet) and gives us back an + 'empty' object with no metadata + """ + + def EmptyDist(filename): + return pretend.stub(name=None, version=None) + + monkeypatch.setattr(package, "DIST_TYPES", {"bdist_wheel": EmptyDist}) + filename = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl' + + with pytest.raises(exceptions.InvalidDistribution) as err: + package.PackageFile.from_filename(filename, comment=None) + + assert 'Invalid distribution metadata' in err.value.args[0] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/tests/test_register.py new/twine-1.14.0/tests/test_register.py --- old/twine-1.13.0/tests/test_register.py 1970-01-01 01:00:00.000000000 +0100 +++ new/twine-1.14.0/tests/test_register.py 2019-06-08 22:45:39.000000000 +0200 @@ -0,0 +1,39 @@ +from __future__ import unicode_literals + +import pytest +import pretend + +from twine.commands import register +from twine import exceptions + + +# TODO: Copied from test_upload.py. Extract to helpers? + +WHEEL_FIXTURE = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl' + + +def test_exception_for_redirect(make_settings): + register_settings = make_settings(""" + [pypi] + repository: https://test.pypi.org/legacy + username:foo + password:bar + """) + + stub_response = pretend.stub( + is_redirect=True, + status_code=301, + headers={'location': 'https://test.pypi.org/legacy/'} + ) + + stub_repository = pretend.stub( + register=lambda package: stub_response, + close=lambda: None + ) + + register_settings.create_repository = lambda: stub_repository + + with pytest.raises(exceptions.RedirectDetected) as err: + register.register(register_settings, WHEEL_FIXTURE) + + assert "https://test.pypi.org/legacy/" in err.value.args[0] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/tests/test_repository.py new/twine-1.14.0/tests/test_repository.py --- old/twine-1.13.0/tests/test_repository.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/tests/test_repository.py 2019-09-07 20:50:56.000000000 +0200 @@ -14,7 +14,7 @@ import requests from twine import repository -from twine.utils import DEFAULT_REPOSITORY +from twine.utils import DEFAULT_REPOSITORY, TEST_REPOSITORY import pretend import pytest @@ -188,3 +188,62 @@ ) repo.upload(package) + + +@pytest.mark.parametrize('package_meta,repository_url,release_urls', [ + # Single package + ( + [('fake', '2.12.0')], + DEFAULT_REPOSITORY, + {'https://pypi.org/project/fake/2.12.0/'}, + ), + # Single package to testpypi + ( + [('fake', '2.12.0')], + TEST_REPOSITORY, + {'https://test.pypi.org/project/fake/2.12.0/'}, + ), + # Multiple packages (faking a wheel and an sdist) + ( + [('fake', '2.12.0'), ('fake', '2.12.0')], + DEFAULT_REPOSITORY, + {'https://pypi.org/project/fake/2.12.0/'}, + ), + # Multiple releases + ( + [('fake', '2.12.0'), ('fake', '2.12.1')], + DEFAULT_REPOSITORY, + { + 'https://pypi.org/project/fake/2.12.0/', + 'https://pypi.org/project/fake/2.12.1/', + }, + ), + # Not pypi + ( + [('fake', '2.12.0')], + 'http://devpi.example.com', + set(), + ), + # No packages + ( + [], + DEFAULT_REPOSITORY, + set(), + ), +]) +def test_release_urls(package_meta, repository_url, release_urls): + packages = [ + pretend.stub( + safe_name=name, + metadata=pretend.stub(version=version), + ) + for name, version in package_meta + ] + + repo = repository.Repository( + repository_url=repository_url, + username='username', + password='password', + ) + + assert repo.release_urls(packages) == release_urls diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/tests/test_upload.py new/twine-1.14.0/tests/test_upload.py --- old/twine-1.13.0/tests/test_upload.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/tests/test_upload.py 2019-09-07 20:50:56.000000000 +0200 @@ -13,117 +13,197 @@ # limitations under the License. from __future__ import unicode_literals -import os -import textwrap - import pretend import pytest +from requests.exceptions import HTTPError from twine.commands import upload -from twine import package, cli, exceptions, settings +from twine import package, cli, exceptions import twine import helpers +SDIST_FIXTURE = 'tests/fixtures/twine-1.5.0.tar.gz' WHEEL_FIXTURE = 'tests/fixtures/twine-1.5.0-py2.py3-none-any.whl' +RELEASE_URL = 'https://pypi.org/project/twine/1.5.0/' +NEW_SDIST_FIXTURE = 'tests/fixtures/twine-1.6.5.tar.gz' +NEW_WHEEL_FIXTURE = 'tests/fixtures/twine-1.6.5-py2.py3-none-any.whl' +NEW_RELEASE_URL = 'https://pypi.org/project/twine/1.6.5/' -def test_successful_upload(tmpdir): - pypirc = os.path.join(str(tmpdir), ".pypirc") - dists = ["tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"] +def test_successful_upload(make_settings, capsys): + upload_settings = make_settings() - with open(pypirc, "w") as fp: - fp.write(textwrap.dedent(""" - [pypi] - username:foo - password:bar - """)) + stub_response = pretend.stub( + is_redirect=False, + status_code=201, + raise_for_status=lambda: None + ) - upload_settings = settings.Settings( - repository_name="pypi", sign=None, identity=None, username=None, - password=None, comment=None, cert=None, client_cert=None, - sign_with=None, config_file=pypirc, skip_existing=False, - repository_url=None, verbose=False, + stub_repository = pretend.stub( + upload=lambda package: stub_response, + close=lambda: None, + release_urls=lambda packages: {RELEASE_URL, NEW_RELEASE_URL} ) + upload_settings.create_repository = lambda: stub_repository + + result = upload.upload(upload_settings, [ + WHEEL_FIXTURE, SDIST_FIXTURE, NEW_SDIST_FIXTURE, NEW_WHEEL_FIXTURE + ]) + + # A truthy result means the upload failed + assert result is None + + captured = capsys.readouterr() + assert captured.out.count(RELEASE_URL) == 1 + assert captured.out.count(NEW_RELEASE_URL) == 1 + + +@pytest.mark.parametrize('verbose', [False, True]) +def test_exception_for_http_status(verbose, make_settings, capsys): + upload_settings = make_settings() + upload_settings.verbose = verbose + stub_response = pretend.stub( - is_redirect=False, status_code=201, raise_for_status=lambda: None + is_redirect=False, + status_code=403, + text="Invalid or non-existent authentication information", + raise_for_status=pretend.raiser(HTTPError) ) + stub_repository = pretend.stub( - upload=lambda package: stub_response, close=lambda: None + upload=lambda package: stub_response, + close=lambda: None, ) upload_settings.create_repository = lambda: stub_repository - result = upload.upload(upload_settings, dists) + with pytest.raises(HTTPError): + upload.upload(upload_settings, [WHEEL_FIXTURE]) - # Raising an exception or returning anything truthy would mean that the - # upload has failed - assert result is None + captured = capsys.readouterr() + assert RELEASE_URL not in captured.out + if verbose: + assert stub_response.text in captured.out + assert '--verbose' not in captured.out + else: + assert stub_response.text not in captured.out + assert '--verbose' in captured.out -def test_get_config_old_format(tmpdir): - pypirc = os.path.join(str(tmpdir), ".pypirc") - with open(pypirc, "w") as fp: - fp.write(textwrap.dedent(""" +def test_get_config_old_format(make_settings, pypirc): + try: + make_settings(""" [server-login] username:foo password:bar - """)) - - try: - settings.Settings( - repository_name="pypi", sign=None, identity=None, username=None, - password=None, comment=None, cert=None, client_cert=None, - sign_with=None, config_file=pypirc, skip_existing=False, - repository_url=None, verbose=False, - ) + """) except KeyError as err: - assert err.args[0] == ( - "Missing 'pypi' section from the configuration file\n" - "or not a complete URL in --repository-url.\n" - "Maybe you have a out-dated '{0}' format?\n" - "more info: " - "https://docs.python.org/distutils/packageindex.html#pypirc\n" - ).format(pypirc) + assert all(text in err.args[0] for text in [ + "'pypi'", + "--repository-url", + pypirc, + "https://docs.python.org/", + ]) -def test_deprecated_repo(tmpdir): +def test_deprecated_repo(make_settings): with pytest.raises(exceptions.UploadToDeprecatedPyPIDetected) as err: - pypirc = os.path.join(str(tmpdir), ".pypirc") - dists = ["tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"] + upload_settings = make_settings(""" + [pypi] + repository: https://pypi.python.org/pypi/ + username:foo + password:bar + """) + + upload.upload(upload_settings, [WHEEL_FIXTURE]) + + assert all(text in err.value.args[0] for text in [ + "https://pypi.python.org/pypi/", + "https://upload.pypi.org/legacy/", + "https://test.pypi.org/legacy/", + "https://packaging.python.org/", + ]) + + +def test_exception_for_redirect(make_settings): + upload_settings = make_settings(""" + [pypi] + repository: https://test.pypi.org/legacy + username:foo + password:bar + """) + + stub_response = pretend.stub( + is_redirect=True, + status_code=301, + headers={'location': 'https://test.pypi.org/legacy/'} + ) + + stub_repository = pretend.stub( + upload=lambda package: stub_response, + close=lambda: None + ) + + upload_settings.create_repository = lambda: stub_repository + + with pytest.raises(exceptions.RedirectDetected) as err: + upload.upload(upload_settings, [WHEEL_FIXTURE]) + + assert "https://test.pypi.org/legacy/" in err.value.args[0] + + +def test_prints_skip_message_for_uploaded_package(make_settings, capsys): + upload_settings = make_settings(skip_existing=True) + + stub_repository = pretend.stub( + # Short-circuit the upload, so no need for a stub response + package_is_uploaded=lambda package: True, + release_urls=lambda packages: {}, + close=lambda: None + ) + + upload_settings.create_repository = lambda: stub_repository + + result = upload.upload(upload_settings, [WHEEL_FIXTURE]) + + # A truthy result means the upload failed + assert result is None + + captured = capsys.readouterr() + assert "Skipping twine-1.5.0-py2.py3-none-any.whl" in captured.out + assert RELEASE_URL not in captured.out + + +def test_prints_skip_message_for_response(make_settings, capsys): + upload_settings = make_settings(skip_existing=True) - with open(pypirc, "w") as fp: - fp.write(textwrap.dedent(""" - [pypi] - repository: https://pypi.python.org/pypi/ - username:foo - password:bar - """)) - - upload_settings = settings.Settings( - repository_name="pypi", sign=None, identity=None, username=None, - password=None, comment=None, cert=None, client_cert=None, - sign_with=None, config_file=pypirc, skip_existing=False, - repository_url=None, verbose=False, - ) - - upload.upload(upload_settings, dists) - - assert err.value.args[0] == ( - "You're trying to upload to the legacy PyPI site " - "'https://pypi.python.org/pypi/'. " - "Uploading to those sites is deprecated. \n " - "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. \n " - "More at " - "https://packaging.python.org/guides/migrating-to-pypi-org/ ." + stub_response = pretend.stub( + is_redirect=False, + status_code=409, ) + stub_repository = pretend.stub( + # Do the upload, triggering the error response + package_is_uploaded=lambda package: False, + release_urls=lambda packages: {}, + upload=lambda package: stub_response, + close=lambda: None + ) + + upload_settings.create_repository = lambda: stub_repository + + result = upload.upload(upload_settings, [WHEEL_FIXTURE]) + + # A truthy result means the upload failed + assert result is None + + captured = capsys.readouterr() + assert "Skipping twine-1.5.0-py2.py3-none-any.whl" in captured.out + assert RELEASE_URL not in captured.out + def test_skip_existing_skips_files_already_on_PyPI(monkeypatch): response = pretend.stub( @@ -193,7 +273,7 @@ def test_values_from_env(monkeypatch): - def none_upload(*args, **kwargs): + def none_upload(*args, **settings_kwargs): pass replaced_upload = pretend.call_recorder(none_upload) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine/__init__.py new/twine-1.14.0/twine/__init__.py --- old/twine-1.13.0/twine/__init__.py 2019-02-13 22:37:01.000000000 +0100 +++ new/twine-1.14.0/twine/__init__.py 2019-09-07 23:21:42.000000000 +0200 @@ -22,7 +22,7 @@ __summary__ = "Collection of utilities for publishing packages on PyPI" __uri__ = "https://twine.readthedocs.io/" -__version__ = "1.13.0" +__version__ = "1.14.0" __author__ = "Donald Stufft and individual contributors" __email__ = "don...@stufft.io" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine/commands/check.py new/twine-1.14.0/twine/commands/check.py --- old/twine-1.13.0/twine/commands/check.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/twine/commands/check.py 2019-09-07 20:50:56.000000000 +0200 @@ -33,7 +33,7 @@ } -# Regular expression used to capture and reformat doctuils warnings into +# Regular expression used to capture and reformat docutils warnings into # something that a human can understand. This is loosely borrowed from # Sphinx: https://github.com/sphinx-doc/sphinx/blob # /c35eb6fade7a3b4a6de4183d1dd4196f04a5edaf/sphinx/util/docutils.py#L199 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine/commands/register.py new/twine-1.14.0/twine/commands/register.py --- old/twine-1.13.0/twine/commands/register.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/twine/commands/register.py 2019-06-08 22:45:39.000000000 +0200 @@ -38,16 +38,19 @@ repository.close() if resp.is_redirect: - raise exceptions.RedirectDetected( - ('"{0}" attempted to redirect to "{1}" during registration.' - ' Aborting...').format(repository_url, - resp.headers["location"])) + raise exceptions.RedirectDetected.from_args( + repository_url, + resp.headers["location"], + ) resp.raise_for_status() def main(args): - parser = argparse.ArgumentParser(prog="twine register") + parser = argparse.ArgumentParser( + prog="twine register", + description="register operation is not required with PyPI.org", + ) settings.Settings.register_argparse_arguments(parser) parser.add_argument( "package", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine/commands/upload.py new/twine-1.14.0/twine/commands/upload.py --- old/twine-1.13.0/twine/commands/upload.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/twine/commands/upload.py 2019-09-07 20:50:56.000000000 +0200 @@ -63,6 +63,7 @@ print("Uploading distributions to {}".format(repository_url)) repository = upload_settings.create_repository() + uploaded_packages = [] for filename in uploads: package = PackageFile.from_filename(filename, upload_settings.comment) @@ -92,16 +93,25 @@ # by PyPI should never happen in reality. This should catch malicious # redirects as well. if resp.is_redirect: - raise exceptions.RedirectDetected( - ('"{0}" attempted to redirect to "{1}" during upload.' - ' Aborting...').format(repository_url, - resp.headers["location"])) + raise exceptions.RedirectDetected.from_args( + repository_url, + resp.headers["location"], + ) if skip_upload(resp, upload_settings.skip_existing, package): print(skip_message) continue + utils.check_status_code(resp, upload_settings.verbose) + uploaded_packages.append(package) + + release_urls = repository.release_urls(uploaded_packages) + if release_urls: + print('\nView at:') + for url in release_urls: + print(url) + # Bug 28. Try to silence a ResourceWarning by clearing the connection # pool. repository.close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine/exceptions.py new/twine-1.14.0/twine/exceptions.py --- old/twine-1.13.0/twine/exceptions.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/twine/exceptions.py 2019-09-07 20:50:56.000000000 +0200 @@ -29,7 +29,17 @@ redirecting them. """ - pass + @classmethod + def from_args(cls, repository_url, redirect_url): + msg = "\n".join([ + "{} attempted to redirect to {}." + .format(repository_url, redirect_url), + "If you trust these URLs, set {} as your repository URL." + .format(redirect_url), + "Aborting." + ]) + + return cls(msg) class PackageNotFound(TwineException): @@ -74,6 +84,12 @@ pass + +class InvalidSigningExecutable(TwineException): + """Signing executable must be installed on system.""" + + pass + class InvalidConfiguration(TwineException): """Raised when configuration is invalid.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine/package.py new/twine-1.14.0/twine/package.py --- old/twine-1.13.0/twine/package.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/twine/package.py 2019-09-07 20:50:56.000000000 +0200 @@ -33,6 +33,12 @@ from twine.wininst import WinInst from twine import exceptions +try: + FileNotFoundError = FileNotFoundError +except NameError: + FileNotFoundError = IOError # Py2 + + DIST_TYPES = { "bdist_wheel": Wheel, "bdist_wininst": WinInst, @@ -84,6 +90,15 @@ os.path.basename(filename) ) + # If pkginfo encounters a metadata version it doesn't support, it may + # give us back empty metadata. At the very least, we should have a name + # and version + if not (meta.name and meta.version): + raise exceptions.InvalidDistribution( + "Invalid distribution metadata. Try upgrading twine if " + "possible." + ) + if dtype == "bdist_egg": pkgd = pkg_resources.Distribution.from_filename(filename) py_version = pkgd.py_version @@ -165,10 +180,31 @@ if identity: gpg_args += ("--local-user", identity) gpg_args += ("-a", self.filename) - subprocess.check_call(gpg_args) + self.run_gpg(gpg_args) self.add_gpg_signature(self.signed_filename, self.signed_basefilename) + @classmethod + def run_gpg(cls, gpg_args): + try: + subprocess.check_call(gpg_args) + return + except FileNotFoundError: + if gpg_args[0] != "gpg": + raise exceptions.InvalidSigningExecutable( + "{} executable not available.".format(gpg_args[0])) + + print("gpg executable not available. Attempting fallback to gpg2.") + try: + subprocess.check_call(("gpg2",) + gpg_args[1:]) + except FileNotFoundError: + print("gpg2 executable not available.") + raise exceptions.InvalidSigningExecutable( + "'gpg' or 'gpg2' executables not available. " + "Try installing one of these or specifying an executable " + "with the --sign-with flag." + ) + Hexdigest = collections.namedtuple('Hexdigest', ['md5', 'sha2', 'blake2']) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine/repository.py new/twine-1.14.0/twine/repository.py --- old/twine-1.13.0/twine/repository.py 2019-02-13 21:35:39.000000000 +0100 +++ new/twine-1.14.0/twine/repository.py 2019-09-07 20:50:56.000000000 +0200 @@ -34,6 +34,8 @@ LEGACY_TEST_PYPI = 'https://testpypi.python.org/' WAREHOUSE = 'https://upload.pypi.org/' OLD_WAREHOUSE = 'https://upload.pypi.io/' +TEST_WAREHOUSE = 'https://test.pypi.org/' +WAREHOUSE_WEB = 'https://pypi.org/' class ProgressBar(tqdm): @@ -208,6 +210,21 @@ return False + def release_urls(self, packages): + if self.url.startswith(WAREHOUSE): + url = WAREHOUSE_WEB + elif self.url.startswith(TEST_WAREHOUSE): + url = TEST_WAREHOUSE + else: + return set() + + return { + '{}project/{}/{}/'.format( + url, package.safe_name, package.metadata.version + ) + for package in packages + } + def verify_package_integrity(self, package): # TODO(sigmavirus24): Add a way for users to download the package and # check it's hash against what it has locally. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine.egg-info/PKG-INFO new/twine-1.14.0/twine.egg-info/PKG-INFO --- old/twine-1.13.0/twine.egg-info/PKG-INFO 2019-02-13 22:38:54.000000000 +0100 +++ new/twine-1.14.0/twine.egg-info/PKG-INFO 2019-09-07 23:30:47.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: twine -Version: 1.13.0 +Version: 1.14.0 Summary: Collection of utilities for publishing packages on PyPI Home-page: https://twine.readthedocs.io/ Author: Donald Stufft and individual contributors @@ -369,5 +369,5 @@ Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* -Provides-Extra: with-blake2 Provides-Extra: keyring +Provides-Extra: with-blake2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-1.13.0/twine.egg-info/SOURCES.txt new/twine-1.14.0/twine.egg-info/SOURCES.txt --- old/twine-1.13.0/twine.egg-info/SOURCES.txt 2019-02-13 22:38:54.000000000 +0100 +++ new/twine-1.14.0/twine.egg-info/SOURCES.txt 2019-09-07 23:30:47.000000000 +0200 @@ -13,12 +13,14 @@ docs/make.bat docs/requirements.txt docs/_static/.empty +tests/conftest.py tests/helpers.py tests/test_check.py tests/test_cli.py tests/test_commands.py tests/test_main.py tests/test_package.py +tests/test_register.py tests/test_repository.py tests/test_settings.py tests/test_upload.py @@ -27,6 +29,9 @@ tests/alt-fixtures/twine-1.5.0-py2.py3-none-any.whl tests/fixtures/deprecated-pypirc tests/fixtures/twine-1.5.0-py2.py3-none-any.whl +tests/fixtures/twine-1.5.0.tar.gz +tests/fixtures/twine-1.6.5-py2.py3-none-any.whl +tests/fixtures/twine-1.6.5.tar.gz twine/__init__.py twine/__main__.py twine/_installed.py