Skip to content

Commit a3fe92a

Browse files
committed
Normalize license expression
1 parent 3ecc644 commit a3fe92a

File tree

3 files changed

+40
-4
lines changed

3 files changed

+40
-4
lines changed

setuptools/config/_apply_pyprojecttoml.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def apply(dist: Distribution, config: dict, filename: StrPath) -> Distribution:
5757
os.chdir(root_dir)
5858
try:
5959
dist._finalize_requires()
60+
dist._finalize_license_expression()
6061
dist._finalize_license_files()
6162
finally:
6263
os.chdir(current_directory)

setuptools/dist.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import TYPE_CHECKING, Any, Union
1313

1414
from more_itertools import partition, unique_everseen
15+
from packaging.licenses import canonicalize_license_expression
1516
from packaging.markers import InvalidMarker, Marker
1617
from packaging.specifiers import InvalidSpecifier, SpecifierSet
1718
from packaging.version import Version
@@ -26,6 +27,7 @@
2627
from ._reqs import _StrOrIter
2728
from .config import pyprojecttoml, setupcfg
2829
from .discovery import ConfigDiscovery
30+
from .errors import InvalidConfigError
2931
from .monkey import get_unpatched
3032
from .warnings import InformationOnly, SetuptoolsDeprecationWarning
3133

@@ -398,6 +400,23 @@ def _normalize_requires(self):
398400
k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items()
399401
}
400402

403+
def _finalize_license_expression(self) -> None:
404+
"""Normalize license and license_expression."""
405+
license_expr = self.metadata.license_expression
406+
if license_expr:
407+
normalized = canonicalize_license_expression(license_expr)
408+
if license_expr != normalized:
409+
InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'")
410+
self.metadata.license_expression = normalized
411+
412+
for cl in self.metadata.get_classifiers():
413+
if not cl.startswith("License :: "):
414+
continue
415+
raise InvalidConfigError(
416+
"License classifier are deprecated in favor of the license expression. "
417+
f"Remove the '{cl}' classifier."
418+
)
419+
401420
def _finalize_license_files(self) -> None:
402421
"""Compute names of all license files which should be included."""
403422
license_files: list[str] | None = self.metadata.license_files
@@ -649,6 +668,7 @@ def parse_config_files(
649668
pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
650669

651670
self._finalize_requires()
671+
self._finalize_license_expression()
652672
self._finalize_license_files()
653673

654674
def fetch_build_eggs(

setuptools/tests/config/test_apply_pyprojecttoml.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from setuptools.config import expand, pyprojecttoml, setupcfg
2323
from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter
2424
from setuptools.dist import Distribution
25-
from setuptools.errors import RemovedConfigError
25+
from setuptools.errors import InvalidConfigError, RemovedConfigError
2626

2727
from .downloads import retrieve_file, urls_from_file
2828

@@ -174,7 +174,10 @@ def main_tomatoes(): pass
174174
{email = "[email protected]"},
175175
{name = "Tzu-Ping Chung"}
176176
]
177-
license = "MIT"
177+
license = "mit or apache-2.0" # should be normalized in metadata
178+
classifiers = [
179+
"Development Status :: 5 - Production/Stable",
180+
]
178181
"""
179182

180183

@@ -285,8 +288,8 @@ def test_utf8_maintainer_in_metadata( # issue-3663
285288
pytest.param(
286289
PEP639_LICENSE_EXPRESSION,
287290
None,
288-
'MIT',
289-
'License-Expression: MIT',
291+
'MIT OR Apache-2.0',
292+
'License-Expression: MIT OR Apache-2.0',
290293
id='license-expression',
291294
),
292295
),
@@ -313,6 +316,18 @@ def test_license_in_metadata(
313316
assert content_str in content
314317

315318

319+
def test_license_expression_with_bad_classifier(tmp_path):
320+
text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0]
321+
pyproject = _pep621_example_project(
322+
tmp_path,
323+
"README",
324+
f"{text}\n \"License :: OSI Approved :: MIT License\"\n]",
325+
)
326+
msg = "License classifier are deprecated.*'License :: OSI Approved :: MIT License'"
327+
with pytest.raises(InvalidConfigError, match=msg):
328+
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
329+
330+
316331
class TestLicenseFiles:
317332
def base_pyproject(self, tmp_path, additional_text):
318333
pyproject = _pep621_example_project(tmp_path, "README")

0 commit comments

Comments
 (0)