Skip to content

Commit f30cd58

Browse files
committed
refactor: implicit needs
Signed-off-by: Henry Schreiner <[email protected]>
1 parent cbddac0 commit f30cd58

File tree

8 files changed

+98
-93
lines changed

8 files changed

+98
-93
lines changed

docs/configuration/dynamic.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,12 @@ You can access other metadata fields and produce templated outputs.
124124
```toml
125125
[tool.scikit-build.metadata.optional-dependencies]
126126
provider = "scikit_build_core.metadata.template"
127-
needs = ["version"]
128-
result = {"dev" = ["subpackage=={project.version}"]}
127+
result = {"dev" = ["{project[name]}=={project[version]}"]}
129128
```
130-
131-
You can specify `needs` to ensure other metadata is computed before this one.
132-
You can use `result` to specify the output. The result must match the type of
133-
the metadata field you are writing to. You can use `project` to access the
134-
current metadata values.
129+
You can use `project` to access the current metadata values. You can reference
130+
other dynamic metadata fields, and they will be computed before this one. You
131+
can use `result` to specify the output. The result must match the type of the
132+
metadata field you are writing to.
135133

136134
```{versionadded} 0.11.2
137135

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ dependencies = [
3939
"pathspec >=0.10.1",
4040
"tomli >=1.2.2; python_version<'3.11'",
4141
"typing-extensions >=3.10.0; python_version<'3.9'",
42-
"graphlib_backport >=1; python_version<'3.9'",
4342
]
4443
# Note: cmake and possibly ninja are also required if those are not already
4544
# present (user controllable) - but a system version is fine.
@@ -183,7 +182,7 @@ disallow_untyped_defs = true
183182
disallow_incomplete_defs = true
184183

185184
[[tool.mypy.overrides]]
186-
module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv", "graphlib"]
185+
module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv"]
187186
ignore_missing_imports = true
188187

189188

src/scikit_build_core/build/metadata.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import copy
4-
import inspect
54
import sys
65
from typing import TYPE_CHECKING, Any
76

@@ -39,28 +38,12 @@ def get_standard_metadata(
3938
pyproject_dict: Mapping[str, Any],
4039
settings: ScikitBuildSettings,
4140
) -> StandardMetadata:
42-
new_pyproject_dict = copy.deepcopy(pyproject_dict)
41+
new_pyproject_dict = copy.deepcopy(dict(pyproject_dict))
4342

4443
# Handle any dynamic metadata
45-
for field, provider, config in load_dynamic_metadata(settings.metadata):
46-
if provider is None:
47-
msg = f"{field} is missing provider"
48-
raise KeyError(msg)
49-
if field not in pyproject_dict.get("project", {}).get("dynamic", []):
50-
msg = f"{field} is not in project.dynamic"
51-
raise KeyError(msg)
52-
53-
sig = inspect.signature(provider.dynamic_metadata)
54-
if len(sig.parameters) < 3:
55-
# Backcompat for dynamic_metadata without metadata dict
56-
new_pyproject_dict["project"][field] = provider.dynamic_metadata( # type: ignore[call-arg]
57-
field, config
58-
)
59-
else:
60-
new_pyproject_dict["project"][field] = provider.dynamic_metadata(
61-
field, config, new_pyproject_dict["project"].copy()
62-
)
63-
new_pyproject_dict["project"]["dynamic"].remove(field)
44+
new_pyproject_dict["project"] = load_dynamic_metadata(
45+
new_pyproject_dict["project"], settings.metadata
46+
)
6447

6548
if settings.strict_config:
6649
extra_keys_top = extras_top_level(new_pyproject_dict)
Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import importlib
5+
import inspect
46
import sys
7+
from collections.abc import Iterator, Mapping
58
from pathlib import Path
69
from typing import TYPE_CHECKING, Any, Protocol, Union, runtime_checkable
710

8-
from graphlib import TopologicalSorter
9-
1011
if TYPE_CHECKING:
11-
from collections.abc import Generator, Iterable, Mapping
12+
from collections.abc import Generator, Iterable
13+
14+
StrMapping = Mapping[str, Any]
15+
else:
16+
StrMapping = Mapping
1217

1318
from ..metadata import _ALL_FIELDS
1419

@@ -22,7 +27,10 @@ def __dir__() -> list[str]:
2227
@runtime_checkable
2328
class DynamicMetadataProtocol(Protocol):
2429
def dynamic_metadata(
25-
self, fields: Iterable[str], settings: dict[str, Any], metadata: dict[str, Any]
30+
self,
31+
fields: Iterable[str],
32+
settings: dict[str, Any],
33+
metadata: Mapping[str, Any],
2634
) -> dict[str, Any]: ...
2735

2836

@@ -40,20 +48,10 @@ def dynamic_wheel(
4048
) -> bool: ...
4149

4250

43-
@runtime_checkable
44-
class DynamicMetadataNeeds(DynamicMetadataProtocol, Protocol):
45-
def dynamic_metadata_needs(
46-
self,
47-
field: str,
48-
settings: Mapping[str, object] | None = None,
49-
) -> list[str]: ...
50-
51-
5251
DMProtocols = Union[
5352
DynamicMetadataProtocol,
5453
DynamicMetadataRequirementsProtocol,
5554
DynamicMetadataWheelProtocol,
56-
DynamicMetadataNeeds,
5755
]
5856

5957

@@ -77,39 +75,76 @@ def load_provider(
7775

7876
def _load_dynamic_metadata(
7977
metadata: Mapping[str, Mapping[str, str]],
80-
) -> Generator[
81-
tuple[str, DMProtocols | None, dict[str, str], frozenset[str]], None, None
82-
]:
78+
) -> Generator[tuple[str, DMProtocols, dict[str, str]], None, None]:
8379
for field, orig_config in metadata.items():
84-
if "provider" in orig_config:
85-
if field not in _ALL_FIELDS:
86-
msg = f"{field} is not a valid field"
87-
raise KeyError(msg)
88-
config = dict(orig_config)
89-
provider = config.pop("provider")
90-
provider_path = config.pop("provider-path", None)
91-
loaded_provider = load_provider(provider, provider_path)
92-
needs = frozenset(
93-
loaded_provider.dynamic_metadata_needs(field, config)
94-
if isinstance(loaded_provider, DynamicMetadataNeeds)
95-
else []
80+
if "provider" not in orig_config:
81+
msg = "Missing provider in dynamic metadata"
82+
raise KeyError(msg)
83+
84+
if field not in _ALL_FIELDS:
85+
msg = f"{field} is not a valid field"
86+
raise KeyError(msg)
87+
config = dict(orig_config)
88+
provider = config.pop("provider")
89+
provider_path = config.pop("provider-path", None)
90+
loaded_provider = load_provider(provider, provider_path)
91+
yield field, loaded_provider, config
92+
93+
94+
@dataclasses.dataclass
95+
class DynamicSettings(StrMapping):
96+
settings: dict[str, dict[str, Any]]
97+
project: dict[str, Any]
98+
providers: dict[str, DMProtocols]
99+
100+
def __getitem__(self, key: str) -> Any:
101+
# Try to get the settings from either the static file or dynamic metadata provider
102+
if key in self.project:
103+
return self.project[key]
104+
105+
# Check if we are in a loop, i.e. something else is already requesting
106+
# this key while trying to get another key
107+
if key not in self.providers:
108+
dep_type = "missing" if key in self.settings else "circular"
109+
msg = f"Encountered a {dep_type} dependency at {key}"
110+
raise ValueError(msg)
111+
112+
provider = self.providers.pop(key)
113+
sig = inspect.signature(provider.dynamic_metadata)
114+
if len(sig.parameters) < 3:
115+
# Backcompat for dynamic_metadata without metadata dict
116+
self.project[key] = provider.dynamic_metadata( # type: ignore[call-arg]
117+
key, self.settings[key]
96118
)
97-
if needs > _ALL_FIELDS:
98-
msg = f"Invalid dyanmic_metada_needs: {needs - _ALL_FIELDS}"
99-
raise KeyError(msg)
100-
yield field, loaded_provider, config, needs
101119
else:
102-
yield field, None, dict(orig_config), frozenset()
120+
self.project[key] = provider.dynamic_metadata(
121+
key, self.settings[key], self.project
122+
)
123+
self.project["dynamic"].remove(key)
124+
125+
return self.project[key]
126+
127+
def __iter__(self) -> Iterator[str]:
128+
# Iterate over the keys of the static settings
129+
yield from self.project
130+
131+
# Iterate over the keys of the dynamic metadata providers
132+
yield from self.providers
133+
134+
def __len__(self) -> int:
135+
return len(self.project) + len(self.providers)
103136

104137

105138
def load_dynamic_metadata(
139+
project: Mapping[str, Any],
106140
metadata: Mapping[str, Mapping[str, str]],
107-
) -> list[tuple[str, DMProtocols | None, dict[str, str]]]:
108-
initial = {f: (p, c, n) for (f, p, c, n) in _load_dynamic_metadata(metadata)}
141+
) -> dict[str, Any]:
142+
initial = {f: (p, c) for (f, p, c) in _load_dynamic_metadata(metadata)}
109143

110-
dynamic_fields = initial.keys()
111-
sorter = TopologicalSorter(
112-
{f: n & dynamic_fields for f, (_, _, n) in initial.items()}
144+
settings = DynamicSettings(
145+
settings={f: c for f, (v, c) in initial.items()},
146+
project=dict(project),
147+
providers={k: v for k, (v, _) in initial.items()},
113148
)
114-
order = sorter.static_order()
115-
return [(f, *initial[f][:2]) for f in order]
149+
150+
return dict(settings)

src/scikit_build_core/metadata/fancy_pypi_readme.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from __future__ import annotations
22

33
from pathlib import Path
4-
from typing import Any
4+
from typing import TYPE_CHECKING, Any
55

66
from .._compat import tomllib
77

8+
if TYPE_CHECKING:
9+
from collections.abc import Mapping
10+
811
__all__ = [
912
"dynamic_metadata",
10-
"dynamic_requires_needs",
1113
"get_requires_for_dynamic_metadata",
1214
]
1315

@@ -19,7 +21,7 @@ def __dir__() -> list[str]:
1921
def dynamic_metadata(
2022
field: str,
2123
settings: dict[str, list[str] | str],
22-
metadata: dict[str, Any],
24+
metadata: Mapping[str, Any],
2325
) -> str | dict[str, str]:
2426
from hatch_fancy_pypi_readme._builder import build_text
2527
from hatch_fancy_pypi_readme._config import load_and_validate_config
@@ -64,10 +66,3 @@ def get_requires_for_dynamic_metadata(
6466
_settings: dict[str, object] | None = None,
6567
) -> list[str]:
6668
return ["hatch-fancy-pypi-readme>=22.3"]
67-
68-
69-
def dynamic_requires_needs(
70-
_field: str,
71-
_settings: dict[str, object],
72-
) -> list[str]:
73-
return ["version"]

src/scikit_build_core/metadata/template.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77

88
from . import _process_dynamic_metadata
99

10-
__all__ = ["dynamic_metadata", "dynamic_metadata_needs"]
10+
__all__ = ["dynamic_metadata"]
1111

1212

1313
def __dir__() -> list[str]:
1414
return __all__
1515

1616

17-
KEYS = {"needs", "result"}
17+
KEYS = {"result"}
1818

1919

2020
def dynamic_metadata(
@@ -34,13 +34,6 @@ def dynamic_metadata(
3434

3535
return _process_dynamic_metadata(
3636
field,
37-
lambda r: r.format(**metadata),
37+
lambda r: r.format(project=metadata),
3838
result,
3939
)
40-
41-
42-
def dynamic_metadata_needs(
43-
field: str, # noqa: ARG001
44-
settings: Mapping[str, Any],
45-
) -> list[str]:
46-
return settings.get("needs", [])

tests/packages/dynamic_metadata/plugin_project.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ readme.provider = "scikit_build_core.metadata.fancy_pypi_readme"
1212

1313
[tool.scikit-build.metadata.optional-dependencies]
1414
provider = "scikit_build_core.metadata.template"
15-
needs = ["version"]
16-
result = {"dev" = ["{name}=={version}"]}
15+
result = {"dev" = ["{project[name]}=={project[version]}"]}
1716

1817
[tool.setuptools_scm]
1918

tests/test_dynamic_metadata.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ def test_plugin_metadata():
159159

160160
@pytest.mark.usefixtures("package_dynamic_metadata")
161161
def test_faulty_metadata():
162+
reason_msg = "install hatch-fancy-pypi-readme to test the dynamic metadata plugins"
163+
pytest.importorskip("hatch_fancy_pypi_readme", reason=reason_msg)
164+
162165
with Path("faulty_project.toml").open("rb") as ft:
163166
pyproject = tomllib.load(ft)
164167
settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")

0 commit comments

Comments
 (0)