Repository: pypa/setuptools
Pull request #4830 (2025-02-17 13:37:52)

Bump core metadata version to 2.4
## Summary of changes

Should be rebased after #4706 is merged into `feature/pep639`.

- Bump core metadata version to `2.4`
- Add `License-Expression` to core metadata

Refs:
- https://github.com/pypa/setuptools/issues/4629
- https://github.com/pypa/setuptools/pull/4829


Patch:
diff --git a/newsfragments/4830.feature.rst b/newsfragments/4830.feature.rst
new file mode 100644
index 000000000..f21d17515
--- /dev/null
+++ b/newsfragments/4830.feature.rst
@@ -0,0 +1,1 @@
+Bump core metadata version to ``2.4``. -- by :user:`cdce8p`
diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py
index 5342186c0..60b47b375 100644
--- a/setuptools/_core_metadata.py
+++ b/setuptools/_core_metadata.py
@@ -28,7 +28,7 @@ from distutils.util import rfc822_escape
 def get_metadata_version(self):
     mv = getattr(self, 'metadata_version', None)
     if mv is None:
-        mv = Version('2.2')
+        mv = Version('2.4')
         self.metadata_version = mv
     return mv
 
@@ -176,8 +176,9 @@ def write_pkg_file(self, file):  # noqa: C901  # is too complex (14)  # FIXME
         if attr_val is not None:
             write_field(field, attr_val)
 
-    license = self.license_expression or self.get_license()
-    if license:
+    if license_expression := self.license_expression:
+        write_field('License-Expression', license_expression)
+    elif license := self.get_license():
         write_field('License', rfc822_escape(license))
 
     for label, url in self.project_urls.items():


Test patch:
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 468a1ba01..c43798870 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -280,20 +280,28 @@ def test_utf8_maintainer_in_metadata(  # issue-3663
 
 
 @pytest.mark.parametrize(
-    ('pyproject_text', 'license', 'license_expression', 'content_str'),
+    (
+        'pyproject_text',
+        'license',
+        'license_expression',
+        'content_str',
+        'not_content_str',
+    ),
     (
         pytest.param(
             PEP639_LICENSE_TEXT,
             'MIT',
             None,
             'License: MIT',
+            'License-Expression: ',
             id='license-text',
         ),
         pytest.param(
             PEP639_LICENSE_EXPRESSION,
             None,
             'MIT OR Apache-2.0',
-            'License: MIT OR Apache-2.0',  # TODO Metadata version '2.4'
+            'License-Expression: MIT OR Apache-2.0',
+            'License: ',
             id='license-expression',
         ),
     ),
@@ -302,6 +310,7 @@ def test_license_in_metadata(
     license,
     license_expression,
     content_str,
+    not_content_str,
     pyproject_text,
     tmp_path,
 ):
@@ -317,7 +326,9 @@ def test_license_in_metadata(
     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 "Metadata-Version: 2.4" in content
     assert content_str in content
+    assert not_content_str not in content
 
 
 def test_license_expression_with_bad_classifier(tmp_path):
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
index 9756d7c51..528e2c13d 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -522,7 +522,7 @@ class TestEggInfo:
         with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
             pkg_info_lines = fp.read().split('\n')
         assert 'Provides-Extra: foobar' in pkg_info_lines
-        assert 'Metadata-Version: 2.2' in pkg_info_lines
+        assert 'Metadata-Version: 2.4' in pkg_info_lines
 
     def test_doesnt_provides_extra(self, tmpdir_cwd, env):
         self._setup_script_with_requires(
@@ -1089,7 +1089,7 @@ class TestEggInfo:
         with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
             pkg_info_lines = fp.read().split('\n')
         # Update metadata version if changed
-        assert self._extract_mv_version(pkg_info_lines) == (2, 2)
+        assert self._extract_mv_version(pkg_info_lines) == (2, 4)
 
     def test_long_description_content_type(self, tmpdir_cwd, env):
         # Test that specifying a `long_description_content_type` keyword arg to
@@ -1116,7 +1116,7 @@ class TestEggInfo:
             pkg_info_lines = fp.read().split('\n')
         expected_line = 'Description-Content-Type: text/markdown'
         assert expected_line in pkg_info_lines
-        assert 'Metadata-Version: 2.2' in pkg_info_lines
+        assert 'Metadata-Version: 2.4' in pkg_info_lines
 
     def test_long_description(self, tmpdir_cwd, env):
         # Test that specifying `long_description` and `long_description_content_type`
@@ -1135,7 +1135,7 @@ class TestEggInfo:
         egg_info_dir = os.path.join('.', 'foo.egg-info')
         with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
             pkg_info_lines = fp.read().split('\n')
-        assert 'Metadata-Version: 2.2' in pkg_info_lines
+        assert 'Metadata-Version: 2.4' in pkg_info_lines
         assert '' == pkg_info_lines[-1]  # last line should be empty
         long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :]
         assert 'This is a long description' in long_desc_lines