Skip to content
3 changes: 3 additions & 0 deletions newsfragments/4833.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added deprecation warning for license classifiers,
according to `PEP 639
<https://peps.python.org/pep-0639/#deprecate-license-classifiers>`_.
26 changes: 16 additions & 10 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,22 +406,28 @@ 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 :: ")}

if license_classifiers:
SetuptoolsDeprecationWarning.emit(
"License classifiers are deprecated in favor of the license expression.",
"Please remove the classifiers:\n\n" + "\n".join(license_classifiers),
see_url="https://peps.python.org/pep-0639/",
due_date=(2027, 2, 17), # Introduced 2025-02-17
)

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:
# Filter classifiers but preserve "static-ness" of metadata
Copy link
Contributor Author

@abravalheri abravalheri Feb 17, 2025

Choose a reason for hiding this comment

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

Not sure if we should indeed preserve the "static-ness" of the classifiers, or simply let Dynamic: classifier appear in the core metadata (we are modifying them after all...)

list_ = _static.List if _static.is_static(classifiers) else list
filtered = (cl for cl in classifiers if cl not in license_classifiers)
self.metadata.set_classifiers(list_(filtered))
Copy link
Member

Choose a reason for hiding this comment

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

I don't think setuptools should remove the license classifier. Consider the build is done is CI, this could lead to cases were packages are distributed without any license information (neither license expression nor license classifier). This would even apply to setuptools itself.

Warn about it yes, but we shouldn't remove items from the classifier list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the review @cdce8p.

In this case it will not be distributed without license information right? (Because metadata.license_expression is set).

But I am rethinking... There is a chance either twine or PyPI themselves will fail the package validation if they decide to go for a more strict interpretation of the PEP. So we might implement your initial suggestion of error for users opting into license = "..." in pyproject.toml and warning elsewhere. Let me make another commit.

This would even apply to setuptools itself.

In the case of setuptools I believe that the best way of convying the license information is to use License-File instead of License-Expression given the complexity and maintenance of vendored dependencies (I saw your comment in the discourse thread, but the text in the SPDX standard seems to indicate that the correct would be to use an ... AND ... expression. This would require an evolution of the tooling for vendoring and increase complexity).

TL;DR: My impression is that if the licensing is easy enough, people should use the SPDX, but when it starts to get more complicated the best is to rely on the actual license files so that no mistake is introduced.

Copy link
Member

Choose a reason for hiding this comment

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

In this case it will not be distributed without license information right? (Because metadata.license_expression is set).

Yes. Missed the sneaky if license_expr I added myself 🙈

But I am rethinking... There is a chance either twine or PyPI themselves will fail the package validation if they decide to go for a more strict interpretation of the PEP. So we might implement your initial suggestion of error for users opting into license = "..." in pyproject.toml and warning elsewhere. Let me make another commit.

👍🏻

In the case of setuptools I believe that the best way of convying the license information is to use License-File instead of License-Expression given the complexity and maintenance of vendored dependencies (I saw your comment in the discourse thread, but the text in the SPDX standard seems to indicate that the correct would be to use an ... AND ... expression. This would require an evolution of the tooling for vendoring and increase complexity).

TL;DR: My impression is that if the licensing is easy enough, people should use the SPDX, but when it starts to get more complicated the best is to rely on the actual license files so that no mistake is introduced.

My understanding of the PEP might be wrong but the way I see it is that the license itself is determined by the license expression and the license files are only there to guarantee that the license information are properly distributed as well. Most licenses require the license text itself to be part of the distribution which often didn't happen in the past.

Copy link
Contributor Author

@abravalheri abravalheri Feb 17, 2025

Choose a reason for hiding this comment

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

My understanding of the PEP might be wrong but the way I see it is that the license itself is determined by the license expression and the license files are only there to guarantee that the license information are properly distributed as well.

If that is how the PEP intends it, then it is very problematic...

But I don't know if that is the case...

After all, the license files are the ultimate source of truth for the licensing model, that is the whole point. SPDX indexes are nice to have (if correctly derived), but ultimately is the text of the license that determines how the project can be distributed or not.

Some of the conversations of the discourse thread seem to hint at the same conclusion:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But I am rethinking... There is a chance either twine or PyPI themselves will fail the package validation if they decide to go for a more strict interpretation of the PEP. So we might implement your initial suggestion of error for users opting into license = "..." in pyproject.toml and warning elsewhere. Let me make another commit.

@cdce8p, in ea4095d I updated the PR to warn or raise an exception accordingly.


def _finalize_license_files(self) -> None:
"""Compute names of all license files which should be included."""
Expand Down
8 changes: 4 additions & 4 deletions setuptools/tests/config/test_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,18 +327,18 @@ def test_license_expression_with_bad_classifier(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 are deprecated"
with pytest.raises(SetuptoolsDeprecationWarning, match=msg) as exc:
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert "License :: OSI Approved :: MIT License" in str(exc.value)

with warnings.catch_warnings():
warnings.simplefilter("ignore", SetuptoolsDeprecationWarning)
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
# Check license classifier is still included
# Check 'License :: OSI Approved :: MIT License' is removed
assert dist.metadata.get_classifiers() == [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python",
"License :: OSI Approved :: MIT License",
]


Expand Down
Loading