diff --git a/newsfragments/4523.feature.rst b/newsfragments/4523.feature.rst new file mode 100644 index 0000000000..ca74e07d56 --- /dev/null +++ b/newsfragments/4523.feature.rst @@ -0,0 +1 @@ +Added a warning when ``tool.setuptools.dynamic`` defines a field that is not listed in ``project.dynamic``. diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 9e2b16c2e5..3097065a6f 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -215,6 +215,7 @@ def expand(self): self._expand_packages() self._canonic_package_data() self._canonic_package_data("exclude-package-data") + self._warn_about_missing_dynamic() # A distribution object is required for discovering the correct package_dir dist = self._ensure_dist() @@ -228,6 +229,19 @@ def expand(self): dist._referenced_files.update(self._referenced_files) return self.config + def _warn_about_missing_dynamic(self) -> None: + """Warn when a directive cannot be used because ``project.dynamic`` omits it.""" + for field in sorted(self.dynamic_cfg): + if not self._uses_dynamic_directive(field): + _MissingDynamicDirective.emit(field=field) + + def _uses_dynamic_directive(self, field: str) -> bool: + if field in self.dynamic: + return True + return field == "entry-points" and any( + item in self.dynamic for item in ("scripts", "gui-scripts") + ) + def _expand_packages(self): packages = self.setuptools_cfg.get("packages") if packages is None or isinstance(packages, (list, tuple)): @@ -475,3 +489,19 @@ class _ToolsTypoInMetadata(SetuptoolsWarning): _SUMMARY = ( "Ignoring [tools.setuptools] in pyproject.toml, did you mean [tool.setuptools]?" ) + + +class _MissingDynamicDirective(SetuptoolsWarning): + _SUMMARY = "`tool.setuptools.dynamic.{field}` is ignored." + + _DETAILS = """ + The following seems to be defined in `tool.setuptools.dynamic`: + + `{field}` + + According to the spec, setuptools cannot use this value unless `{field}` is + listed as `dynamic` in the `[project]` table. + + To prevent this problem, you can list `{field}` under `project.dynamic` or + remove the corresponding `tool.setuptools.dynamic.{field}` configuration. + """ diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index e031ea810b..ae447d09f3 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -1,4 +1,5 @@ import re +import warnings from configparser import ConfigParser from inspect import cleandoc @@ -210,6 +211,23 @@ def test_scripts_not_listed_in_dynamic(self, tmp_path, missing_dynamic): with pytest.raises(OptionError, match=re.compile(msg, re.DOTALL)): expand_configuration(self.pyproject(dynamic), tmp_path) + def test_entry_points_file_used_for_scripts_without_warning(self, tmp_path): + entry_points = ConfigParser() + entry_points.read_dict({"console_scripts": {"a": "mod.a:func"}}) + with open(tmp_path / "entry-points.txt", "w", encoding="utf-8") as f: + entry_points.write(f) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + expanded = expand_configuration(self.pyproject(["scripts"]), tmp_path) + + assert expanded["project"]["scripts"]["a"] == "mod.a:func" + assert not [ + warning + for warning in caught + if "tool.setuptools.dynamic.entry-points" in str(warning.message) + ] + class TestClassifiers: def test_dynamic(self, tmp_path): diff --git a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py index 9fc8050743..5465063270 100644 --- a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py +++ b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py @@ -33,6 +33,31 @@ def test_dynamic_dependencies(tmp_path): assert dist.install_requires == ["six"] +def test_warns_when_dynamic_dependencies_not_listed(tmp_path): + files = { + "requirements.txt": "six\n", + "pyproject.toml": cleandoc( + """ + [project] + name = "myproj" + version = "1.0" + + [tool.setuptools.dynamic.dependencies] + file = ["requirements.txt"] + """ + ), + } + path.build(files, prefix=tmp_path) + dist = Distribution() + with pytest.warns( + SetuptoolsWarning, + match=r"(?s)tool\.setuptools\.dynamic\.dependencies.*project\.dynamic", + ): + dist = apply_configuration(dist, tmp_path / "pyproject.toml") + + assert not dist.install_requires + + def test_dynamic_optional_dependencies(tmp_path): files = { "requirements-docs.txt": "sphinx\n # comment\n",