diff --git a/README.md b/README.md index 4a6ba577..6de03844 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,7 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 - [`PP002`](https://learn.scientific-python.org/development/guides/packaging-simple#PP002): Has a proper build-system table - [`PP003`](https://learn.scientific-python.org/development/guides/packaging-classic#PP003): Does not list wheel as a build-dep - [`PP004`](https://learn.scientific-python.org/development/guides/packaging-simple#PP004): Does not upper cap Python requires +- [`PP005`](https://learn.scientific-python.org/development/guides/packaging-simple#PP005): Using SPDX project.license should not use deprecated trove classifiers - [`PP301`](https://learn.scientific-python.org/development/guides/pytest#PP301): Has pytest in pyproject - [`PP302`](https://learn.scientific-python.org/development/guides/pytest#PP302): Sets a minimum pytest to at least 6 - [`PP303`](https://learn.scientific-python.org/development/guides/pytest#PP303): Sets the test paths diff --git a/docs/_includes/pyproject.md b/docs/_includes/pyproject.md index bf8e6b5f..41fedee2 100644 --- a/docs/_includes/pyproject.md +++ b/docs/_includes/pyproject.md @@ -45,6 +45,22 @@ You can read more about each field, and all allowed fields, in or [Whey](https://whey.readthedocs.io/en/latest/configuration.html). Note that "Homepage" is special, and replaces the old url setting. +### License + +The license can be done one of two ways. The classic convention (shown above) +uses one or more [Trove Classifiers][] to specify the license. The other way is +to use the `license` field and an [SPDX identifier expression][spdx]: + +```toml +license = "BSD-3-Clause" +``` + +You can also specify files to include with the `license-files` field. + +You should not include the `License ::` classifiers if you use the `license` +field {% rr PP007 %}. Some backends do not support this fully yet (notably +Poetry and Setuptools). + ### Extras It is recommended to use extras instead of or in addition to making requirement @@ -85,3 +101,5 @@ function, followed by a colon, then the function to call. If you use work to call the app (`__name__` will be `"__main__"` in that case). [metadata]: https://packaging.python.org/en/latest/specifications/core-metadata/ +[trove classifiers]: https://pypi.org/classifiers/ +[spdx]: https://spdx.org/licenses diff --git a/pyproject.toml b/pyproject.toml index 2add3fb1..8fa544ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,13 +10,13 @@ authors = [ ] description = "Review repos for compliance to the Scientific-Python development guidelines" requires-python = ">=3.10" +license-expression = 'BSD-3-Clause' classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: WebAssembly :: Emscripten", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", diff --git a/src/sp_repo_review/checks/pyproject.py b/src/sp_repo_review/checks/pyproject.py index ef0bbc41..8f6be23b 100644 --- a/src/sp_repo_review/checks/pyproject.py +++ b/src/sp_repo_review/checks/pyproject.py @@ -94,6 +94,29 @@ def check(pyproject: dict[str, Any], package: Traversable) -> bool | None: return None +class PP005(PyProject): + "Using SPDX project.license should not use deprecated trove classifiers" + + requires = {"PY001"} + url = mk_url("packaging-simple") + + @staticmethod + def check(pyproject: dict[str, Any]) -> bool | None: + """ + If you use SPDX identifiers in `project.license`, then all the `License ::` + classifiers are deprecated. + + See https://packaging.python.org/en/latest/specifications/core-metadata/#license-expression + """ + match pyproject: + case {"project": {"license": str(), "classifiers": classifiers}}: + return all(not c.startswith("License ::") for c in classifiers) + case {"project": {"license": str()}}: + return True + case _: + return None + + class PP301(PyProject): "Has pytest in pyproject" diff --git a/tests/packages/ruff_extend/ruff.toml b/tests/packages/ruff_extend/ruff.toml index b8c006e9..00c86cbb 100644 --- a/tests/packages/ruff_extend/ruff.toml +++ b/tests/packages/ruff_extend/ruff.toml @@ -1,2 +1,2 @@ extend = "pyproject.toml" -target-version = "3.9" +target-version = "py39" diff --git a/tests/test_pyproject.py b/tests/test_pyproject.py index ff283b8a..abf65037 100644 --- a/tests/test_pyproject.py +++ b/tests/test_pyproject.py @@ -113,6 +113,55 @@ def test_PP004_not_present(tmp_path: Path): assert compute_check("PP004", pyproject={}, package=tmp_path).result is None +def test_PP005_no_license(): + toml = toml_loads(""" + [project] + license.text = "MIT" + classifiers = ["License :: OSI Approved :: MIT License"] + """) + + assert compute_check("PP005", pyproject=toml).result is None + + +def test_PP005_pass(): + toml = toml_loads(""" + [project] + license = "MIT" + """) + + assert compute_check("PP005", pyproject=toml).result + + +def test_PP005_pass_empty_classifiers(): + toml = toml_loads(""" + [project] + license = "MIT" + classifiers = [] + """) + + assert compute_check("PP005", pyproject=toml).result + + +def test_PP005_pass_other_classifiers(): + toml = toml_loads(""" + [project] + license = "MIT" + classifiers = ["Something :: Else"] + """) + + assert compute_check("PP005", pyproject=toml).result + + +def test_PP005_both(): + toml = toml_loads(""" + [project] + license = "MIT" + classifiers = ["License :: OSI Approved :: MIT License"] + """) + + assert not compute_check("PP005", pyproject=toml).result + + def test_PP302_okay_intstr(): toml = toml_loads(""" [tool.pytest.ini_options]