Skip to content

Commit f45606b

Browse files
authored
Validate SPDX license expressions (#217)
2 parents 73977d9 + 7a8c7dc commit f45606b

File tree

6 files changed

+77
-5
lines changed

6 files changed

+77
-5
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Download = "https://pypi.org/project/validate-pyproject/#files"
3030

3131
[project.optional-dependencies]
3232
all = [
33-
"packaging>=20.4",
33+
"packaging>=24.2",
3434
"tomli>=1.2.1; python_version<'3.11'",
3535
"trove-classifiers>=2021.10.20",
3636
]

src/validate_pyproject/formats.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,25 @@ def int(value: builtins.int) -> bool:
378378
return -(2**63) <= value < 2**63
379379

380380

381-
def SPDX(value: str) -> bool:
382-
"""Should validate eventually"""
383-
# TODO: validate conditional to the presence of (the right version) of packaging
384-
return True
381+
try:
382+
from packaging import licenses as _licenses
383+
384+
def SPDX(value: str) -> bool:
385+
"""See :ref:`PyPA's License-Expression specification
386+
<pypa:core-metadata-license-expression>` (added in :pep:`639`).
387+
"""
388+
try:
389+
_licenses.canonicalize_license_expression(value)
390+
return True
391+
except _licenses.InvalidLicenseExpression:
392+
return False
393+
394+
except ImportError: # pragma: no cover
395+
_logger.warning(
396+
"Could not find an up-to-date installation of `packaging`. "
397+
"License expressions might not be validated. "
398+
"To enforce validation, please install `packaging>=24.2`."
399+
)
400+
401+
def SPDX(value: str) -> bool:
402+
return True
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`project.license` must be valid exactly by one definition (0 matches found)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[project]
2+
name = "example"
3+
version = "1.2.3"
4+
license = "Apache Software License" # should be "Apache-2.0"
5+
license-files = ["licenses/LICENSE"]

tests/test_formats.py

+48
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,54 @@ def test_invalid_module_name_relaxed(example):
293293
assert formats.python_module_name_relaxed(example) is False
294294

295295

296+
@pytest.mark.parametrize(
297+
"example",
298+
[
299+
"MIT",
300+
"Bsd-3-clause",
301+
"mit and (apache-2.0 or bsd-2-clause)",
302+
"MIT OR GPL-2.0-or-later OR (FSFUL AND BSD-2-Clause)",
303+
"GPL-3.0-only WITH Classpath-exception-2.0 OR BSD-3-Clause",
304+
"LicenseRef-Special-License OR CC0-1.0 OR Unlicense",
305+
"LicenseRef-Public-Domain",
306+
"licenseref-proprietary",
307+
"LicenseRef-Beerware-4.2",
308+
"(LicenseRef-Special-License OR LicenseRef-OtherLicense) OR Unlicense",
309+
],
310+
)
311+
def test_valid_pep639_license_expression(example):
312+
assert formats.SPDX(example) is True
313+
314+
315+
@pytest.mark.parametrize(
316+
"example",
317+
[
318+
"",
319+
"Use-it-after-midnight",
320+
"LicenseRef-License with spaces",
321+
"LicenseRef-License_with_underscores",
322+
"or",
323+
"and",
324+
"with",
325+
"mit or",
326+
"mit and",
327+
"mit with",
328+
"or mit",
329+
"and mit",
330+
"with mit",
331+
"(mit",
332+
"mit)",
333+
"mit or or apache-2.0",
334+
# Missing an operator before `(`.
335+
"mit or apache-2.0 (bsd-3-clause and MPL-2.0)",
336+
# "2-BSD-Clause is not a valid license.
337+
"Apache-2.0 OR 2-BSD-Clause",
338+
],
339+
)
340+
def test_invalid_pep639_license_expression(example):
341+
assert formats.SPDX(example) is False
342+
343+
296344
class TestClassifiers:
297345
"""The ``_TroveClassifier`` class and ``_download_classifiers`` are part of the
298346
private API and therefore need to be tested.

0 commit comments

Comments
 (0)