summaryrefslogtreecommitdiff
path: root/setuptools/config
diff options
context:
space:
mode:
authorAnderson Bravalheri <andersonbravalheri@gmail.com>2022-03-27 00:25:16 +0000
committerAnderson Bravalheri <andersonbravalheri@gmail.com>2022-03-27 00:50:17 +0000
commitd968977b4eac4064ae500d9c3e89cea1e3f769a3 (patch)
treeeece7043fa38e59c59c34162bf0a3b333a3775dc /setuptools/config
parent4e29d013f13dda7d9db7daaab011ab037af21f66 (diff)
downloadpython-setuptools-git-d968977b4eac4064ae500d9c3e89cea1e3f769a3.tar.gz
Warn if a project metadata is set outside of pyproject without dynamic
- PEP 621 requires the build backend to not backfill values without dynamic. - Some users seem to been writing ``pyproject.toml`` with a "partial" ``[project]`` table even before setuptools added support for pyproject metadata. In several cases this table is incomplete and the real metadata lives either in ``setup.py`` or ``setup.cfg``. To avoid ignoring metadata in these scenarios and resulting in failing builds, the change implemented here adopts a more "forgiving" posture and warns an informative message during the transition period.
Diffstat (limited to 'setuptools/config')
-rw-r--r--setuptools/config/_apply_pyprojecttoml.py99
1 files changed, 89 insertions, 10 deletions
diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 55eab26b..203a5770 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -7,9 +7,10 @@ need to be processed before being applied.
"""
import logging
import os
+import warnings
from collections.abc import Mapping
from email.headerregistry import Address
-from functools import partial
+from functools import partial, reduce
from itertools import chain
from types import MappingProxyType
from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple,
@@ -35,9 +36,29 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
return dist # short-circuit unrelated pyproject.toml file
root_dir = os.path.dirname(filename) or "."
- tool_table = config.get("tool", {}).get("setuptools", {})
+
+ _apply_project_table(dist, config, root_dir)
+ _apply_tool_table(dist, config, filename)
+
+ current_directory = os.getcwd()
+ os.chdir(root_dir)
+ try:
+ dist._finalize_requires()
+ dist._finalize_license_files()
+ finally:
+ os.chdir(current_directory)
+
+ return dist
+
+
+def _apply_project_table(dist: "Distribution", config: dict, root_dir: _Path):
project_table = config.get("project", {}).copy()
+ if not project_table:
+ return # short-circuit
+
+ _handle_missing_dynamic(dist, project_table)
_unify_entry_points(project_table)
+
for field, value in project_table.items():
norm_key = json_compatible_key(field)
corresp = PYPROJECT_CORRESPONDENCE.get(norm_key, norm_key)
@@ -46,6 +67,12 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
else:
_set_config(dist, corresp, value)
+
+def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path):
+ tool_table = config.get("tool", {}).get("setuptools", {})
+ if not tool_table:
+ return # short-circuit
+
for field, value in tool_table.items():
norm_key = json_compatible_key(field)
norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
@@ -53,15 +80,17 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
_copy_command_options(config, dist, filename)
- current_directory = os.getcwd()
- os.chdir(root_dir)
- try:
- dist._finalize_requires()
- dist._finalize_license_files()
- finally:
- os.chdir(current_directory)
- return dist
+def _handle_missing_dynamic(dist: "Distribution", project_table: dict):
+ """Be temporarily forgiving with ``dynamic`` fields not listed in ``dynamic``"""
+ # TODO: Set fields back to `None` once the feature stabilizes
+ dynamic = set(project_table.get("dynamic", []))
+ for field, getter in _PREVIOUSLY_DEFINED.items():
+ if not (field in project_table or field in dynamic):
+ value = getter(dist)
+ if value:
+ msg = _WouldIgnoreField.message(field, value)
+ warnings.warn(msg, _WouldIgnoreField)
def json_compatible_key(key: str) -> str:
@@ -235,6 +264,39 @@ def _normalise_cmd_options(desc: List[Tuple[str, Optional[str], str]]) -> Set[st
return {_normalise_cmd_option_key(fancy_option[0]) for fancy_option in desc}
+def _attrgetter(attr):
+ """
+ Similar to ``operator.attrgetter`` but returns None if ``attr`` is not found
+ >>> from types import SimpleNamespace
+ >>> obj = SimpleNamespace(a=42, b=SimpleNamespace(c=13))
+ >>> _attrgetter("a")(obj)
+ 42
+ >>> _attrgetter("b.c")(obj)
+ 13
+ >>> _attrgetter("d")(obj) is None
+ True
+ """
+ return partial(reduce, lambda acc, x: getattr(acc, x, None), attr.split("."))
+
+
+def _some_attrgetter(*items):
+ """
+ Return the first "truth-y" attribute or None
+ >>> from types import SimpleNamespace
+ >>> obj = SimpleNamespace(a=42, b=SimpleNamespace(c=13))
+ >>> _some_attrgetter("d", "a", "b.c")(obj)
+ 42
+ >>> _some_attrgetter("d", "e", "b.c", "a")(obj)
+ 13
+ >>> _some_attrgetter("d", "e", "f")(obj) is None
+ True
+ """
+ def _acessor(obj):
+ values = (_attrgetter(i)(obj) for i in items)
+ return next((i for i in values if i), None)
+ return _acessor
+
+
PYPROJECT_CORRESPONDENCE: Dict[str, _Correspondence] = {
"readme": _long_description,
"license": _license,
@@ -251,6 +313,23 @@ TOOL_TABLE_RENAMES = {"script_files": "scripts"}
SETUPTOOLS_PATCHES = {"long_description_content_type", "project_urls",
"provides_extras", "license_file", "license_files"}
+_PREVIOUSLY_DEFINED = {
+ "name": _attrgetter("metadata.name"),
+ "version": _attrgetter("metadata.version"),
+ "description": _attrgetter("metadata.description"),
+ "readme": _attrgetter("metadata.long_description"),
+ "requires-python": _some_attrgetter("python_requires", "metadata.python_requires"),
+ "license": _attrgetter("metadata.license"),
+ "authors": _some_attrgetter("metadata.author", "metadata.author_email"),
+ "maintainers": _some_attrgetter("metadata.maintainer", "metadata.maintainer_email"),
+ "keywords": _attrgetter("metadata.keywords"),
+ "classifiers": _attrgetter("metadata.classifiers"),
+ "urls": _attrgetter("metadata.project_urls"),
+ "entry-points": _attrgetter("entry_points"),
+ "dependencies": _some_attrgetter("_orig_install_requires", "install_requires"),
+ "optional-dependencies": _some_attrgetter("_orig_extras_require", "extras_require"),
+}
+
class _WouldIgnoreField(UserWarning):
"""Inform users that ``pyproject.toml`` would overwrite previously defined metadata.