diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5d07d193..25ac6872 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,6 +48,13 @@ jobs: - ubuntu-latest - macos-latest - windows-latest + tox-environment: + - py + include: + # Test with the oldest supported ``packaging`` version. + - platform: ubuntu-latest + python-version: "3.8" + tox-environment: py-packaging240 runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4.2.2 @@ -59,7 +66,7 @@ jobs: - name: Run type-checking run: python -m tox -e types - name: Run tests - run: python -m tox -e py + run: python -m tox -e ${{ matrix.tox-environment }} # Because the tests can be flaky, they shouldn't be required for merge, but # it's still helpful to run them on PRs. See: diff --git a/changelog/1217.bugfix.rst b/changelog/1217.bugfix.rst new file mode 100644 index 00000000..46a34e32 --- /dev/null +++ b/changelog/1217.bugfix.rst @@ -0,0 +1,2 @@ +Fix compatibility kludge for invalid License-File metadata entries emitted by +build backends to work also with ``packaging`` version 24.0. diff --git a/tests/fixtures/everything.metadata23 b/tests/fixtures/everything.metadata23 new file mode 100644 index 00000000..2e02c7ce --- /dev/null +++ b/tests/fixtures/everything.metadata23 @@ -0,0 +1,41 @@ +Metadata-Version: 2.3 +Name: BeagleVote +Version: 1.0a2 +Platform: ObscureUnix +Platform: RareDOS +Supported-Platform: RedHat 7.2 +Supported-Platform: i386-win32-2791 +Summary: A module for collecting votes from beagles. +Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM +Keywords: dog,puppy,voting,election +Home-page: http://www.example.com/~cschultz/bvote/ +Download-URL: …/BeagleVote-0.45.tgz +Author: C. Schultz, Universal Features Syndicate, + Los Angeles, CA +Author-email: "C. Schultz" +Maintainer: C. Schultz, Universal Features Syndicate, + Los Angeles, CA +Maintainer-email: "C. Schultz" +License: This software may only be obtained by sending the + author a postcard, and then the user promises not + to redistribute it. +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console (Text Based) +Provides-Extra: pdf +Requires-Dist: reportlab; extra == 'pdf' +Requires-Dist: pkginfo +Requires-Dist: PasteDeploy +Requires-Dist: zope.interface (>3.5.0) +Requires-Dist: pywin32 >1.0; sys_platform == 'win32' +Requires-Python: >=3 +Requires-External: C +Requires-External: libpng (>=1.5) +Requires-External: make; sys_platform != "win32" +Project-URL: Bug Tracker, http://bitbucket.org/tarek/distribute/issues/ +Project-URL: Documentation, https://example.com/BeagleVote +Provides-Dist: OtherProject +Provides-Dist: AnotherProject (3.4) +Provides-Dist: virtual_package; python_version >= "3.4" +Dynamic: Obsoletes-Dist + +This description intentionally left blank. diff --git a/tests/fixtures/everything.metadata b/tests/fixtures/everything.metadata24 similarity index 100% rename from tests/fixtures/everything.metadata rename to tests/fixtures/everything.metadata24 diff --git a/tests/test_package.py b/tests/test_package.py index d843fa17..78f40a55 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -14,9 +14,11 @@ import json import string +import packaging import pretend import pytest from packaging import metadata +from packaging import version from twine import exceptions from twine import package as package_file @@ -175,6 +177,10 @@ def test_package_safe_name_is_correct(pkg_name, expected_name): def test_metadata_keys_consistency(): """Check that the translation keys exist in the respective ``TypedDict``.""" raw_keys = metadata.RawMetadata.__annotations__.keys() + if version.Version(packaging.__version__) < version.Version("24.1"): + # ``packaging`` version 24.0 and earlier do not know about the + # License-Expression and License-File metadata fields yet. + raw_keys = raw_keys | set(("license_files", "license_expression")) assert set(package_file._RAW_TO_PACKAGE_METADATA.keys()).issubset(raw_keys) package_keys = package_file.PackageMetadata.__annotations__.keys() assert set(package_file._RAW_TO_PACKAGE_METADATA.values()).issubset(package_keys) @@ -434,32 +440,37 @@ def test_package_from_unrecognized_file_error(): assert "Unknown distribution format" in err.value.args[0] -@pytest.mark.parametrize( - "read_data, filtered", - [ - pytest.param( - "Metadata-Version: 2.1\n" - "Name: test-package\n" - "Version: 1.0.0\n" - "License-File: LICENSE\n", - True, - id="invalid License-File", - ), - pytest.param( - "Metadata-Version: 2.4\n" - "Name: test-package\n" - "Version: 1.0.0\n" - "License-File: LICENSE\n", - False, - id="valid License-File", - ), - ], -) -def test_setuptools_license_file(read_data, filtered, monkeypatch): +def test_setuptools_license_file_invalid(monkeypatch): """Drop License-File metadata entries if Metadata-Version is less than 2.4.""" + read_data = ( + "Metadata-Version: 2.1\n" + "Name: test-package\n" + "Version: 1.0.0\n" + "License-File: LICENSE\n" + ) + monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data) + filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" + + package = package_file.PackageFile.from_filename(filename, comment=None) + meta = package.metadata_dictionary() + assert "license_file" not in meta + + +@pytest.mark.skipif( + version.Version(packaging.__version__) < version.Version("24.1"), + reason="packaging is too old", +) +def test_setuptools_license_file_valid(monkeypatch): + """License-File metadata entries are kept when Metadata-Version is 2.4.""" + read_data = ( + "Metadata-Version: 2.4\n" + "Name: test-package\n" + "Version: 1.0.0\n" + "License-File: LICENSE\n" + ) monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data) filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" package = package_file.PackageFile.from_filename(filename, comment=None) meta = package.metadata_dictionary() - assert filtered != ("license_file" in meta) + assert "license_file" in meta diff --git a/tests/test_repository.py b/tests/test_repository.py index 7526a36f..e89a3712 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -14,9 +14,11 @@ import logging from contextlib import contextmanager +import packaging import pretend import pytest import requests +from packaging import version from twine import package from twine import repository @@ -61,8 +63,12 @@ def test_iterables_are_flattened(): def test_all_metadata_fields_are_flattened(monkeypatch): """Verify that package metadata fields are correctly flattened.""" - # This file contains all metadata fields known up to metadata version 2.4. - metadata = open("tests/fixtures/everything.metadata") + if version.Version(packaging.__version__) < version.Version("24.1"): + # All metadata fields up to metadata version 2.3 + metadata = open("tests/fixtures/everything.metadata23") + else: + # All metadata fields up to metadata version 2.4 + metadata = open("tests/fixtures/everything.metadata24") monkeypatch.setattr(package.wheel.Wheel, "read", metadata.read) filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" data = package.PackageFile.from_filename( diff --git a/tox.ini b/tox.ini index bf36c319..40b870ec 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.8 -envlist = lint,types,py{38,39,310,311,312,313},integration,docs +envlist = lint,types,py{38,39,310,311,312,313}{,-packaging240},integration,docs isolated_build = True [testenv] @@ -9,6 +9,7 @@ deps = pytest pytest-socket coverage + packaging240: packaging==24.0 passenv = PYTEST_ADDOPTS setenv = diff --git a/twine/package.py b/twine/package.py index 831bcdbd..3bd28d0e 100644 --- a/twine/package.py +++ b/twine/package.py @@ -214,14 +214,7 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile": # Parse and validate metadata. meta, unparsed = metadata.parse_email(data) - if unparsed: - raise exceptions.InvalidDistribution( - "Invalid distribution metadata: {}".format( - "; ".join( - f"unrecognized or malformed field {key!r}" for key in unparsed - ) - ) - ) + # setuptools emits License-File metadata fields while declaring # Metadata-Version 2.1. This is invalid because the metadata # specification does not allow to add arbitrary fields, and because @@ -232,6 +225,22 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile": # than 2.4. if version.Version(meta.get("metadata_version", "0")) < version.Version("2.4"): meta.pop("license_files", None) + # Support for metadata version 2.4 requires packaging version 24.1 + # or later. When parsing metadata with an older packaging, the + # invalid License-File fields are not understood and added to the + # unparsed dictionary. Remove them to avoid triggering the + # following check. + unparsed.pop("license-file", None) + + if unparsed: + raise exceptions.InvalidDistribution( + "Invalid distribution metadata: {}".format( + "; ".join( + f"unrecognized or malformed field {key!r}" for key in unparsed + ) + ) + ) + try: metadata.Metadata.from_raw(meta) except metadata.ExceptionGroup as group: