From 6410380863bc2a643c984c1ef282cf3d9086bd41 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 16:28:24 +0000 Subject: [PATCH 1/9] Prevent deprecated license classifiers from being written to core metadata --- setuptools/dist.py | 25 +++++++++++-------- .../tests/config/test_apply_pyprojecttoml.py | 8 +++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 27e8095709..fcedf679e7 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -406,22 +406,27 @@ 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 classifier 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 + filtered = [cl for cl in classifiers if cl not in license_classifiers] + self.metadata.set_classifiers(classifiers.__class__(filtered)) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 468a1ba01d..eb1ca671fd 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -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 classifier 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", ] From 778e679b4d95e9bed8c41052883052e39e44e881 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 16:36:39 +0000 Subject: [PATCH 2/9] Improve message in warning --- setuptools/dist.py | 2 +- setuptools/tests/config/test_apply_pyprojecttoml.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index fcedf679e7..dc301a6369 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -411,7 +411,7 @@ def _finalize_license_expression(self) -> None: if license_classifiers: SetuptoolsDeprecationWarning.emit( - "License classifier are deprecated in favor of the license expression.", + "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 diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index eb1ca671fd..d858236278 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -327,7 +327,7 @@ def test_license_expression_with_bad_classifier(tmp_path): "README", f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", ) - msg = "License classifier are deprecated" + 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) From ee51110a3781937c09cb03c6af373f0245c3bbe1 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 16:41:31 +0000 Subject: [PATCH 3/9] Use a more explicit method for preserving static-ness of classifiers --- setuptools/dist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index dc301a6369..cc627f8583 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -425,8 +425,9 @@ def _finalize_license_expression(self) -> None: self.metadata.license_expression = normalized if license_classifiers: # Filter classifiers but preserve "static-ness" of metadata - filtered = [cl for cl in classifiers if cl not in license_classifiers] - self.metadata.set_classifiers(classifiers.__class__(filtered)) + 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)) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" From 9bdad9f8fa39705abc2664a6cd090250c1772006 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 17:00:44 +0000 Subject: [PATCH 4/9] Add news fragment --- newsfragments/4833.feature.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 newsfragments/4833.feature.rst diff --git a/newsfragments/4833.feature.rst b/newsfragments/4833.feature.rst new file mode 100644 index 0000000000..6a61c5ca05 --- /dev/null +++ b/newsfragments/4833.feature.rst @@ -0,0 +1,3 @@ +Added deprecation warning for license classifiers, +according to `PEP 639 +`_. From ea4095d0d2311fb4266b5ae9aa00f3e5be08c9b5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 18:04:14 +0000 Subject: [PATCH 5/9] Keep warning about license classifiers but raise an error if license expression is used --- setuptools/dist.py | 30 +++++++++++-------- .../tests/config/test_apply_pyprojecttoml.py | 29 +++++++++++------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index cc627f8583..9d7118a5d2 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -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 @@ -407,15 +408,7 @@ 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_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")] license_expr = self.metadata.license_expression if license_expr: @@ -424,10 +417,21 @@ def _finalize_license_expression(self) -> None: InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") self.metadata.license_expression = normalized if license_classifiers: - # Filter classifiers but preserve "static-ness" of metadata - 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)) + 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: + 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="https://peps.python.org/pep-0639/", + # 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.""" diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index d858236278..62baf7f827 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -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 @@ -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 @@ -320,25 +319,35 @@ 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 classifiers are deprecated" - with pytest.raises(SetuptoolsDeprecationWarning, match=msg) as exc: + msg = "License classifiers have been superseded by license expressions" + with pytest.raises(InvalidConfigError, 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) + +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 :: OSI Approved :: MIT License' is removed + # Check license classifier is still included assert dist.metadata.get_classifiers() == [ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", + "License :: OSI Approved :: MIT License" ] From 3af67b8183f808e91bab8648132b4d6a91704e3e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 18:06:20 +0000 Subject: [PATCH 6/9] Update newsfragment --- newsfragments/4833.feature.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/newsfragments/4833.feature.rst b/newsfragments/4833.feature.rst index 6a61c5ca05..d8801becf7 100644 --- a/newsfragments/4833.feature.rst +++ b/newsfragments/4833.feature.rst @@ -1,3 +1,2 @@ -Added deprecation warning for license classifiers, -according to `PEP 639 -`_. +Added exception (or warning) when deprecated license classifiers are used, +according to `PEP 639 `_. From 3b71b5f9f4a277f8ffd95f60aee1fc10f7e0e011 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 18:18:29 +0000 Subject: [PATCH 7/9] Use a better docs URL for warning --- setuptools/dist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 9d7118a5d2..44e600df5c 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -423,11 +423,12 @@ def _finalize_license_expression(self) -> None: + "\n".join(license_classifiers), ) elif license_classifiers: + pypa_guides = "guides/licensing-examples-and-user-scenarios/" 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="https://peps.python.org/pep-0639/", + 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... From 29302de33aa57e4df01edcafcd331362e09ca8cd Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 10:31:37 +0000 Subject: [PATCH 8/9] Update URL for warning --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 44e600df5c..d202dbf504 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -423,7 +423,7 @@ def _finalize_license_expression(self) -> None: + "\n".join(license_classifiers), ) elif license_classifiers: - pypa_guides = "guides/licensing-examples-and-user-scenarios/" + pypa_guides = "guides/writing-pyproject-toml/#license" SetuptoolsDeprecationWarning.emit( "License classifiers are deprecated.", "Please consider removing the following classifiers in favor of a " From a20512e7f513b9a0471845777170560b64cedd39 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 10:43:30 +0000 Subject: [PATCH 9/9] Fix bypassed assertion in tests --- setuptools/tests/config/test_apply_pyprojecttoml.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 62baf7f827..5b7ca0f40d 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -329,7 +329,8 @@ def test_license_classifier_with_license_expression(tmp_path): msg = "License classifiers have been superseded by license expressions" with pytest.raises(InvalidConfigError, match=msg) as exc: pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - assert "License :: OSI Approved :: MIT License" in str(exc.value) + + assert "License :: OSI Approved :: MIT License" in str(exc.value) def test_license_classifier_without_license_expression(tmp_path): @@ -345,10 +346,9 @@ def test_license_classifier_without_license_expression(tmp_path): 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() == [ - "License :: OSI Approved :: MIT License" - ] + + # Check license classifier is still included + assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"] class TestLicenseFiles: