Skip to content
2 changes: 2 additions & 0 deletions newsfragments/4833.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added exception (or warning) when deprecated license classifiers are used,
according to `PEP 639 <https://peps.python.org/pep-0639/#deprecate-license-classifiers>`_.
29 changes: 20 additions & 9 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."""
Expand Down
35 changes: 22 additions & 13 deletions setuptools/tests/config/test_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"]
Comment on lines +347 to +351
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed that this uses pytest.warns instead of pytest.raises. So my previous post here was wrong. The nested block is actually fully executed.

Anyway, the current setup will also work.



class TestLicenseFiles:
Expand Down
Loading