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


Reply via email to