Skip to content

Commit

Permalink
Add initial support for License-Expression (PEP 639)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Oct 28, 2024
1 parent 6998ef2 commit 93d0c67
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 11 deletions.
2 changes: 1 addition & 1 deletion docs/userguide/pyproject_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions newsfragments/4706.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added initial support for ``License-Expression`` (`PEP 639 <https://peps.python.org/pep-0639/#add-license-expression-field>`_). -- by :user:`cdce8p`
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ backend-path = ["."]
[project]
name = "setuptools"
version = "75.2.0"
license = "MIT"
authors = [
{ name = "Python Packaging Authority", email = "[email protected]" },
]
Expand All @@ -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",
Expand Down Expand Up @@ -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'",
Expand Down
10 changes: 7 additions & 3 deletions setuptools/_core_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def read_pkg_file(self, file):
self.url = _read_field_from_msg(msg, 'home-page')
self.download_url = _read_field_from_msg(msg, 'download-url')
self.license = _read_field_unescaped_from_msg(msg, 'license')
self.license_expression = _read_field_unescaped_from_msg(msg, 'license_expression')

self.long_description = _read_field_unescaped_from_msg(msg, 'description')
if self.long_description is None and self.metadata_version >= Version('2.1'):
Expand Down Expand Up @@ -174,9 +175,12 @@ 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))
if self.license_expression:
write_field('License-Expression', rfc822_escape(self.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)
Expand Down
7 changes: 7 additions & 0 deletions setuptools/_distutils/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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

Expand Down
13 changes: 8 additions & 5 deletions setuptools/config/_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ class Distribution(_Distribution):
'long_description_content_type': lambda: None,
'project_urls': dict,
'provides_extras': dict, # behaves like an ordered set
'license_expression': lambda: None,
'license_file': lambda: None,
'license_files': lambda: None,
'install_requires': list,
Expand Down
63 changes: 63 additions & 0 deletions setuptools/tests/config/test_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,28 @@ def main_gui(): pass
def main_tomatoes(): pass
"""

PEP639_LICENSE_TEXT = """\
[project]
name = "spam"
version = "2020.0.0"
authors = [
{email = "[email protected]"},
{name = "Tzu-Ping Chung"}
]
license = {text = "MIT"}
"""

PEP639_LICENSE_EXPRESSION = """\
[project]
name = "spam"
version = "2020.0.0"
authors = [
{email = "[email protected]"},
{name = "Tzu-Ping Chung"}
]
license = "MIT"
"""


def _pep621_example_project(
tmp_path,
Expand Down Expand Up @@ -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`
Expand Down

0 comments on commit 93d0c67

Please sign in to comment.