Skip to content

Commit 023c98d

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

File tree

5 files changed

+83
-82
lines changed

5 files changed

+83
-82
lines changed

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: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
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
1213

1314
from ..metadata import _ALL_FIELDS
1415

@@ -22,7 +23,10 @@ def __dir__() -> list[str]:
2223
@runtime_checkable
2324
class DynamicMetadataProtocol(Protocol):
2425
def dynamic_metadata(
25-
self, fields: Iterable[str], settings: dict[str, Any], metadata: dict[str, Any]
26+
self,
27+
fields: Iterable[str],
28+
settings: dict[str, Any],
29+
metadata: Mapping[str, Any],
2630
) -> dict[str, Any]: ...
2731

2832

@@ -40,20 +44,10 @@ def dynamic_wheel(
4044
) -> bool: ...
4145

4246

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-
5247
DMProtocols = Union[
5348
DynamicMetadataProtocol,
5449
DynamicMetadataRequirementsProtocol,
5550
DynamicMetadataWheelProtocol,
56-
DynamicMetadataNeeds,
5751
]
5852

5953

@@ -77,39 +71,76 @@ def load_provider(
7771

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

104133

105134
def load_dynamic_metadata(
135+
project: Mapping[str, Any],
106136
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)}
137+
) -> dict[str, Any]:
138+
initial = {f: (p, c) for (f, p, c) in _load_dynamic_metadata(metadata)}
109139

110-
dynamic_fields = initial.keys()
111-
sorter = TopologicalSorter(
112-
{f: n & dynamic_fields for f, (_, _, n) in initial.items()}
140+
settings = DynamicSettings(
141+
settings={f: c for f, (v, c) in initial.items()},
142+
project=dict(project),
143+
providers={k: v for k, (v, _) in initial.items()},
113144
)
114-
order = sorter.static_order()
115-
return [(f, *initial[f][:2]) for f in order]
145+
146+
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: 2 additions & 9 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(
@@ -37,10 +37,3 @@ def dynamic_metadata(
3737
lambda r: r.format(**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: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ 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"]
1615
result = {"dev" = ["{name}=={version}"]}
1716

1817
[tool.setuptools_scm]

0 commit comments

Comments
 (0)