diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 6664c6158a..6bb2bea514 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -58,6 +58,7 @@ def apply(dist: Distribution, config: dict, filename: StrPath) -> Distribution: os.chdir(root_dir) try: dist._finalize_requires() + dist._finalize_license_expression() dist._finalize_license_files() finally: os.chdir(current_directory) diff --git a/setuptools/dist.py b/setuptools/dist.py index d3940187e2..6904e64e1c 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Union from more_itertools import partition, unique_everseen +from packaging.licenses import canonicalize_license_expression from packaging.markers import InvalidMarker, Marker from packaging.specifiers import InvalidSpecifier, SpecifierSet from packaging.version import Version @@ -27,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 @@ -403,6 +405,23 @@ def _normalize_requires(self): (k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items() ) + def _finalize_license_expression(self) -> None: + """Normalize license and license_expression.""" + 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 + raise InvalidConfigError( + "License classifier are deprecated in favor of the license expression. " + f"Remove the '{cl}' classifier." + ) + def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" license_files: list[str] | None = self.metadata.license_files @@ -653,6 +672,7 @@ def parse_config_files( pyprojecttoml.apply_configuration(self, filename, ignore_option_errors) self._finalize_requires() + self._finalize_license_expression() self._finalize_license_files() def fetch_build_eggs( diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 03f950ecd1..91883b4618 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -23,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 .downloads import retrieve_file, urls_from_file @@ -175,7 +175,10 @@ def main_tomatoes(): pass {email = "hi@pradyunsg.me"}, {name = "Tzu-Ping Chung"} ] -license = "MIT" +license = "mit or apache-2.0" # should be normalized in metadata +classifiers = [ + "Development Status :: 5 - Production/Stable", +] """ @@ -286,8 +289,8 @@ def test_utf8_maintainer_in_metadata( # issue-3663 pytest.param( PEP639_LICENSE_EXPRESSION, None, - 'MIT', - 'License-Expression: MIT', + 'MIT OR Apache-2.0', + 'License-Expression: MIT OR Apache-2.0', id='license-expression', ), ), @@ -314,6 +317,18 @@ def test_license_in_metadata( assert content_str in content +def test_license_expression_with_bad_classifier(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.*'License :: OSI Approved :: MIT License'" + with pytest.raises(InvalidConfigError, match=msg): + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + class TestLicenseFiles: def base_pyproject(self, tmp_path, additional_text): pyproject = _pep621_example_project(tmp_path, "README")