Repository: pypa/setuptools
Pull request #4837 (2025-02-18 11:42:34)

Deprecate tools.setuptools.license-files
Deprecate `tools.setuptools.license-files` in favor of `project.license-files`.

Ref #4829

/CC @abravalheri


Patch:
diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst
index 7354bd3a8..1b493fba3 100644
--- a/docs/userguide/miscellaneous.rst
+++ b/docs/userguide/miscellaneous.rst
@@ -24,7 +24,7 @@ files are included in a source distribution by default:
   in ``pyproject.toml`` and/or equivalent in ``setup.cfg``/``setup.py``;
   note that if you don't explicitly set this parameter, ``setuptools``
   will include any files that match the following glob patterns:
-  ``LICENSE*``, ``LICENCE*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS**``;
+  ``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS**``;
 - ``pyproject.toml``;
 - ``setup.cfg``;
 - ``setup.py``;
diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index efc68603a..3bf99bed1 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -99,7 +99,7 @@ Key                       Value Type (TOML)           Notes
                                                       See :doc:`/userguide/datafiles`.
 ``exclude-package-data``  table/inline-table          Empty by default. See :doc:`/userguide/datafiles`.
 ------------------------- --------------------------- -------------------------
-``license-files``         array of glob patterns      **Provisional** - likely to change with :pep:`639`
+``license-files``         array of glob patterns      **Deprecated** - use ``project.license-files`` instead. See :doc:`PyPUG:guides/writing-pyproject-toml`
                                                       (by default: ``['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']``)
 ``data-files``            table/inline-table          **Discouraged** - check :doc:`/userguide/datafiles`.
                                                       Whenever possible, consider using data files inside the package directories.
diff --git a/newsfragments/4837.feature.rst b/newsfragments/4837.feature.rst
new file mode 100644
index 000000000..4ad97b951
--- /dev/null
+++ b/newsfragments/4837.feature.rst
@@ -0,0 +1,3 @@
+Deprecated ``tools.setuptools.license-files`` in favor of ``project.license-files``
+and added exception if ``project.license-files`` and ``tools.setuptools.license-files``
+are used together. -- by :user:`cdce8p`
diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index bcb3c0389..9c60196e5 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -22,9 +22,9 @@ from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
 
 from .. import _static
 from .._path import StrPath
-from ..errors import RemovedConfigError
+from ..errors import InvalidConfigError, RemovedConfigError
 from ..extension import Extension
-from ..warnings import SetuptoolsWarning
+from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
 
 if TYPE_CHECKING:
     from typing_extensions import TypeAlias
@@ -89,6 +89,21 @@ def _apply_tool_table(dist: Distribution, config: dict, filename: StrPath):
     if not tool_table:
         return  # short-circuit
 
+    if "license-files" in tool_table:
+        if dist.metadata.license_files:
+            raise InvalidConfigError(
+                "'project.license-files' is defined already. "
+                "Remove 'tool.setuptools.license-files'."
+            )
+
+        pypa_guides = "guides/writing-pyproject-toml/#license-files"
+        SetuptoolsDeprecationWarning.emit(
+            "'tool.setuptools.license-files' is deprecated in favor of "
+            "'project.license-files'",
+            see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",
+            due_date=(2026, 2, 18),  # Warning introduced on 2025-02-18
+        )
+
     for field, value in tool_table.items():
         norm_key = json_compatible_key(field)
 


Test patch:
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index c43798870..29f6ffaf3 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -24,7 +24,7 @@ from setuptools.command.egg_info import write_requirements
 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
@@ -366,7 +366,7 @@ class TestLicenseFiles:
         pyproject.write_text(text, encoding="utf-8")
         return pyproject
 
-    def base_pyproject_license_pep639(self, tmp_path):
+    def base_pyproject_license_pep639(self, tmp_path, additional_text=""):
         pyproject = _pep621_example_project(tmp_path, "README")
         text = pyproject.read_text(encoding="utf-8")
 
@@ -381,6 +381,8 @@ class TestLicenseFiles:
             text,
             count=1,
         )
+        if additional_text:
+            text = f"{text}\n{additional_text}\n"
         pyproject.write_text(text, encoding="utf-8")
         return pyproject
 
@@ -396,7 +398,9 @@ class TestLicenseFiles:
         license = tmp_path / "LICENSE.txt"
         license.write_text("LicenseRef-Proprietary\n", encoding="utf-8")
 
-        dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+        msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'"
+        with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
+            dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
         assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
         assert dist.metadata.license == "LicenseRef-Proprietary\n"
 
@@ -412,6 +416,15 @@ class TestLicenseFiles:
         assert dist.metadata.license is None
         assert dist.metadata.license_expression == "LicenseRef-Proprietary"
 
+    def test_license_files_defined_twice(self, tmp_path):
+        # Set project.license-files and tools.setuptools.license-files
+        setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]'
+        pyproject = self.base_pyproject_license_pep639(tmp_path, setuptools_config)
+
+        msg = "'project.license-files' is defined already. Remove 'tool.setuptools.license-files'"
+        with pytest.raises(InvalidConfigError, match=msg):
+            pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
     def test_default_patterns(self, tmp_path):
         setuptools_config = '[tool.setuptools]\nzip-safe = false'
         # ^ used just to trigger section validation
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index b26fd2f5b..859857847 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -6,6 +6,7 @@ import shutil
 import signal
 import sys
 import tarfile
+import warnings
 from concurrent import futures
 from pathlib import Path
 from typing import Any, Callable
@@ -15,6 +16,8 @@ import pytest
 from jaraco import path
 from packaging.requirements import Requirement
 
+from setuptools.warnings import SetuptoolsDeprecationWarning
+
 from .textwrap import DALS
 
 SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
@@ -384,8 +387,11 @@ class TestBuildMetaBackend:
         build_backend = self.get_build_backend()
         with tmpdir.as_cwd():
             path.build(files)
-            sdist_path = build_backend.build_sdist("temp")
-            wheel_file = build_backend.build_wheel("temp")
+            with warnings.catch_warnings():
+                msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'"
+                warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning)
+                sdist_path = build_backend.build_sdist("temp")
+                wheel_file = build_backend.build_wheel("temp")
 
         with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
             sdist_contents = set(tar.getnames())