diff options
| author | Anderson Bravalheri <andersonbravalheri@gmail.com> | 2022-03-27 00:25:16 +0000 |
|---|---|---|
| committer | Anderson Bravalheri <andersonbravalheri@gmail.com> | 2022-03-27 00:50:17 +0000 |
| commit | d968977b4eac4064ae500d9c3e89cea1e3f769a3 (patch) | |
| tree | eece7043fa38e59c59c34162bf0a3b333a3775dc /setuptools/config | |
| parent | 4e29d013f13dda7d9db7daaab011ab037af21f66 (diff) | |
| download | python-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.py | 99 |
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. |
