diff --git a/newsfragments/4840.feature.rst b/newsfragments/4840.feature.rst new file mode 100644 index 0000000000..a033fd2afb --- /dev/null +++ b/newsfragments/4840.feature.rst @@ -0,0 +1,5 @@ +Deprecated ``project.license`` as a TOML table in +``pyproject.toml``. Users are expected to move towards using +``project.license-files`` and/or SPDX expressions (as strings) in +``pyproject.license``. +See :pep:`PEP 639 <639#deprecate-license-key-table-subkeys>`. diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 9c60196e54..f23d0d2de3 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -203,6 +203,14 @@ def _license(dist: Distribution, val: str | dict, root_dir: StrPath | None): if isinstance(val, str): _set_config(dist, "license_expression", _static.Str(val)) else: + pypa_guides = "guides/writing-pyproject-toml/#license" + SetuptoolsDeprecationWarning.emit( + "`project.license` as a TOML table is deprecated", + "Please use a simple string containing a SPDX expression for " + "`project.license`. You can also use `project.license-files`.", + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + due_date=(2026, 2, 18), # Introduced on 2025-02-18 + ) if "file" in val: # XXX: Is it completely safe to assume static? value = expand.read_files([val["file"]], root_dir) diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index c489c99bd6..848f44745f 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -93,7 +93,7 @@ def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path): description = "Lovely Spam! Wonderful Spam!" readme = "README.rst" requires-python = ">=3.8" -license = {file = "LICENSE.txt"} +license-files = ["LICENSE.txt"] # Updated to be PEP 639 compliant keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] authors = [ {email = "hi@pradyunsg.me"}, @@ -206,7 +206,6 @@ def test_pep621_example(tmp_path): """Make sure the example in PEP 621 works""" pyproject = _pep621_example_project(tmp_path) dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - assert dist.metadata.license == "--- LICENSE stub ---" assert set(dist.metadata.license_files) == {"LICENSE.txt"} @@ -294,6 +293,11 @@ def test_utf8_maintainer_in_metadata( # issue-3663 'License: MIT', 'License-Expression: ', id='license-text', + marks=[ + pytest.mark.filterwarnings( + "ignore:.project.license. as a TOML table is deprecated", + ) + ], ), pytest.param( PEP639_LICENSE_EXPRESSION, @@ -354,8 +358,12 @@ def test_license_classifier_without_license_expression(tmp_path): """ pyproject = _pep621_example_project(tmp_path, "README", text) - msg = "License classifiers are deprecated(?:.|\n)*MIT License" - with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + msg1 = "License classifiers are deprecated(?:.|\n)*MIT License" + msg2 = ".project.license. as a TOML table is deprecated" + with ( + pytest.warns(SetuptoolsDeprecationWarning, match=msg1), + pytest.warns(SetuptoolsDeprecationWarning, match=msg2), + ): dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) # Check license classifier is still included @@ -363,38 +371,38 @@ def test_license_classifier_without_license_expression(tmp_path): class TestLicenseFiles: - def base_pyproject(self, tmp_path, additional_text): - pyproject = _pep621_example_project(tmp_path, "README") - text = pyproject.read_text(encoding="utf-8") - - # Sanity-check - assert 'license = {file = "LICENSE.txt"}' in text - assert "[tool.setuptools]" not in text - - text = f"{text}\n{additional_text}\n" - pyproject.write_text(text, encoding="utf-8") - return pyproject - - def base_pyproject_license_pep639(self, tmp_path, additional_text=""): - pyproject = _pep621_example_project(tmp_path, "README") - text = pyproject.read_text(encoding="utf-8") + def base_pyproject( + self, + tmp_path, + additional_text="", + license_toml='license = {file = "LICENSE.txt"}\n', + ): + text = PEP639_LICENSE_EXPRESSION # Sanity-check - assert 'license = {file = "LICENSE.txt"}' in text + assert 'license = "mit or apache-2.0"' in text assert 'license-files' not in text assert "[tool.setuptools]" not in text text = re.sub( - r"(license = {file = \"LICENSE.txt\"})\n", - ("license = \"licenseref-Proprietary\"\nlicense-files = [\"_FILE*\"]\n"), + r"(license = .*)\n", + license_toml, text, count=1, ) - if additional_text: - text = f"{text}\n{additional_text}\n" - pyproject.write_text(text, encoding="utf-8") + assert license_toml in text # sanity check + text = f"{text}\n{additional_text}\n" + pyproject = _pep621_example_project(tmp_path, "README", pyproject_text=text) return pyproject + def base_pyproject_license_pep639(self, tmp_path, additional_text=""): + return self.base_pyproject( + tmp_path, + additional_text=additional_text, + license_toml='license = "licenseref-Proprietary"' + '\nlicense-files = ["_FILE*"]\n', + ) + def test_both_license_and_license_files_defined(self, tmp_path): setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]' pyproject = self.base_pyproject(tmp_path, setuptools_config) @@ -407,8 +415,12 @@ def test_both_license_and_license_files_defined(self, tmp_path): license = tmp_path / "LICENSE.txt" license.write_text("LicenseRef-Proprietary\n", encoding="utf-8") - msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" - with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + msg1 = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" + msg2 = ".project.license. as a TOML table is deprecated" + with ( + pytest.warns(SetuptoolsDeprecationWarning, match=msg1), + pytest.warns(SetuptoolsDeprecationWarning, match=msg2), + ): 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" @@ -437,7 +449,7 @@ def test_license_files_defined_twice(self, tmp_path): def test_default_patterns(self, tmp_path): setuptools_config = '[tool.setuptools]\nzip-safe = false' # ^ used just to trigger section validation - pyproject = self.base_pyproject(tmp_path, setuptools_config) + pyproject = self.base_pyproject(tmp_path, setuptools_config, license_toml="") license_files = "LICENCE-a.html COPYING-abc.txt AUTHORS-xyz NOTICE,def".split() @@ -445,9 +457,27 @@ def test_default_patterns(self, tmp_path): (tmp_path / fname).write_text(f"{fname}\n", encoding="utf-8") dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert (tmp_path / "LICENSE.txt").exists() # from base example assert set(dist.metadata.license_files) == {*license_files, "LICENSE.txt"} + def test_deprecated_file_expands_to_text(self, tmp_path): + """Make sure the old example with ``license = {text = ...}`` works""" + + assert 'license-files = ["LICENSE.txt"]' in PEP621_EXAMPLE # sanity check + text = PEP621_EXAMPLE.replace( + 'license-files = ["LICENSE.txt"]', + 'license = {file = "LICENSE.txt"}', + ) + pyproject = _pep621_example_project(tmp_path, pyproject_text=text) + + msg = ".project.license. as a TOML table is deprecated" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + assert dist.metadata.license == "--- LICENSE stub ---" + assert set(dist.metadata.license_files) == {"LICENSE.txt"} # auto-filled + class TestPyModules: # https://github.com/pypa/setuptools/issues/4316 diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 8598578475..624bba862e 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -387,9 +387,13 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): build_backend = self.get_build_backend() with tmpdir.as_cwd(): path.build(files) + msgs = [ + "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'", + "`project.license` as a TOML table is deprecated", + ] with warnings.catch_warnings(): - msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" - warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning) + for msg in msgs: + warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning) sdist_path = build_backend.build_sdist("temp") wheel_file = build_backend.build_wheel("temp") diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 3ee0511b1c..19d8ddf6da 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -708,12 +708,21 @@ def test_sdist_with_latin1_encoded_filename(self): [project] name = "testing" readme = "USAGE.rst" - license = {file = "DOWHATYOUWANT"} + license-files = ["DOWHATYOUWANT"] dynamic = ["version"] [tool.setuptools.dynamic] version = {file = ["src/VERSION.txt"]} """, "pyproject.toml - directive with str instead of list": """ + [project] + name = "testing" + readme = "USAGE.rst" + license-files = ["DOWHATYOUWANT"] + dynamic = ["version"] + [tool.setuptools.dynamic] + version = {file = "src/VERSION.txt"} + """, + "pyproject.toml - deprecated license table with file entry": """ [project] name = "testing" readme = "USAGE.rst" @@ -725,6 +734,9 @@ def test_sdist_with_latin1_encoded_filename(self): } @pytest.mark.parametrize("config", _EXAMPLE_DIRECTIVES.keys()) + @pytest.mark.filterwarnings( + "ignore:.project.license. as a TOML table is deprecated" + ) def test_add_files_referenced_by_config_directives(self, source_dir, config): config_file, _, _ = config.partition(" - ") config_text = self._EXAMPLE_DIRECTIVES[config]