diff --git a/newsfragments/4833.feature.rst b/newsfragments/4833.feature.rst new file mode 100644 index 0000000000..d8801becf7 --- /dev/null +++ b/newsfragments/4833.feature.rst @@ -0,0 +1,2 @@ +Added exception (or warning) when deprecated license classifiers are used, +according to `PEP 639 `_. diff --git a/setuptools/dist.py b/setuptools/dist.py index 27e8095709..d202dbf504 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -28,6 +28,7 @@ from ._reqs import _StrOrIter from .config import pyprojecttoml, setupcfg from .discovery import ConfigDiscovery +from .errors import InvalidConfigError from .monkey import get_unpatched from .warnings import InformationOnly, SetuptoolsDeprecationWarning @@ -406,22 +407,32 @@ def _normalize_requires(self): def _finalize_license_expression(self) -> None: """Normalize license and license_expression.""" + classifiers = self.metadata.get_classifiers() + license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")] + license_expr = self.metadata.license_expression if license_expr: normalized = canonicalize_license_expression(license_expr) if license_expr != normalized: InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") self.metadata.license_expression = normalized - - for cl in self.metadata.get_classifiers(): - if not cl.startswith("License :: "): - continue - SetuptoolsDeprecationWarning.emit( - "License classifier are deprecated in favor of the license expression.", - f"Please remove the '{cl}' classifier.", - see_url="https://peps.python.org/pep-0639/", - due_date=(2027, 2, 17), # Introduced 2025-02-17 + if license_classifiers: + raise InvalidConfigError( + "License classifiers have been superseded by license expressions " + "(see https://peps.python.org/pep-0639/). Please remove:\n\n" + + "\n".join(license_classifiers), ) + elif license_classifiers: + pypa_guides = "guides/writing-pyproject-toml/#license" + SetuptoolsDeprecationWarning.emit( + "License classifiers are deprecated.", + "Please consider removing the following classifiers in favor of a " + "SPDX license expression:\n\n" + "\n".join(license_classifiers), + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + # Warning introduced on 2025-02-17 + # TODO: Should we add a due date? It may affect old/unmaintained + # packages in the ecosystem and cause problems... + ) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 468a1ba01d..5b7ca0f40d 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -9,7 +9,6 @@ import io import re import tarfile -import warnings from inspect import cleandoc from pathlib import Path from unittest.mock import Mock @@ -24,7 +23,7 @@ from setuptools.config import expand, pyprojecttoml, setupcfg from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter from setuptools.dist import Distribution -from setuptools.errors import RemovedConfigError +from setuptools.errors import InvalidConfigError, RemovedConfigError from setuptools.warnings import SetuptoolsDeprecationWarning from .downloads import retrieve_file, urls_from_file @@ -320,26 +319,36 @@ def test_license_in_metadata( assert content_str in content -def test_license_expression_with_bad_classifier(tmp_path): +def test_license_classifier_with_license_expression(tmp_path): text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0] pyproject = _pep621_example_project( tmp_path, "README", f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", ) - msg = "License classifier are deprecated(?:.|\n)*'License :: OSI Approved :: MIT License'" - with pytest.raises(SetuptoolsDeprecationWarning, match=msg): + msg = "License classifiers have been superseded by license expressions" + with pytest.raises(InvalidConfigError, match=msg) as exc: pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", SetuptoolsDeprecationWarning) + assert "License :: OSI Approved :: MIT License" in str(exc.value) + + +def test_license_classifier_without_license_expression(tmp_path): + text = """\ + [project] + name = "spam" + version = "2020.0.0" + license = {text = "mit or apache-2.0"} + classifiers = ["License :: OSI Approved :: MIT License"] + """ + pyproject = _pep621_example_project(tmp_path, "README", text) + + msg = "License classifiers are deprecated(?:.|\n)*MIT License" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - # Check license classifier is still included - assert dist.metadata.get_classifiers() == [ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", - "License :: OSI Approved :: MIT License", - ] + + # Check license classifier is still included + assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"] class TestLicenseFiles: