diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 08a10bcdcc..83214e238b 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -57,6 +57,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 151f9a851f..51a0ddd52c 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 @@ -26,6 +27,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 @@ -398,6 +400,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 @@ -649,6 +668,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 b44c479998..c90f7aadc2 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -22,7 +22,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 @@ -174,7 +174,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", +] """ @@ -285,8 +288,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', ), ), @@ -313,6 +316,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")