From 5edc136b67af4b5db8efe2296e723e4188f13b6f Mon Sep 17 00:00:00 2001 From: linhongkuan Date: Thu, 25 Jun 2026 08:05:35 +0800 Subject: [PATCH] Preserve dynamic metadata for empty dependencies --- newsfragments/5120.bugfix.rst | 1 + setuptools/_core_metadata.py | 5 ++++- setuptools/config/_apply_pyprojecttoml.py | 9 +++++++++ .../tests/config/test_apply_pyprojecttoml.py | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5120.bugfix.rst diff --git a/newsfragments/5120.bugfix.rst b/newsfragments/5120.bugfix.rst new file mode 100644 index 0000000000..954295cd6c --- /dev/null +++ b/newsfragments/5120.bugfix.rst @@ -0,0 +1 @@ +Preserved ``Dynamic: requires-dist`` in generated core metadata when ``dependencies`` is listed as dynamic and the resolved dependency list is empty. diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index a52d5cf755..31ca80a13a 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -211,8 +211,11 @@ def write_field(key, value): self._write_list(file, 'License-File', safe_license_files) _write_requirements(self, file) + dynamic_fields = set(getattr(self, "_setuptools_dynamic", ())) for field, attr in _POSSIBLE_DYNAMIC_FIELDS.items(): - if (val := getattr(self, attr, None)) and not is_static(val): + if field in dynamic_fields: + write_field('Dynamic', field) + elif (val := getattr(self, attr, None)) and not is_static(val): write_field('Dynamic', field) long_description = self.get_long_description() diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 140969feee..6adb6647ca 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -73,6 +73,7 @@ def _apply_project_table(dist: Distribution, config: dict, root_dir: StrPath): project_table = {k: _static.attempt_conversion(v) for k, v in orig_config.items()} _handle_missing_dynamic(dist, project_table) + _handle_dynamic_metadata(dist, project_table) _unify_entry_points(project_table) for field, value in project_table.items(): @@ -137,6 +138,14 @@ def _handle_missing_dynamic(dist: Distribution, project_table: dict): project_table[field] = _RESET_PREVIOUSLY_DEFINED.get(field) +def _handle_dynamic_metadata(dist: Distribution, project_table: dict): + dynamic = set(project_table.get("dynamic", [])) + if "dependencies" in dynamic: + metadata_dynamic = set(getattr(dist.metadata, "_setuptools_dynamic", ())) + metadata_dynamic.add("requires-dist") + dist.metadata._setuptools_dynamic = metadata_dynamic + + def json_compatible_key(key: str) -> str: """As defined in :pep:`566#json-compatible-metadata`""" return key.lower().replace("-", "_") diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index b6b21a3e9b..ec151c5b41 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -646,6 +646,22 @@ def test_listed_in_dynamic(self, tmp_path, attr, field, value): dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist) assert dist_value == value + def test_empty_dynamic_dependencies_in_metadata(self, tmp_path): + pyproject = self.pyproject(tmp_path, ["dependencies"]) + dist = makedist(tmp_path, install_requires=[]) + dist = pyprojecttoml.apply_configuration(dist, pyproject) + metadata = core_metadata(dist) + assert "Dynamic: requires-dist\n" in metadata + assert "Requires-Dist:" not in metadata + + def test_empty_static_dependencies_not_dynamic_in_metadata(self, tmp_path): + extra = "dependencies = []\n" + pyproject = self.pyproject(tmp_path, [], extra) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + metadata = core_metadata(dist) + assert "Dynamic:" not in metadata + assert "Requires-Dist:" not in metadata + def test_license_files_exempt_from_dynamic(self, monkeypatch, tmp_path): """ license-file is currently not considered in the context of dynamic.