Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-twine for openSUSE:Factory checked in at 2024-07-01 11:19:20 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-twine (Old) and /work/SRC/openSUSE:Factory/.python-twine.new.18349 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-twine" Mon Jul 1 11:19:20 2024 rev:16 rq:1183989 version:5.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-twine/python-twine.changes 2024-04-11 19:40:31.876380354 +0200 +++ /work/SRC/openSUSE:Factory/.python-twine.new.18349/python-twine.changes 2024-07-01 11:19:24.976975118 +0200 @@ -1,0 +2,6 @@ +Sat Jun 29 12:59:17 UTC 2024 - Dirk Müller <dmuel...@suse.com> + +- update to 5.1.0: + * Add the experimental --attestations flag. + +------------------------------------------------------------------- Old: ---- twine-5.0.0.tar.gz New: ---- twine-5.1.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-twine.spec ++++++ --- /var/tmp/diff_new_pack.C4i2Qh/_old 2024-07-01 11:19:25.773004118 +0200 +++ /var/tmp/diff_new_pack.C4i2Qh/_new 2024-07-01 11:19:25.773004118 +0200 @@ -16,10 +16,9 @@ # -%define skip_python2 1 %{?sle15_python_module_pythons} Name: python-twine -Version: 5.0.0 +Version: 5.1.0 Release: 0 Summary: Collection of utilities for interacting with PyPI License: Apache-2.0 ++++++ skip-unsupported-Metadata-Version-test.patch ++++++ --- /var/tmp/diff_new_pack.C4i2Qh/_old 2024-07-01 11:19:25.809005429 +0200 +++ /var/tmp/diff_new_pack.C4i2Qh/_new 2024-07-01 11:19:25.813005575 +0200 @@ -2,23 +2,20 @@ tests/test_package.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) ---- a/tests/test_package.py -+++ b/tests/test_package.py -@@ -339,11 +339,11 @@ def test_fips_metadata_excludes_md5_and_ - @pytest.mark.parametrize( +Index: twine-5.1.0/tests/test_package.py +=================================================================== +--- twine-5.1.0.orig/tests/test_package.py ++++ twine-5.1.0/tests/test_package.py +@@ -384,11 +384,6 @@ def test_fips_metadata_excludes_md5_and_ "read_data, missing_fields", [ -- pytest.param( -- b"Metadata-Version: 2.3\nName: test-package\nVersion: 1.0.0\n", + pytest.param( +- b"Metadata-Version: 102.3\nName: test-package\nVersion: 1.0.0\n", - "Name, Version", - id="unsupported Metadata-Version", - ), -+# pytest.param( -+# b"Metadata-Version: 2.3\nName: test-package\nVersion: 1.0.0\n", -+# "Name, Version", -+# id="unsupported Metadata-Version", -+# ), - pytest.param( - b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: UNKNOWN\n", +- pytest.param( + b"Metadata-Version: 2.3\nName: UNKNOWN\nVersion: UNKNOWN\n", "Name, Version", + id="missing Name and Version", ++++++ twine-5.0.0.tar.gz -> twine-5.1.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/.github/ISSUE_TEMPLATE/01_upload_failed.yml new/twine-5.1.0/.github/ISSUE_TEMPLATE/01_upload_failed.yml --- old/twine-5.0.0/.github/ISSUE_TEMPLATE/01_upload_failed.yml 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/.github/ISSUE_TEMPLATE/01_upload_failed.yml 2024-05-16 15:46:47.000000000 +0200 @@ -44,7 +44,7 @@ validations: required: true - - type: markdown + - type: input id: environment-os-other attributes: label: "If you selected 'Other', describe your Operating System here" @@ -90,7 +90,7 @@ validations: required: true - - type: markdown + - type: input id: package-repository attributes: label: "Which package repository are you using?" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/.github/ISSUE_TEMPLATE/02_bug.yml new/twine-5.1.0/.github/ISSUE_TEMPLATE/02_bug.yml --- old/twine-5.0.0/.github/ISSUE_TEMPLATE/02_bug.yml 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/.github/ISSUE_TEMPLATE/02_bug.yml 2024-05-16 15:46:47.000000000 +0200 @@ -44,10 +44,11 @@ validations: required: true - - type: markdown + - type: input id: environment-os-other attributes: label: "If you selected 'Other', describe your Operating System here" + placeholder: "example: Linux hostname 6.5.10-200.fc38.x86_64" validations: required: false @@ -90,7 +91,7 @@ validations: required: true - - type: markdown + - type: input id: package-repository attributes: label: "Which package repository are you using?" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/.github/workflows/codeql-analysis.yml new/twine-5.1.0/.github/workflows/codeql-analysis.yml --- old/twine-5.0.0/.github/workflows/codeql-analysis.yml 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/.github/workflows/codeql-analysis.yml 2024-05-16 15:46:47.000000000 +0200 @@ -41,7 +41,7 @@ steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/.github/workflows/main.yml new/twine-5.1.0/.github/workflows/main.yml --- old/twine-5.0.0/.github/workflows/main.yml 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/.github/workflows/main.yml 2024-05-16 15:46:47.000000000 +0200 @@ -25,8 +25,8 @@ lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4.1.5 + - uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} - name: Install dependencies @@ -49,8 +49,8 @@ - windows-latest runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4.1.5 + - uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -67,8 +67,8 @@ # Only run on Ubuntu because most of the tests are skipped on Windows runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4.1.5 + - uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.MIN_PYTHON_VERSION }} - name: Install dependencies @@ -79,8 +79,8 @@ docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4.1.5 + - uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.MIN_PYTHON_VERSION }} - name: Install dependencies @@ -113,8 +113,8 @@ if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4.1.5 + - uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.MIN_PYTHON_VERSION }} - name: Install dependencies diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/.github/workflows/release.yml new/twine-5.1.0/.github/workflows/release.yml --- old/twine-5.0.0/.github/workflows/release.yml 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/.github/workflows/release.yml 2024-05-16 15:46:47.000000000 +0200 @@ -19,10 +19,10 @@ steps: - name: "Checkout repository" - uses: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3" + uses: "actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b" - name: "Setup Python" - uses: "actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b" + uses: "actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d" with: python-version: "3.x" @@ -37,10 +37,10 @@ - name: "Generate hashes" id: hash run: | - cd dist && echo "::set-output name=hashes::$(sha256sum * | base64 -w0)" + cd dist && echo "hashes=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - name: "Upload dists" - uses: "actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce" + uses: "actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808" with: name: "dist" path: "dist/" @@ -53,7 +53,7 @@ actions: read contents: write id-token: write # Needed to access the workflow's OIDC identity. - uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.5.0" + uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0" with: base64-subjects: "${{ needs.build.outputs.hashes }}" upload-assets: true @@ -70,10 +70,10 @@ steps: - name: "Download dists" - uses: "actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a" + uses: "actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e" with: name: "dist" path: "dist/" - name: "Publish dists to PyPI" - uses: "pypa/gh-action-pypi-publish@48b317d84d5f59668bb13be49d1697e36b3ad009" + uses: "pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/PKG-INFO new/twine-5.1.0/PKG-INFO --- old/twine-5.0.0/PKG-INFO 2024-02-11 14:45:14.069720500 +0100 +++ new/twine-5.1.0/PKG-INFO 2024-05-16 15:46:51.487540500 +0200 @@ -1,10 +1,9 @@ Metadata-Version: 2.1 Name: twine -Version: 5.0.0 +Version: 5.1.0 Summary: Collection of utilities for publishing packages on PyPI -Home-page: https://twine.readthedocs.io/ -Author: Donald Stufft and individual contributors -Author-email: don...@stufft.io +Author-email: Donald Stufft and individual contributors <don...@stufft.io> +Project-URL: Homepage, https://twine.readthedocs.io/ Project-URL: Source, https://github.com/pypa/twine/ Project-URL: Documentation, https://twine.readthedocs.io/en/latest/ Project-URL: Packaging tutorial, https://packaging.python.org/tutorials/packaging-projects/ @@ -38,18 +37,20 @@ Requires-Dist: rfc3986>=1.4.0 Requires-Dist: rich>=12.0.0 -.. image:: https://img.shields.io/pypi/v/twine.svg +.. |twine-version| image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine -.. image:: https://img.shields.io/pypi/pyversions/twine.svg +.. |python-versions| image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine -.. image:: https://img.shields.io/readthedocs/twine +.. |docs-badge| image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io -.. image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main +.. |build-badge| image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions +|twine-version| |python-versions| |docs-badge| |build-badge| + twine ===== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/README.rst new/twine-5.1.0/README.rst --- old/twine-5.0.0/README.rst 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/README.rst 2024-05-16 15:46:47.000000000 +0200 @@ -1,15 +1,17 @@ -.. image:: https://img.shields.io/pypi/v/twine.svg +.. |twine-version| image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine -.. image:: https://img.shields.io/pypi/pyversions/twine.svg +.. |python-versions| image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine -.. image:: https://img.shields.io/readthedocs/twine +.. |docs-badge| image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io -.. image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main +.. |build-badge| image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions +|twine-version| |python-versions| |docs-badge| |build-badge| + twine ===== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/docs/changelog.rst new/twine-5.1.0/docs/changelog.rst --- old/twine-5.0.0/docs/changelog.rst 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/docs/changelog.rst 2024-05-16 15:46:47.000000000 +0200 @@ -12,6 +12,24 @@ .. towncrier release notes start +Twine 5.1.0 (2024-05-15) +------------------------ + +Features +^^^^^^^^ + +- Add the experimental ``--attestations`` flag. (`#1095 <https://github.com/pypa/twine/issues/1095>`_) + + +Twine 5.1.0 (2024-05-15) +------------------------ + +Misc +^^^^ + +- `#1104 <https://github.com/pypa/twine/issues/1104>`_ + + Twine 5.0.0 (2024-02-10) ------------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/pyproject.toml new/twine-5.1.0/pyproject.toml --- old/twine-5.0.0/pyproject.toml 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/pyproject.toml 2024-05-16 15:46:47.000000000 +0200 @@ -1,8 +1,73 @@ # pyproject.toml [build-system] -requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"] +requires = ["setuptools>=61.2", "wheel", "setuptools_scm[toml]>=6.0"] build-backend = "setuptools.build_meta" +[project] +name = "twine" +authors = [ + { name = "Donald Stufft and individual contributors", email = "don...@stufft.io" }, +] +description = "Collection of utilities for publishing packages on PyPI" +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX", + "Operating System :: POSIX :: BSD", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", +] +requires-python = ">=3.8" +dependencies = [ + "pkginfo >= 1.8.1", + "readme-renderer >= 35.0", + "requests >= 2.20", + "requests-toolbelt >= 0.8.0, != 0.9.0", + "urllib3 >= 1.26.0", + "importlib-metadata >= 3.6", + "keyring >= 15.1", + "rfc3986 >= 1.4.0", + "rich >= 12.0.0", +] +dynamic = ["version"] + +[project.readme] +file = "README.rst" +content-type = "text/x-rst" + +[project.urls] +Homepage = "https://twine.readthedocs.io/" +Source = "https://github.com/pypa/twine/" +Documentation = "https://twine.readthedocs.io/en/latest/" +"Packaging tutorial" = "https://packaging.python.org/tutorials/packaging-projects/" + +[project.entry-points."twine.registered_commands"] +check = "twine.commands.check:main" +upload = "twine.commands.upload:main" +register = "twine.commands.register:main" + +[project.scripts] +twine = "twine.__main__:main" + +[tool.setuptools] +packages = [ + "twine", + "twine.commands", +] +include-package-data = true +license-files = ["LICENSE"] + [tool.setuptools_scm] [tool.towncrier] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/setup.cfg new/twine-5.1.0/setup.cfg --- old/twine-5.0.0/setup.cfg 2024-02-11 14:45:14.069720500 +0100 +++ new/twine-5.1.0/setup.cfg 2024-05-16 15:46:51.487540500 +0200 @@ -1,60 +1,3 @@ -[metadata] -license_files = LICENSE -name = twine -author = Donald Stufft and individual contributors -author_email = don...@stufft.io -description = Collection of utilities for publishing packages on PyPI -long_description = file:README.rst -long_description_content_type = text/x-rst -url = https://twine.readthedocs.io/ -project_urls = - Source = https://github.com/pypa/twine/ - Documentation = https://twine.readthedocs.io/en/latest/ - Packaging tutorial = https://packaging.python.org/tutorials/packaging-projects/ -classifiers = - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Natural Language :: English - Operating System :: MacOS :: MacOS X - Operating System :: POSIX - Operating System :: POSIX :: BSD - Operating System :: POSIX :: Linux - Operating System :: Microsoft :: Windows - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: Implementation :: CPython - -[options] -packages = - twine - twine.commands -python_requires = >=3.8 -install_requires = - pkginfo >= 1.8.1 - readme-renderer >= 35.0 - requests >= 2.20 - requests-toolbelt >= 0.8.0, != 0.9.0 - urllib3 >= 1.26.0 - importlib-metadata >= 3.6 - keyring >= 15.1 - rfc3986 >= 1.4.0 - rich >= 12.0.0 -include_package_data = True - -[options.entry_points] -twine.registered_commands = - check = twine.commands.check:main - upload = twine.commands.upload:main - register = twine.commands.register:main -console_scripts = - twine = twine.__main__:main - [egg_info] tag_build = tag_date = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/tests/test_integration.py new/twine-5.1.0/tests/test_integration.py --- old/twine-5.0.0/tests/test_integration.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/tests/test_integration.py 2024-05-16 15:46:47.000000000 +0200 @@ -48,7 +48,15 @@ run([sys.executable, "-m", "build", "--sdist"], cwd=checkout) [dist, *_] = (checkout / "dist").glob("*") - assert dist.name == f"twine-sampleproject-3.0.0.post{tag}.tar.gz" + # NOTE: newer versions of setuptools (invoked via build) adhere to PEP 625, + # causing the dist name to be `twine_sampleproject` instead of + # `twine-sampleproject`. Both are allowed here for now, but the hyphenated + # version can be removed eventually. + # See: https://github.com/pypa/setuptools/issues/3593 + assert dist.name in ( + f"twine-sampleproject-3.0.0.post{tag}.tar.gz", + f"twine_sampleproject-3.0.0.post{tag}.tar.gz", + ) return dist diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/tests/test_package.py new/twine-5.1.0/tests/test_package.py --- old/twine-5.0.0/tests/test_package.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/tests/test_package.py 2024-05-16 15:46:47.000000000 +0200 @@ -11,6 +11,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 json import string import pretend @@ -114,6 +115,40 @@ assert package.signed_filename == (filename + ".asc") +def test_package_add_attestations(tmp_path): + package = package_file.PackageFile.from_filename(helpers.WHEEL_FIXTURE, None) + + assert package.attestations is None + + attestations = [] + for i in range(3): + path = tmp_path / f"fake.{i}.attestation" + path.write_text(json.dumps({"fake": f"attestation {i}"})) + attestations.append(str(path)) + + package.add_attestations(attestations) + + assert package.attestations == [ + {"fake": "attestation 0"}, + {"fake": "attestation 1"}, + {"fake": "attestation 2"}, + ] + + +def test_package_add_attestations_invalid_json(tmp_path): + package = package_file.PackageFile.from_filename(helpers.WHEEL_FIXTURE, None) + + assert package.attestations is None + + attestation = tmp_path / "fake.publish.attestation" + attestation.write_text("this is not valid JSON") + + with pytest.raises( + exceptions.InvalidDistribution, match="invalid JSON in attestation" + ): + package.add_attestations([attestation]) + + @pytest.mark.parametrize( "pkg_name,expected_name", [ @@ -177,7 +212,7 @@ "requires_external", "requires_python", # Metadata 2.1 - "provides_extras", + "provides_extra", "description_content_type", # Metadata 2.2 "dynamic", @@ -185,7 +220,8 @@ @pytest.mark.parametrize("gpg_signature", [(None), (pretend.stub())]) -def test_metadata_dictionary_values(gpg_signature): +@pytest.mark.parametrize("attestation", [(None), ({"fake": "attestation"})]) +def test_metadata_dictionary_values(gpg_signature, attestation): """Pass values from pkginfo.Distribution through to dictionary.""" meta = pretend.stub( name="whatever", @@ -226,6 +262,8 @@ filetype=pretend.stub(), ) package.gpg_signature = gpg_signature + if attestation: + package.attestations = [attestation] result = package.metadata_dictionary() @@ -268,7 +306,7 @@ assert result["requires_python"] == meta.requires_python # Metadata 2.1 - assert result["provides_extras"] == meta.provides_extras + assert result["provides_extra"] == meta.provides_extras assert result["description_content_type"] == meta.description_content_type # Metadata 2.2 @@ -277,6 +315,12 @@ # GPG signature assert result.get("gpg_signature") == gpg_signature + # Attestations + if attestation: + assert result["attestations"] == json.dumps(package.attestations) + else: + assert "attestations" not in result + TWINE_1_5_0_WHEEL_HEXDIGEST = package_file.Hexdigest( "1919f967e990bee7413e2a4bc35fd5d1", @@ -340,21 +384,36 @@ "read_data, missing_fields", [ pytest.param( - b"Metadata-Version: 2.3\nName: test-package\nVersion: 1.0.0\n", + b"Metadata-Version: 102.3\nName: test-package\nVersion: 1.0.0\n", "Name, Version", id="unsupported Metadata-Version", ), pytest.param( + b"Metadata-Version: 2.3\nName: UNKNOWN\nVersion: UNKNOWN\n", + "Name, Version", + id="missing Name and Version", + ), + pytest.param( b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: UNKNOWN\n", "Name, Version", id="missing Name and Version", ), pytest.param( + b"Metadata-Version: 2.3\nName: UNKNOWN\nVersion: 1.0.0\n", + "Name", + id="missing Name", + ), + pytest.param( b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: 1.0.0\n", "Name", id="missing Name", ), pytest.param( + b"Metadata-Version: 2.3\nName: test-package\nVersion: UNKNOWN\n", + "Version", + id="missing Version", + ), + pytest.param( b"Metadata-Version: 2.2\nName: test-package\nVersion: UNKNOWN\n", "Version", id="missing Version", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/tests/test_settings.py new/twine-5.1.0/tests/test_settings.py --- old/twine-5.0.0/tests/test_settings.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/tests/test_settings.py 2024-05-16 15:46:47.000000000 +0200 @@ -164,3 +164,7 @@ monkeypatch.setenv("TWINE_NON_INTERACTIVE", "0") args = self.parse_args([]) assert not args.non_interactive + + def test_attestations_flag(self): + args = self.parse_args(["--attestations"]) + assert args.attestations diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/tests/test_upload.py new/twine-5.1.0/tests/test_upload.py --- old/twine-5.0.0/tests/test_upload.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/tests/test_upload.py 2024-05-16 15:46:47.000000000 +0200 @@ -69,10 +69,11 @@ upload_settings.sign = True upload_settings.verbose = True - package = upload._make_package(filename, signatures, upload_settings) + package = upload._make_package(filename, signatures, [], upload_settings) assert package.filename == filename assert package.gpg_signature is not None + assert package.attestations is None assert caplog.messages == [ f"{filename} ({expected_size})", @@ -94,7 +95,7 @@ monkeypatch.setattr(package_file.PackageFile, "sign", stub_sign) - package = upload._make_package(filename, signatures, upload_settings) + package = upload._make_package(filename, signatures, [], upload_settings) assert package.filename == filename assert package.gpg_signature is not None @@ -105,6 +106,56 @@ ] +def test_make_package_attestations_flagged_but_missing(upload_settings): + """Fail when the user requests attestations but does not supply any attestations.""" + upload_settings.attestations = True + + with pytest.raises( + exceptions.InvalidDistribution, match="Upload with attestations requested" + ): + upload._make_package(helpers.NEW_WHEEL_FIXTURE, {}, [], upload_settings) + + +def test_split_inputs(): + """Split inputs into dists, signatures, and attestations.""" + inputs = [ + helpers.WHEEL_FIXTURE, + helpers.WHEEL_FIXTURE + ".asc", + helpers.WHEEL_FIXTURE + ".build.attestation", + helpers.WHEEL_FIXTURE + ".publish.attestation", + helpers.SDIST_FIXTURE, + helpers.SDIST_FIXTURE + ".asc", + helpers.NEW_WHEEL_FIXTURE, + helpers.NEW_WHEEL_FIXTURE + ".frob.attestation", + helpers.NEW_SDIST_FIXTURE, + ] + + inputs = upload._split_inputs(inputs) + + assert inputs.dists == [ + helpers.WHEEL_FIXTURE, + helpers.SDIST_FIXTURE, + helpers.NEW_WHEEL_FIXTURE, + helpers.NEW_SDIST_FIXTURE, + ] + + expected_signatures = { + os.path.basename(dist) + ".asc": dist + ".asc" + for dist in [helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE] + } + assert inputs.signatures == expected_signatures + + assert inputs.attestations_by_dist == { + helpers.WHEEL_FIXTURE: [ + helpers.WHEEL_FIXTURE + ".build.attestation", + helpers.WHEEL_FIXTURE + ".publish.attestation", + ], + helpers.SDIST_FIXTURE: [], + helpers.NEW_WHEEL_FIXTURE: [helpers.NEW_WHEEL_FIXTURE + ".frob.attestation"], + helpers.NEW_SDIST_FIXTURE: [], + } + + def test_successs_prints_release_urls(upload_settings, stub_repository, capsys): """Print PyPI release URLS for each uploaded package.""" stub_repository.release_urls = lambda packages: {RELEASE_URL, NEW_RELEASE_URL} @@ -619,3 +670,22 @@ helpers.NEW_WHEEL_FIXTURE, ], ) + + +def test_upload_warns_attestations_non_pypi(upload_settings, caplog, stub_response): + upload_settings.repository_config["repository"] = "https://notpypi.example.com" + upload_settings.attestations = True + + # This fails because the attestation isn't a real file, which is fine + # since our functionality under test happens before the failure. + with pytest.raises(exceptions.InvalidDistribution): + upload.upload( + upload_settings, + [helpers.WHEEL_FIXTURE, helpers.WHEEL_FIXTURE + ".foo.attestation"], + ) + + assert ( + "Only PyPI and TestPyPI support attestations; if you experience " + "failures, remove the --attestations flag and re-try this command" + in caplog.messages + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/tests/test_utils.py new/twine-5.1.0/tests/test_utils.py --- old/twine-5.0.0/tests/test_utils.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/tests/test_utils.py 2024-05-16 15:46:47.000000000 +0200 @@ -150,6 +150,31 @@ assert utils.get_repository_from_config(config_file, "pypi") == exp +def test_get_repository_config_url_with_auth(config_file): + repository_url = "https://user:p...@notexisting.python.org/pypi" + exp = { + "repository": "https://notexisting.python.org/pypi", + "username": "user", + "password": "pass", + } + assert utils.get_repository_from_config(config_file, "foo", repository_url) == exp + assert utils.get_repository_from_config(config_file, "pypi", repository_url) == exp + + +@pytest.mark.parametrize( + "input_url, expected_url", + [ + ("https://upload.pypi.org/legacy/", "https://upload.pypi.org/legacy/"), + ( + "https://user:p...@upload.pypi.org/legacy/", + "https://********@upload.pypi.org/legacy/", + ), + ], +) +def test_sanitize_url(input_url: str, expected_url: str) -> None: + assert utils.sanitize_url(input_url) == expected_url + + @pytest.mark.parametrize( "repo_url, message", [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/tests/test_wheel.py new/twine-5.1.0/tests/test_wheel.py --- old/twine-5.0.0/tests/test_wheel.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/tests/test_wheel.py 2024-05-16 15:46:47.000000000 +0200 @@ -96,6 +96,8 @@ with pytest.raises( exceptions.InvalidDistribution, - match=re.escape(f"No METADATA in archive: {whl_file}"), + match=re.escape( + f"No METADATA in archive or METADATA missing 'Metadata-Version': {whl_file}" + ), ): wheel.Wheel(whl_file) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/tox.ini new/twine-5.1.0/tox.ini --- old/twine-5.0.0/tox.ini 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/tox.ini 2024-05-16 15:46:47.000000000 +0200 @@ -79,13 +79,17 @@ [testenv:types] deps = mypy - lxml + # required for report generation. 5.2.1 is forbidden due to an observed + # broken wheel on CPython 3.8: + # https://bugs.launchpad.net/lxml/+bug/2064158 + lxml >= 5.2.0, != 5.2.1 # required for more thorough type declarations keyring >= 22.3 # consider replacing with `mypy --install-types` when # https://github.com/python/mypy/issues/10600 is resolved types-requests commands = + pip list mypy --html-report mypy --txt-report mypy {posargs:twine} python -c 'with open("mypy/index.txt") as f: print(f.read())' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine/cli.py new/twine-5.1.0/twine/cli.py --- old/twine-5.0.0/twine/cli.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/twine/cli.py 2024-05-16 15:46:47.000000000 +0200 @@ -118,6 +118,6 @@ configure_output() - main = registered_commands[args.command].load() # type: ignore[no-untyped-call] # python/importlib_metadata#288 # noqa: E501 + main = registered_commands[args.command].load() return main(args.args) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine/commands/upload.py new/twine-5.1.0/twine/commands/upload.py --- old/twine-5.0.0/twine/commands/upload.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/twine/commands/upload.py 2024-05-16 15:46:47.000000000 +0200 @@ -14,9 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import argparse +import fnmatch import logging import os.path -from typing import Dict, List, cast +from typing import Dict, List, NamedTuple, cast import requests from rich import print @@ -72,9 +73,16 @@ def _make_package( - filename: str, signatures: Dict[str, str], upload_settings: settings.Settings + filename: str, + signatures: Dict[str, str], + attestations: List[str], + upload_settings: settings.Settings, ) -> package_file.PackageFile: - """Create and sign a package, based off of filename, signatures and settings.""" + """Create and sign a package, based off of filename, signatures, and settings. + + Additionally, any supplied attestations are attached to the package when + the settings indicate to do so. + """ package = package_file.PackageFile.from_filename(filename, upload_settings.comment) signed_name = package.signed_basefilename @@ -83,6 +91,17 @@ elif upload_settings.sign: package.sign(upload_settings.sign_with, upload_settings.identity) + # Attestations are only attached if explicitly requested with `--attestations`. + if upload_settings.attestations: + # Passing `--attestations` without any actual attestations present + # indicates user confusion, so we fail rather than silently allowing it. + if not attestations: + raise exceptions.InvalidDistribution( + "Upload with attestations requested, but " + f"{filename} has no associated attestations" + ) + package.add_attestations(attestations) + file_size = utils.get_file_size(package.filename) logger.info(f"{package.filename} ({file_size})") if package.gpg_signature: @@ -91,6 +110,44 @@ return package +class Inputs(NamedTuple): + """Represents structured user inputs.""" + + dists: List[str] + signatures: Dict[str, str] + attestations_by_dist: Dict[str, List[str]] + + +def _split_inputs( + inputs: List[str], +) -> Inputs: + """ + Split the unstructured list of input files provided by the user into groups. + + Three groups are returned: upload files (i.e. dists), signatures, and attestations. + + Upload files are returned as a linear list, signatures are returned as a + dict of ``basename -> path``, and attestations are returned as a dict of + ``dist-path -> [attestation-path]``. + """ + signatures = {os.path.basename(i): i for i in fnmatch.filter(inputs, "*.asc")} + attestations = fnmatch.filter(inputs, "*.*.attestation") + dists = [ + dist + for dist in inputs + if dist not in (set(signatures.values()) | set(attestations)) + ] + + attestations_by_dist = {} + for dist in dists: + dist_basename = os.path.basename(dist) + attestations_by_dist[dist] = [ + a for a in attestations if os.path.basename(a).startswith(dist_basename) + ] + + return Inputs(dists, signatures, attestations_by_dist) + + def upload(upload_settings: settings.Settings, dists: List[str]) -> None: """Upload one or more distributions to a repository, and display the progress. @@ -105,24 +162,40 @@ The configured options related to uploading to a repository. :param dists: The distribution files to upload to the repository. This can also include - ``.asc`` files; the GPG signatures will be added to the corresponding uploads. + ``.asc`` and ``.attestation`` files, which will be added to their respective + file uploads. :raises twine.exceptions.TwineException: The upload failed due to a configuration error. :raises requests.HTTPError: The repository responded with an error. """ - dists = commands._find_dists(dists) - # Determine if the user has passed in pre-signed distributions - signatures = {os.path.basename(d): d for d in dists if d.endswith(".asc")} - uploads = [i for i in dists if not i.endswith(".asc")] - upload_settings.check_repository_url() repository_url = cast(str, upload_settings.repository_config["repository"]) - print(f"Uploading distributions to {repository_url}") + + # Attestations are only supported on PyPI and TestPyPI at the moment. + # We warn instead of failing to allow twine to be used in local testing + # setups (where the PyPI deployment doesn't have a well-known domain). + if upload_settings.attestations and not repository_url.startswith( + (utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY) + ): + logger.warning( + "Only PyPI and TestPyPI support attestations; " + "if you experience failures, remove the --attestations flag and " + "re-try this command" + ) + + dists = commands._find_dists(dists) + # Determine if the user has passed in pre-signed distributions or any attestations. + uploads, signatures, attestations_by_dist = _split_inputs(dists) + + print(f"Uploading distributions to {utils.sanitize_url(repository_url)}") packages_to_upload = [ - _make_package(filename, signatures, upload_settings) for filename in uploads + _make_package( + filename, signatures, attestations_by_dist[filename], upload_settings + ) + for filename in uploads ] if any(p.gpg_signature for p in packages_to_upload): @@ -177,8 +250,8 @@ # redirects as well. if resp.is_redirect: raise exceptions.RedirectDetected.from_args( - repository_url, - resp.headers["location"], + utils.sanitize_url(repository_url), + utils.sanitize_url(resp.headers["location"]), ) if skip_upload(resp, upload_settings.skip_existing, package): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine/package.py new/twine-5.1.0/twine/package.py --- old/twine-5.0.0/twine/package.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/twine/package.py 2024-05-16 15:46:47.000000000 +0200 @@ -13,11 +13,12 @@ # limitations under the License. import hashlib import io +import json import logging import os import re import subprocess -from typing import Dict, NamedTuple, Optional, Sequence, Tuple, Union, cast +from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union, cast import importlib_metadata import pkginfo @@ -78,6 +79,7 @@ self.signed_filename = self.filename + ".asc" self.signed_basefilename = self.basefilename + ".asc" self.gpg_signature: Optional[Tuple[str, bytes]] = None + self.attestations: Optional[List[Dict[Any, str]]] = None hasher = HashManager(filename) hasher.hash() @@ -177,7 +179,7 @@ "requires_external": meta.requires_external, "requires_python": meta.requires_python, # Metadata 2.1 - "provides_extras": meta.provides_extras, + "provides_extra": meta.provides_extras, "description_content_type": meta.description_content_type, # Metadata 2.2 "dynamic": meta.dynamic, @@ -186,6 +188,9 @@ if self.gpg_signature is not None: data["gpg_signature"] = self.gpg_signature + if self.attestations is not None: + data["attestations"] = json.dumps(self.attestations) + # FIPS disables MD5 and Blake2, making the digest values None. Some package # repositories don't allow null values, so this only sends non-null values. # See also: https://github.com/pypa/twine/issues/775 @@ -197,6 +202,19 @@ return data + def add_attestations(self, attestations: List[str]) -> None: + loaded_attestations = [] + for attestation in attestations: + with open(attestation, "rb") as att: + try: + loaded_attestations.append(json.load(att)) + except json.JSONDecodeError: + raise exceptions.InvalidDistribution( + f"invalid JSON in attestation: {attestation}" + ) + + self.attestations = loaded_attestations + def add_gpg_signature( self, signature_filepath: str, signature_filename: str ) -> None: @@ -266,7 +284,7 @@ self._blake_hasher = None try: self._blake_hasher = hashlib.blake2b(digest_size=256 // 8) - except (ValueError, TypeError): + except (ValueError, TypeError, AttributeError): # FIPS mode disables blake2 pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine/repository.py new/twine-5.1.0/twine/repository.py --- old/twine-5.0.0/twine/repository.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/twine/repository.py 2024-05-16 15:46:47.000000000 +0200 @@ -25,7 +25,7 @@ import twine from twine import package as package_file -KEYWORDS_TO_NOT_FLATTEN = {"gpg_signature", "content"} +KEYWORDS_TO_NOT_FLATTEN = {"gpg_signature", "attestations", "content"} LEGACY_PYPI = "https://pypi.python.org/" LEGACY_TEST_PYPI = "https://testpypi.python.org/" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine/settings.py new/twine-5.1.0/twine/settings.py --- old/twine-5.0.0/twine/settings.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/twine/settings.py 2024-05-16 15:46:47.000000000 +0200 @@ -45,6 +45,7 @@ def __init__( self, *, + attestations: bool = False, sign: bool = False, sign_with: str = "gpg", identity: Optional[str] = None, @@ -64,6 +65,8 @@ ) -> None: """Initialize our settings instance. + :param attestations: + Whether the package file should be uploaded with attestations. :param sign: Configure whether the package file should be signed. :param sign_with: @@ -114,6 +117,7 @@ repository_name=repository_name, repository_url=repository_url, ) + self.attestations = attestations self._handle_package_signing( sign=sign, sign_with=sign_with, @@ -176,6 +180,12 @@ "(Can also be set via %(env)s environment variable.)", ) parser.add_argument( + "--attestations", + action="store_true", + default=False, + help="Upload each file's associated attestations.", + ) + parser.add_argument( "-s", "--sign", action="store_true", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine/utils.py new/twine-5.1.0/twine/utils.py --- old/twine-5.0.0/twine/utils.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/twine/utils.py 2024-05-16 15:46:47.000000000 +0200 @@ -100,6 +100,24 @@ return dict(config) +def sanitize_url(url: str) -> str: + """Sanitize a URL. + + Sanitize URLs, removing any user:password combinations and replacing them with + asterisks. Returns the original URL if the string is a non-matching pattern. + + :param url: + str containing a URL to sanitize. + + return: + str either sanitized or as entered depending on pattern match. + """ + uri = rfc3986.urlparse(url) + if uri.userinfo: + return cast(str, uri.copy_with(userinfo="*" * 8).unsplit()) + return url + + def _validate_repository_url(repository_url: str) -> None: """Validate the given url for allowed schemes and components.""" # Allowed schemes are http and https, based on whether the repository @@ -126,11 +144,7 @@ # Prefer CLI `repository_url` over `repository` or .pypirc if repository_url: _validate_repository_url(repository_url) - return { - "repository": repository_url, - "username": None, - "password": None, - } + return _config_from_repository_url(repository_url) try: config = get_config(config_file)[repository] @@ -154,6 +168,17 @@ } +def _config_from_repository_url(url: str) -> RepositoryConfig: + parsed = urlparse(url) + config = {"repository": url, "username": None, "password": None} + if parsed.username: + config["username"] = parsed.username + config["password"] = parsed.password + config["repository"] = urlunparse((parsed.scheme, parsed.hostname) + parsed[2:]) + config["repository"] = normalize_repository_url(cast(str, config["repository"])) + return config + + def normalize_repository_url(url: str) -> str: parsed = urlparse(url) if parsed.netloc in _HOSTNAMES: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine/wheel.py new/twine-5.1.0/twine/wheel.py --- old/twine-5.0.0/twine/wheel.py 2024-02-11 14:45:06.000000000 +0100 +++ new/twine-5.1.0/twine/wheel.py 2024-05-16 15:46:47.000000000 +0200 @@ -16,6 +16,7 @@ import re import zipfile from typing import List, Optional +from typing import cast as type_cast from pkginfo import distribution @@ -72,20 +73,27 @@ "Not a known archive format for file: %s" % fqn ) + searched_files: List[str] = [] try: for path in self.find_candidate_metadata_files(names): candidate = "/".join(path) data = read_file(candidate) if b"Metadata-Version" in data: return data + searched_files.append(candidate) finally: archive.close() - raise exceptions.InvalidDistribution("No METADATA in archive: %s" % fqn) + raise exceptions.InvalidDistribution( + "No METADATA in archive or METADATA missing 'Metadata-Version': " + "%s (searched %s)" % (fqn, ",".join(searched_files)) + ) def parse(self, data: bytes) -> None: super().parse(data) fp = io.StringIO(data.decode("utf-8", errors="replace")) + # msg is ``email.message.Message`` which is a legacy API documented + # here: https://docs.python.org/3/library/email.compat32-message.html msg = distribution.parse(fp) - self.description = msg.get_payload() + self.description = type_cast(str, msg.get_payload()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine.egg-info/PKG-INFO new/twine-5.1.0/twine.egg-info/PKG-INFO --- old/twine-5.0.0/twine.egg-info/PKG-INFO 2024-02-11 14:45:14.000000000 +0100 +++ new/twine-5.1.0/twine.egg-info/PKG-INFO 2024-05-16 15:46:51.000000000 +0200 @@ -1,10 +1,9 @@ Metadata-Version: 2.1 Name: twine -Version: 5.0.0 +Version: 5.1.0 Summary: Collection of utilities for publishing packages on PyPI -Home-page: https://twine.readthedocs.io/ -Author: Donald Stufft and individual contributors -Author-email: don...@stufft.io +Author-email: Donald Stufft and individual contributors <don...@stufft.io> +Project-URL: Homepage, https://twine.readthedocs.io/ Project-URL: Source, https://github.com/pypa/twine/ Project-URL: Documentation, https://twine.readthedocs.io/en/latest/ Project-URL: Packaging tutorial, https://packaging.python.org/tutorials/packaging-projects/ @@ -38,18 +37,20 @@ Requires-Dist: rfc3986>=1.4.0 Requires-Dist: rich>=12.0.0 -.. image:: https://img.shields.io/pypi/v/twine.svg +.. |twine-version| image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine -.. image:: https://img.shields.io/pypi/pyversions/twine.svg +.. |python-versions| image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine -.. image:: https://img.shields.io/readthedocs/twine +.. |docs-badge| image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io -.. image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main +.. |build-badge| image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions +|twine-version| |python-versions| |docs-badge| |build-badge| + twine ===== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/twine-5.0.0/twine.egg-info/SOURCES.txt new/twine-5.1.0/twine.egg-info/SOURCES.txt --- old/twine-5.0.0/twine.egg-info/SOURCES.txt 2024-02-11 14:45:14.000000000 +0100 +++ new/twine-5.1.0/twine.egg-info/SOURCES.txt 2024-05-16 15:46:51.000000000 +0200 @@ -10,7 +10,6 @@ mypy.ini pyproject.toml pytest.ini -setup.cfg tox.ini .github/dependabot.yml .github/ISSUE_TEMPLATE/01_upload_failed.yml