Skip to content

Commit 8b22d73

Browse files
committed
Mark values from pyproject.toml as static
1 parent f699fd8 commit 8b22d73

File tree

3 files changed

+82
-25
lines changed

3 files changed

+82
-25
lines changed

setuptools/_static.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,7 @@ def is_static(value: Any) -> bool:
180180
False
181181
"""
182182
return isinstance(value, Static) and not value._mutated_
183+
184+
185+
EMPTY_LIST = List()
186+
EMPTY_DICT = Dict()

setuptools/config/_apply_pyprojecttoml.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from types import MappingProxyType
2121
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
2222

23+
from .. import _static
2324
from .._path import StrPath
2425
from ..errors import RemovedConfigError
2526
from ..extension import Extension
@@ -65,10 +66,11 @@ def apply(dist: Distribution, config: dict, filename: StrPath) -> Distribution:
6566

6667

6768
def _apply_project_table(dist: Distribution, config: dict, root_dir: StrPath):
68-
project_table = config.get("project", {}).copy()
69-
if not project_table:
69+
orig_config = config.get("project", {})
70+
if not orig_config:
7071
return # short-circuit
7172

73+
project_table = {k: _static.attempt_conversion(v) for k, v in orig_config.items()}
7274
_handle_missing_dynamic(dist, project_table)
7375
_unify_entry_points(project_table)
7476

@@ -98,7 +100,11 @@ def _apply_tool_table(dist: Distribution, config: dict, filename: StrPath):
98100
raise RemovedConfigError("\n".join([cleandoc(msg), suggestion]))
99101

100102
norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
101-
_set_config(dist, norm_key, value)
103+
corresp = TOOL_TABLE_CORRESPONDENCE.get(norm_key, norm_key)
104+
if callable(corresp):
105+
corresp(dist, value)
106+
else:
107+
_set_config(dist, corresp, value)
102108

103109
_copy_command_options(config, dist, filename)
104110

@@ -143,7 +149,7 @@ def _guess_content_type(file: str) -> str | None:
143149
return None
144150

145151
if ext in _CONTENT_TYPES:
146-
return _CONTENT_TYPES[ext]
152+
return _static.Str(_CONTENT_TYPES[ext])
147153

148154
valid = ", ".join(f"{k} ({v})" for k, v in _CONTENT_TYPES.items())
149155
msg = f"only the following file extensions are recognized: {valid}."
@@ -165,10 +171,11 @@ def _long_description(
165171
text = val.get("text") or expand.read_files(file, root_dir)
166172
ctype = val["content-type"]
167173

168-
_set_config(dist, "long_description", text)
174+
# XXX: Is it completely safe to assume static?
175+
_set_config(dist, "long_description", _static.Str(text))
169176

170177
if ctype:
171-
_set_config(dist, "long_description_content_type", ctype)
178+
_set_config(dist, "long_description_content_type", _static.Str(ctype))
172179

173180
if file:
174181
dist._referenced_files.add(file)
@@ -178,10 +185,12 @@ def _license(dist: Distribution, val: dict, root_dir: StrPath | None):
178185
from setuptools.config import expand
179186

180187
if "file" in val:
181-
_set_config(dist, "license", expand.read_files([val["file"]], root_dir))
188+
# XXX: Is it completely safe to assume static?
189+
value = expand.read_files([val["file"]], root_dir)
190+
_set_config(dist, "license", _static.Str(value))
182191
dist._referenced_files.add(val["file"])
183192
else:
184-
_set_config(dist, "license", val["text"])
193+
_set_config(dist, "license", _static.Str(val["text"]))
185194

186195

187196
def _people(dist: Distribution, val: list[dict], _root_dir: StrPath | None, kind: str):
@@ -197,19 +206,17 @@ def _people(dist: Distribution, val: list[dict], _root_dir: StrPath | None, kind
197206
email_field.append(str(addr))
198207

199208
if field:
200-
_set_config(dist, kind, ", ".join(field))
209+
_set_config(dist, kind, _static.Str(", ".join(field)))
201210
if email_field:
202-
_set_config(dist, f"{kind}_email", ", ".join(email_field))
211+
_set_config(dist, f"{kind}_email", _static.Str(", ".join(email_field)))
203212

204213

205214
def _project_urls(dist: Distribution, val: dict, _root_dir: StrPath | None):
206215
_set_config(dist, "project_urls", val)
207216

208217

209218
def _python_requires(dist: Distribution, val: str, _root_dir: StrPath | None):
210-
from packaging.specifiers import SpecifierSet
211-
212-
_set_config(dist, "python_requires", SpecifierSet(val))
219+
_set_config(dist, "python_requires", _static.SpecifierSet(val))
213220

214221

215222
def _dependencies(dist: Distribution, val: list, _root_dir: StrPath | None):
@@ -237,9 +244,14 @@ def _noop(_dist: Distribution, val: _T) -> _T:
237244
return val
238245

239246

247+
def _identity(val: _T) -> _T:
248+
return val
249+
250+
240251
def _unify_entry_points(project_table: dict):
241252
project = project_table
242-
entry_points = project.pop("entry-points", project.pop("entry_points", {}))
253+
given = project.pop("entry-points", project.pop("entry_points", {}))
254+
entry_points = dict(given) # Avoid problems with static
243255
renaming = {"scripts": "console_scripts", "gui_scripts": "gui_scripts"}
244256
for key, value in list(project.items()): # eager to allow modifications
245257
norm_key = json_compatible_key(key)
@@ -333,6 +345,14 @@ def _get_previous_gui_scripts(dist: Distribution) -> list | None:
333345
return value.get("gui_scripts")
334346

335347

348+
def _set_static_list_metadata(attr: str, dist: Distribution, val: list) -> None:
349+
"""Apply distutils metadata validation but preserve "static" behaviour"""
350+
meta = dist.metadata
351+
setter, getter = getattr(meta, f"set_{attr}"), getattr(meta, f"get_{attr}")
352+
setter(val)
353+
setattr(meta, attr, _static.List(getter()))
354+
355+
336356
def _attrgetter(attr):
337357
"""
338358
Similar to ``operator.attrgetter`` but returns None if ``attr`` is not found
@@ -386,6 +406,12 @@ def _acessor(obj):
386406
See https://packaging.python.org/en/latest/guides/packaging-namespace-packages/.
387407
""",
388408
}
409+
TOOL_TABLE_CORRESPONDENCE = {
410+
# Fields with corresponding core metadata need to be marked as static:
411+
"obsoletes": partial(_set_static_list_metadata, "obsoletes"),
412+
"provides": partial(_set_static_list_metadata, "provides"),
413+
"platforms": partial(_set_static_list_metadata, "platforms"),
414+
}
389415

390416
SETUPTOOLS_PATCHES = {
391417
"long_description_content_type",
@@ -422,17 +448,17 @@ def _acessor(obj):
422448
_RESET_PREVIOUSLY_DEFINED: dict = {
423449
# Fix improper setting: given in `setup.py`, but not listed in `dynamic`
424450
# dict: pyproject name => value to which reset
425-
"license": {},
426-
"authors": [],
427-
"maintainers": [],
428-
"keywords": [],
429-
"classifiers": [],
430-
"urls": {},
431-
"entry-points": {},
432-
"scripts": {},
433-
"gui-scripts": {},
434-
"dependencies": [],
435-
"optional-dependencies": {},
451+
"license": _static.EMPTY_DICT,
452+
"authors": _static.EMPTY_LIST,
453+
"maintainers": _static.EMPTY_LIST,
454+
"keywords": _static.EMPTY_LIST,
455+
"classifiers": _static.EMPTY_LIST,
456+
"urls": _static.EMPTY_DICT,
457+
"entry-points": _static.EMPTY_DICT,
458+
"scripts": _static.EMPTY_DICT,
459+
"gui-scripts": _static.EMPTY_DICT,
460+
"dependencies": _static.EMPTY_LIST,
461+
"optional-dependencies": _static.EMPTY_DICT,
436462
}
437463

438464

setuptools/tests/config/test_apply_pyprojecttoml.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from packaging.metadata import Metadata
1919

2020
import setuptools # noqa: F401 # ensure monkey patch to metadata
21+
from setuptools._static import is_static
2122
from setuptools.command.egg_info import write_requirements
2223
from setuptools.config import expand, pyprojecttoml, setupcfg
2324
from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter
@@ -480,6 +481,32 @@ def test_version(self, tmp_path, monkeypatch, capsys):
480481
assert "42.0" in captured.out
481482

482483

484+
class TestStaticConfig:
485+
def test_mark_static_fields(self, tmp_path, monkeypatch):
486+
monkeypatch.chdir(tmp_path)
487+
toml_config = """
488+
[project]
489+
name = "test"
490+
version = "42.0"
491+
dependencies = ["hello"]
492+
keywords = ["world"]
493+
classifiers = ["private :: hello world"]
494+
[tool.setuptools]
495+
obsoletes = ["abcd"]
496+
provides = ["abcd"]
497+
platforms = ["abcd"]
498+
"""
499+
pyproject = Path(tmp_path, "pyproject.toml")
500+
pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
501+
dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)
502+
assert is_static(dist.install_requires)
503+
assert is_static(dist.metadata.keywords)
504+
assert is_static(dist.metadata.classifiers)
505+
assert is_static(dist.metadata.obsoletes)
506+
assert is_static(dist.metadata.provides)
507+
assert is_static(dist.metadata.platforms)
508+
509+
483510
# --- Auxiliary Functions ---
484511

485512

0 commit comments

Comments
 (0)