diff --git a/src/fromager/pkgmetadata/pep639.py b/src/fromager/pkgmetadata/pep639.py index d01192094..2817f4a11 100644 --- a/src/fromager/pkgmetadata/pep639.py +++ b/src/fromager/pkgmetadata/pep639.py @@ -6,7 +6,8 @@ - https://spdx.org/licenses/ """ -from license_expression import LicenseExpression, get_spdx_licensing +from license_expression import ExpressionError, LicenseExpression, get_spdx_licensing +from packaging.licenses import InvalidLicenseExpression, canonicalize_license_expression from packaging.metadata import Metadata @@ -62,10 +63,17 @@ def license_from_metadata_values( def _parse_spdx(text: str, *, simplify: bool = False) -> LicenseExpression: - """Parse, validate, and simplify a SPDX license expression""" - # LicenseRef are references to non-SPDX licenses - validate = not text.startswith("LicenseRef-") - expr = _SPDX.parse(text, validate=validate) + """Parse, validate, and simplify a SPDX license expression + + `canonicalize_license_expression()` validates, so the rules match + PEP 639 exactly (e.g. ``LicenseRef-`` anywhere, but only SPDX + exceptions after ``WITH``); license-expression only builds the object. + """ + try: + canonical = canonicalize_license_expression(text) + except InvalidLicenseExpression as err: + raise ExpressionError(str(err)) from err + expr = _SPDX.parse(canonical, validate=False) if simplify: expr = expr.simplify() return expr diff --git a/tests/test_pkgmetadata.py b/tests/test_pkgmetadata.py index 1bb408500..ffc9384c2 100644 --- a/tests/test_pkgmetadata.py +++ b/tests/test_pkgmetadata.py @@ -1,4 +1,5 @@ import pytest +from license_expression import ExpressionError from packaging.metadata import Metadata, RawMetadata from fromager.pkgmetadata import ( @@ -82,6 +83,14 @@ def test_project_urls_from_metadata( ["metadata", "expected"], [ [md("MIT AND MIT or MIT"), "MIT"], + # LicenseRef is valid anywhere in a PEP 639 expression + [md("MIT OR LicenseRef-Proprietary"), "LicenseRef-Proprietary OR MIT"], + [md("LicenseRef-Proprietary"), "LicenseRef-Proprietary"], + # exception keys must not be reported as unknown next to a LicenseRef + [ + md("MIT WITH LLVM-exception OR LicenseRef-z"), + "LicenseRef-z OR MIT WITH LLVM-exception", + ], [md(license="MIT License"), "MIT"], [md(license="MIT and PSF-2.0"), "MIT AND PSF-2.0"], [md(license="BSD 3-Clause License"), "BSD-3-Clause"], @@ -133,3 +142,28 @@ def test_license_from_metadata_errors( license_text=license, classifiers=classifiers, ) + + +@pytest.mark.parametrize( + ["license_expression", "match"], + [ + # a leading LicenseRef must not disable validation of other keys + ("LicenseRef-Foo OR Apache 2", "Apache 2"), + # SPDX idstrings only allow letters, digits, "." and "-" + ("LicenseRef-a_b", "(?i)LicenseRef-a_b"), + ("MIT WITH NotAnException", "(?i)NotAnException"), + # PEP 639 only allows SPDX exceptions after WITH + ("Apache-2.0 WITH LicenseRef-x", "(?i)LicenseRef-x"), + ], +) +def test_license_from_metadata_invalid_expression( + license_expression: str, match: str +) -> None: + with pytest.raises(ExpressionError, match=match): + license_from_metadata_values(license_expression=license_expression) + + +def test_license_from_metadata_canonicalizes_licenseref_prefix() -> None: + # PEP 639 matches the LicenseRef- prefix case-insensitively + expr = license_from_metadata_values(license_expression="licenseref-foo") + assert str(expr) == "LicenseRef-foo"