From f792a3a66634c476d00b78e0effa576fd92f79c7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:37:28 +0100 Subject: [PATCH 1/5] Validate SPDX license expressions --- pyproject.toml | 2 +- src/validate_pyproject/formats.py | 32 +++++++++++-- .../simple/{pep638.toml => pep639.toml} | 0 .../invalid-examples/simple/pep639.errors.txt | 1 + tests/invalid-examples/simple/pep639.toml | 5 ++ tests/test_formats.py | 48 +++++++++++++++++++ 6 files changed, 83 insertions(+), 5 deletions(-) rename tests/examples/simple/{pep638.toml => pep639.toml} (100%) create mode 100644 tests/invalid-examples/simple/pep639.errors.txt create mode 100644 tests/invalid-examples/simple/pep639.toml diff --git a/pyproject.toml b/pyproject.toml index 2550c5b..e7c901c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ Download = "https://pypi.org/project/validate-pyproject/#files" [project.optional-dependencies] all = [ - "packaging>=20.4", + "packaging>=24.2", "tomli>=1.2.1; python_version<'3.11'", "trove-classifiers>=2021.10.20", ] diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py index 39320a8..dc8cbda 100644 --- a/src/validate_pyproject/formats.py +++ b/src/validate_pyproject/formats.py @@ -378,7 +378,31 @@ def int(value: builtins.int) -> bool: return -(2**63) <= value < 2**63 -def SPDX(value: str) -> bool: - """Should validate eventually""" - # TODO: validate conditional to the presence of (the right version) of packaging - return True +try: + try: + from packaging import licenses as _licenses + except ImportError: # pragma: no cover + # let's try setuptools vendored version + from setuptools._vendor.packaging import ( # type: ignore[no-redef] + licenses as _licenses, + ) + + def SPDX(value: str) -> bool: + """See :ref:`PyPA's license specifiers ` + (amended in :pep:`639`). + """ + try: + _licenses.canonicalize_license_expression(value) + return True + except _licenses.InvalidLicenseExpression: + return False + +except ImportError: # pragma: no cover + _logger.warning( + "Could not find an installation of `packaging`. License expressions " + "might not be validated. " + "To enforce validation, please install `packaging`." + ) + + def SPDX(value: str) -> bool: + return True diff --git a/tests/examples/simple/pep638.toml b/tests/examples/simple/pep639.toml similarity index 100% rename from tests/examples/simple/pep638.toml rename to tests/examples/simple/pep639.toml diff --git a/tests/invalid-examples/simple/pep639.errors.txt b/tests/invalid-examples/simple/pep639.errors.txt new file mode 100644 index 0000000..e371926 --- /dev/null +++ b/tests/invalid-examples/simple/pep639.errors.txt @@ -0,0 +1 @@ +`project.license` must be valid exactly by one definition (0 matches found) diff --git a/tests/invalid-examples/simple/pep639.toml b/tests/invalid-examples/simple/pep639.toml new file mode 100644 index 0000000..8b30929 --- /dev/null +++ b/tests/invalid-examples/simple/pep639.toml @@ -0,0 +1,5 @@ +[project] +name = "example" +version = "1.2.3" +license = "Apache Software License" # should be "Apache-2.0" +license-files = ["licenses/LICENSE"] diff --git a/tests/test_formats.py b/tests/test_formats.py index bd6965f..cdff3ee 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -293,6 +293,54 @@ def test_invalid_module_name_relaxed(example): assert formats.python_module_name_relaxed(example) is False +@pytest.mark.parametrize( + "example", + [ + "MIT", + "Bsd-3-clause", + "mit and (apache-2.0 or bsd-2-clause)", + "MIT OR GPL-2.0-or-later OR (FSFUL AND BSD-2-Clause)", + "GPL-3.0-only WITH Classpath-exception-2.0 OR BSD-3-Clause", + "LicenseRef-Special-License OR CC0-1.0 OR Unlicense", + "LicenseRef-Public-Domain", + "licenseref-proprietary", + "LicenseRef-Beerware-4.2", + "(LicenseRef-Special-License OR LicenseRef-OtherLicense) OR Unlicense", + ], +) +def test_valid_pep639_license_expression(example): + assert formats.SPDX(example) is True + + +@pytest.mark.parametrize( + "example", + [ + "", + "Use-it-after-midnight", + "LicenseRef-License with spaces", + "LicenseRef-License_with_underscores", + "or", + "and", + "with", + "mit or", + "mit and", + "mit with", + "or mit", + "and mit", + "with mit", + "(mit", + "mit)", + "mit or or apache-2.0", + # Missing an operator before `(`. + "mit or apache-2.0 (bsd-3-clause and MPL-2.0)", + # "2-BSD-Clause is not a valid license. + "Apache-2.0 OR 2-BSD-Clause", + ], +) +def test_invalid_pep639_license_expression(example): + assert formats.SPDX(example) is False + + class TestClassifiers: """The ``_TroveClassifier`` class and ``_download_classifiers`` are part of the private API and therefore need to be tested. From ab383469d77b0dbe2770ff7923715b04c1651d24 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:09:56 +0100 Subject: [PATCH 2/5] Remove setuptools fallback --- src/validate_pyproject/formats.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py index dc8cbda..0867da5 100644 --- a/src/validate_pyproject/formats.py +++ b/src/validate_pyproject/formats.py @@ -379,13 +379,7 @@ def int(value: builtins.int) -> bool: try: - try: - from packaging import licenses as _licenses - except ImportError: # pragma: no cover - # let's try setuptools vendored version - from setuptools._vendor.packaging import ( # type: ignore[no-redef] - licenses as _licenses, - ) + from packaging import licenses as _licenses def SPDX(value: str) -> bool: """See :ref:`PyPA's license specifiers ` From 216c44690018ec077bf3a004e9883e0d645f3c91 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:12:34 +0100 Subject: [PATCH 3/5] Update formats.py Co-authored-by: Anderson Bravalheri --- src/validate_pyproject/formats.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py index 0867da5..7fd0dff 100644 --- a/src/validate_pyproject/formats.py +++ b/src/validate_pyproject/formats.py @@ -393,9 +393,9 @@ def SPDX(value: str) -> bool: except ImportError: # pragma: no cover _logger.warning( - "Could not find an installation of `packaging`. License expressions " - "might not be validated. " - "To enforce validation, please install `packaging`." + "Could not find an up-to-date installation of `packaging`. " + "License expressions might not be validated. " + "To enforce validation, please install `packaging>=24.2`." ) def SPDX(value: str) -> bool: From 4eb2516bc099b0961afb0728f9bda6b34da08ea6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:23:24 +0100 Subject: [PATCH 4/5] Fix link --- src/validate_pyproject/formats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py index 7fd0dff..039556f 100644 --- a/src/validate_pyproject/formats.py +++ b/src/validate_pyproject/formats.py @@ -382,8 +382,8 @@ def int(value: builtins.int) -> bool: from packaging import licenses as _licenses def SPDX(value: str) -> bool: - """See :ref:`PyPA's license specifiers ` - (amended in :pep:`639`). + """See ``license`` in the :ref:`PyPA's pyproject project metadata specification + ` (amended in :pep:`639`). """ try: _licenses.canonicalize_license_expression(value) From 7a8c7dce05857f1011b2c6f235b930d0f0611a60 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:29:49 +0100 Subject: [PATCH 5/5] Use better link target --- src/validate_pyproject/formats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validate_pyproject/formats.py b/src/validate_pyproject/formats.py index 039556f..1cf4a46 100644 --- a/src/validate_pyproject/formats.py +++ b/src/validate_pyproject/formats.py @@ -382,8 +382,8 @@ def int(value: builtins.int) -> bool: from packaging import licenses as _licenses def SPDX(value: str) -> bool: - """See ``license`` in the :ref:`PyPA's pyproject project metadata specification - ` (amended in :pep:`639`). + """See :ref:`PyPA's License-Expression specification + ` (added in :pep:`639`). """ try: _licenses.canonicalize_license_expression(value)