diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst index e988fec7acd..efc68603a9c 100644 --- a/docs/userguide/pyproject_config.rst +++ b/docs/userguide/pyproject_config.rst @@ -49,7 +49,7 @@ The ``project`` table contains metadata fields as described by the readme = "README.rst" requires-python = ">=3.8" keywords = ["one", "two"] - license = {text = "BSD-3-Clause"} + license = "BSD-3-Clause" classifiers = [ "Framework :: Django", "Programming Language :: Python :: 3", diff --git a/newsfragments/4706.feature.rst b/newsfragments/4706.feature.rst new file mode 100644 index 00000000000..38b09276c01 --- /dev/null +++ b/newsfragments/4706.feature.rst @@ -0,0 +1 @@ +Added initial support for ``License-Expression`` (`PEP 639 `_). -- by :user:`cdce8p` diff --git a/pyproject.toml b/pyproject.toml index 1a4906fb0c6..5114d3227fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ backend-path = ["."] [project] name = "setuptools" version = "75.2.0" +license = "MIT" authors = [ { name = "Python Packaging Authority", email = "distutils-sig@python.org" }, ] @@ -14,7 +15,6 @@ readme = "README.rst" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", @@ -135,7 +135,7 @@ type = [ # pin mypy version so a new version doesn't suddenly cause the CI to fail, # until types-setuptools is removed from typeshed. - # For help with static-typing issues, or mypy update, ping @Avasam + # For help with static-typing issues, or mypy update, ping @Avasam "mypy==1.12.*", # Typing fixes in version newer than we require at runtime "importlib_metadata>=7.0.2; python_version < '3.10'", diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 2e9c48a77b6..fa8ddc3754a 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -174,9 +174,13 @@ def write_field(key, value): if attr_val is not None: write_field(field, attr_val) - license = self.get_license() - if license: - write_field('License', rfc822_escape(license)) + license_expression = self.get_license_expression() + if license_expression: + write_field('License-Expression', rfc822_escape(license_expression)) + else: + license = self.get_license() + if license: + write_field('License', rfc822_escape(license)) for project_url in self.project_urls.items(): write_field('Project-URL', '%s, %s' % project_url) diff --git a/setuptools/_distutils/dist.py b/setuptools/_distutils/dist.py index 154301baffc..a7b12dd7fa7 100644 --- a/setuptools/_distutils/dist.py +++ b/setuptools/_distutils/dist.py @@ -1052,6 +1052,7 @@ def __init__(self, path=None): self.maintainer_email = None self.url = None self.license = None + self.license_expression = None self.description = None self.long_description = None self.keywords = None @@ -1089,6 +1090,7 @@ def _read_list(name): self.maintainer_email = None self.url = _read_field('home-page') self.license = _read_field('license') + self.license_expression = _read_field('license-expression') if 'download-url' in msg: self.download_url = _read_field('download-url') @@ -1221,6 +1223,11 @@ def get_license(self): get_licence = get_license + def get_license_expression(self): + return self.license_expression + + get_license_expression = get_license_expression + def get_description(self): return self.description diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 2b7fe7bd80b..b61b1205118 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -173,14 +173,17 @@ def _long_description( dist._referenced_files.add(file) -def _license(dist: Distribution, val: dict, root_dir: StrPath | None): +def _license(dist: Distribution, val: str | dict, root_dir: StrPath | None): from setuptools.config import expand - if "file" in val: - _set_config(dist, "license", expand.read_files([val["file"]], root_dir)) - dist._referenced_files.add(val["file"]) + if isinstance(val, str): + _set_config(dist, "license_expression", val) else: - _set_config(dist, "license", val["text"]) + if "file" in val: + _set_config(dist, "license", expand.read_files([val["file"]], root_dir)) + dist._referenced_files.add(val["file"]) + else: + _set_config(dist, "license", val["text"]) def _people(dist: Distribution, val: list[dict], _root_dir: StrPath | None, kind: str): diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index deee6fa47c5..b4ddd8da010 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -155,6 +155,28 @@ def main_gui(): pass def main_tomatoes(): pass """ +PEP639_LICENSE_TEXT = """\ +[project] +name = "spam" +version = "2020.0.0" +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +license = {text = "MIT"} +""" + +PEP639_LICENSE_EXPRESSION = """\ +[project] +name = "spam" +version = "2020.0.0" +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +license = "MIT" +""" + def _pep621_example_project( tmp_path, @@ -250,6 +272,47 @@ def test_utf8_maintainer_in_metadata( # issue-3663 assert f"Maintainer-email: {expected_maintainers_meta_value}" in content +@pytest.mark.parametrize( + ('pyproject_text', 'license', 'license_expression', 'content_str'), + ( + pytest.param( + PEP639_LICENSE_TEXT, + 'MIT', + None, + 'License: MIT', + id='license-text', + ), + pytest.param( + PEP639_LICENSE_EXPRESSION, + None, + 'MIT', + 'License-Expression: MIT', + id='license-expression', + ), + ), +) +def test_license_in_metadata( + license, + license_expression, + content_str, + pyproject_text, + tmp_path, +): + pyproject = _pep621_example_project( + tmp_path, + "README", + pyproject_text=pyproject_text, + ) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert dist.metadata.license == license + assert dist.metadata.license_expression == license_expression + pkg_file = tmp_path / "PKG-FILE" + with open(pkg_file, "w", encoding="utf-8") as fh: + dist.metadata.write_pkg_file(fh) + content = pkg_file.read_text(encoding="utf-8") + assert content_str in content + + class TestLicenseFiles: # TODO: After PEP 639 is accepted, we have to move the license-files # to the `project` table instead of `tool.setuptools`