Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
32 changes: 28 additions & 4 deletions src/validate_pyproject/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Copy link
Owner

@abravalheri abravalheri Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to cover this case...

  • Newer versions of setuptools use this strategy for exposing the _vendor directory:

    sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path])  # fmt: skip

    So packaging will be available directly, without the need for prefixing it with setuptools._vendor.

  • Older versions of setuptools will contain older versions of packaging so importing setuptools._vendor.packaging.licenses will always fail, right?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just wondering now if we should add right at the beginning of the file:

try:
    import setuptools  # Just interested on the side effect in `sys.path`
except ImportError:
    pass

... however, this may:

  1. Hurt performance
  2. Cause version conflict errors when there would be no need for modifying sys.path

So maybe the best for now is to leave the implementation as suggested in the current state of the PR. We can revisit this later if we receive bug reports.


def SPDX(value: str) -> bool:
"""See :ref:`PyPA's license specifiers <pypa:#license>`
(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
File renamed without changes.
1 change: 1 addition & 0 deletions tests/invalid-examples/simple/pep639.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`project.license` must be valid exactly by one definition (0 matches found)
5 changes: 5 additions & 0 deletions tests/invalid-examples/simple/pep639.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
name = "example"
version = "1.2.3"
license = "Apache Software License" # should be "Apache-2.0"
license-files = ["licenses/LICENSE"]
48 changes: 48 additions & 0 deletions tests/test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down