Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions newsfragments/4840.feature.rst
Original file line number Diff line number Diff line change
@@ -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>`.
8 changes: 8 additions & 0 deletions setuptools/config/_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
86 changes: 58 additions & 28 deletions setuptools/tests/config/test_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]"},
Expand Down Expand Up @@ -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"}


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -354,47 +358,51 @@ 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
assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"]


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)
Expand All @@ -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"
Expand Down Expand Up @@ -437,17 +449,35 @@ 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()

for fname in license_files:
(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
Expand Down
8 changes: 6 additions & 2 deletions setuptools/tests/test_build_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
14 changes: 13 additions & 1 deletion setuptools/tests/test_sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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]
Expand Down
Loading