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

Reply via email to