Skip to content

Commit 8ccfee3

Browse files
committed
refactor: deduplicate config-setting handling
Generate the declaration entry schema from the dataclass, fold the triplicated post-override handling into one helper, and drop a duplicated test constant. Assisted-by: ClaudeCode:claude-fable-5
1 parent 19c2e4d commit 8ccfee3

5 files changed

Lines changed: 69 additions & 93 deletions

File tree

src/scikit_build_core/resources/scikit-build.schema.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@
621621
"properties": {
622622
"help": {
623623
"type": "string",
624+
"default": "",
624625
"description": "A description of the setting."
625626
},
626627
"type": {
@@ -644,8 +645,8 @@
644645
},
645646
"env": {
646647
"type": "string",
647-
"minLength": 1,
648-
"description": "An environment variable also read for this setting; it takes precedence over `-C`."
648+
"description": "An environment variable also read for this setting; it takes precedence over `-C`.",
649+
"minLength": 1
649650
}
650651
}
651652
}

src/scikit_build_core/settings/config_settings.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from __future__ import annotations
1+
# No `from __future__ import annotations` here: `to_json_schema` reads real
2+
# runtime types from the dataclass fields, like `skbuild_model.py`.
23

34
__lazy_modules__ = {
45
f"{(__spec__.parent or '').rsplit('.', 1)[0]}._logging",
@@ -9,7 +10,7 @@
910

1011
import dataclasses
1112
import re
12-
from typing import Any, Literal
13+
from typing import Any, Dict, List, Literal, Optional, Union
1314

1415
from .._logging import rich_error
1516
from .skbuild_model import ScikitBuildSettings
@@ -27,7 +28,7 @@
2728
]
2829

2930

30-
def __dir__() -> list[str]:
31+
def __dir__() -> List[str]:
3132
return __all__
3233

3334

@@ -36,10 +37,7 @@ def __dir__() -> list[str]:
3637
_NAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+$")
3738

3839
# A "choices" key (allowed values, str type only) was cut from the initial
39-
# release to keep the surface minimal. To re-add: a `choices: list[str] | None`
40-
# field here, load-time validation (list of strings, not with type = 'bool',
41-
# default must be a member), a resolved-value check in resolve_config_settings,
42-
# and the entry-schema property in skbuild_schema.py.
40+
# release to keep the surface minimal; see git history to re-add.
4341
_DECLARATION_KEYS = frozenset({"help", "type", "default", "env"})
4442

4543

@@ -54,28 +52,45 @@ class ConfigSettingDeclaration:
5452
"""
5553

5654
help: str = ""
55+
"""
56+
A description of the setting.
57+
"""
58+
5759
type: Literal["str", "bool"] = "str"
58-
default: str | bool | None = None
59-
env: str | None = None
60+
"""
61+
The type of the setting.
62+
"""
63+
64+
default: Optional[Union[str, bool]] = None
65+
"""
66+
The value used when the setting is not passed; must match the type.
67+
"""
68+
69+
env: Optional[str] = None
70+
"""
71+
An environment variable also read for this setting; it takes precedence over `-C`.
72+
"""
6073

6174

6275
def _error_context(name: str) -> str:
6376
return f"tool.scikit-build.config-setting.{name!r}"
6477

6578

66-
def load_declarations(raw: Any) -> dict[str, ConfigSettingDeclaration]:
79+
def load_declarations(raw: Any) -> Dict[str, ConfigSettingDeclaration]:
6780
"""
6881
Validate and load the raw ``tool.scikit-build.config-setting`` table.
6982
"""
7083
if not isinstance(raw, dict):
7184
rich_error("tool.scikit-build.config-setting must be a table")
85+
if not raw:
86+
return {}
7287

7388
reserved = {
7489
field.name.replace("_", "-")
7590
for field in dataclasses.fields(ScikitBuildSettings)
7691
} | {"overrides", "config-setting", "skbuild"}
7792

78-
decls: dict[str, ConfigSettingDeclaration] = {}
93+
decls: Dict[str, ConfigSettingDeclaration] = {}
7994
for name, entry in raw.items():
8095
if not _NAME_REGEX.match(name):
8196
rich_error(
@@ -130,17 +145,17 @@ def load_declarations(raw: Any) -> dict[str, ConfigSettingDeclaration]:
130145

131146

132147
def resolve_config_settings(
133-
decls: Mapping[str, ConfigSettingDeclaration],
134-
config_settings: Mapping[str, str | list[str] | bool],
135-
env: Mapping[str, str],
136-
) -> dict[str, str | bool | None]:
148+
decls: "Mapping[str, ConfigSettingDeclaration]",
149+
config_settings: "Mapping[str, Union[str, List[str], bool]]",
150+
env: "Mapping[str, str]",
151+
) -> Dict[str, Optional[Union[str, bool]]]:
137152
"""
138153
Resolve declared config-settings; precedence env var > config-setting >
139154
default, with ``None`` meaning "unset".
140155
"""
141-
values: dict[str, str | bool | None] = {}
156+
values: Dict[str, Optional[Union[str, bool]]] = {}
142157
for name, decl in decls.items():
143-
raw: str | bool | None
158+
raw: Optional[Union[str, bool]]
144159
if decl.env is not None and decl.env in env:
145160
raw = env[decl.env]
146161
elif name in config_settings:
@@ -165,7 +180,7 @@ def resolve_config_settings(
165180

166181

167182
def resolve_define_references(
168-
tool_skb: dict[str, Any], values: Mapping[str, str | bool | None]
183+
tool_skb: Dict[str, Any], values: "Mapping[str, Optional[Union[str, bool]]]"
169184
) -> None:
170185
"""
171186
Replace ``{config-setting = "..."}`` values in the raw ``cmake.define``

src/scikit_build_core/settings/skbuild_read_settings.py

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -296,26 +296,33 @@ def __init__(
296296
self.config_setting_decls, config_settings, environ
297297
)
298298

299+
def process_table(
300+
skb: dict[str, Any], declaration_error: str
301+
) -> tuple[set[str], dict[str, OverrideRecord]]:
302+
"""
303+
Apply overrides to one settings table, reject stray config-setting
304+
declarations (the static table was popped from pyproject.toml above,
305+
so any survivor is misplaced), and resolve define references.
306+
"""
307+
matched, overridden = process_overrides(
308+
skb,
309+
state=state,
310+
env=env,
311+
retry=retry,
312+
config_settings=self.custom_config_settings,
313+
)
314+
if "config-setting" in skb:
315+
rich_error(declaration_error)
316+
resolve_define_references(skb, self.custom_config_settings)
317+
return matched, overridden
318+
299319
# Handle overrides
300-
self.overrides, self.overridden_items = process_overrides(
301-
tool_skb,
302-
state=state,
303-
env=env,
304-
retry=retry,
305-
config_settings=self.custom_config_settings,
320+
self.overrides, self.overridden_items = process_table(
321+
tool_skb, "config-setting declarations may not be set by overrides"
306322
)
307-
# The static table was popped above, so any survivor came from an
308-
# override body.
309-
if "config-setting" in tool_skb:
310-
rich_error("config-setting declarations may not be set by overrides")
311-
resolve_define_references(tool_skb, self.custom_config_settings)
312323

313324
# Support for minimum-version='build-system.requires'
314-
tmp_min_v = (
315-
pyproject.get("tool", {})
316-
.get("scikit-build", {})
317-
.get("minimum-version", None)
318-
)
325+
tmp_min_v = tool_skb.get("minimum-version")
319326
if tmp_min_v == "build-system.requires":
320327
reqlist = pyproject["build-system"]["requires"]
321328
min_v = get_min_requires("scikit-build-core", reqlist)
@@ -324,7 +331,7 @@ def __init__(
324331
"scikit-build-core needs a min version in "
325332
"build-system.requires to use minimum-version='build-system.requires'"
326333
)
327-
pyproject["tool"]["scikit-build"]["minimum-version"] = str(min_v)
334+
tool_skb["minimum-version"] = str(min_v)
328335
toml_srcs = [TOMLSource("tool", "scikit-build", settings=pyproject)]
329336
# Human-readable names parallel to ``toml_srcs`` (used by suggestions).
330337
toml_src_names = ["pyproject.toml"]
@@ -346,18 +353,10 @@ def __init__(
346353

347354
if extra_settings is not None:
348355
extra_skb = copy.deepcopy(dict(extra_settings))
349-
extra_matched, extra_overridden = process_overrides(
356+
extra_matched, extra_overridden = process_table(
350357
extra_skb,
351-
state=state,
352-
env=env,
353-
retry=retry,
354-
config_settings=self.custom_config_settings,
358+
"config-setting declarations are only allowed in pyproject.toml",
355359
)
356-
if "config-setting" in extra_skb:
357-
rich_error(
358-
"config-setting declarations are only allowed in pyproject.toml"
359-
)
360-
resolve_define_references(extra_skb, self.custom_config_settings)
361360
self.overrides |= extra_matched
362361
self.overridden_items.update(extra_overridden)
363362
toml_srcs.insert(0, TOMLSource(settings=extra_skb))
@@ -390,18 +389,10 @@ def __init__(
390389
# which merges dicts but takes lists wholesale from the
391390
# highest-precedence source. A future refactor could process
392391
# overrides on the merged view instead.
393-
ep_matched, ep_overridden = process_overrides(
392+
ep_matched, ep_overridden = process_table(
394393
ep_skb,
395-
state=state,
396-
env=env,
397-
retry=retry,
398-
config_settings=self.custom_config_settings,
394+
"config-setting declarations are only allowed in pyproject.toml",
399395
)
400-
if "config-setting" in ep_skb:
401-
rich_error(
402-
"config-setting declarations are only allowed in pyproject.toml"
403-
)
404-
resolve_define_references(ep_skb, self.custom_config_settings)
405396
self.overrides |= ep_matched
406397
ep_source = TOMLSource(settings=ep_skb)
407398
ep_srcs.append(ep_source)

src/scikit_build_core/settings/skbuild_schema.py

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
4242
"Generate the complete schema for scikit-build settings."
4343
assert tool_name == "scikit-build", "Only scikit-build is supported."
4444

45+
from .config_settings import _NAME_REGEX, ConfigSettingDeclaration
4546
from .json_schema import to_json_schema
4647
from .skbuild_model import ScikitBuildSettings
4748

@@ -226,36 +227,13 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
226227
)
227228

228229
# Added after ``props`` is collected so overrides cannot target it.
230+
declaration_entry = to_json_schema(ConfigSettingDeclaration, normalize_keys=True)
231+
declaration_entry["properties"]["env"]["minLength"] = 1
229232
schema["properties"]["config-setting"] = {
230233
"type": "object",
231234
"description": "Declare package-specific config-settings, settable via `-C name=value` or a bound environment variable.",
232235
"additionalProperties": False,
233-
"patternProperties": {
234-
r"^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+$": {
235-
"type": "object",
236-
"additionalProperties": False,
237-
"properties": {
238-
"help": {
239-
"type": "string",
240-
"description": "A description of the setting.",
241-
},
242-
"type": {
243-
"enum": ["str", "bool"],
244-
"default": "str",
245-
"description": "The type of the setting.",
246-
},
247-
"default": {
248-
"oneOf": [{"type": "string"}, {"type": "boolean"}],
249-
"description": "The value used when the setting is not passed; must match the type.",
250-
},
251-
"env": {
252-
"type": "string",
253-
"minLength": 1,
254-
"description": "An environment variable also read for this setting; it takes precedence over `-C`.",
255-
},
256-
},
257-
}
258-
},
236+
"patternProperties": {_NAME_REGEX.pattern: declaration_entry},
259237
}
260238

261239
schema["properties"]["overrides"] = {

tests/test_config_settings.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,6 @@ def test_define_conf_beats_toml_reference(tmp_path: Path):
289289
assert settings_reader.settings.cmake.define["ZMQ_PREFIX"] == "/explicit"
290290

291291

292-
PREFIX_DECLARATION = dedent(
293-
"""\
294-
[tool.scikit-build.config-setting."zmq.prefix"]
295-
help = "Prefix to search for libzmq"
296-
env = "ZMQ_PREFIX"
297-
"""
298-
)
299-
300-
301292
@pytest.mark.parametrize("with_decl", [True, False])
302293
@pytest.mark.parametrize(
303294
"malformed",
@@ -311,7 +302,7 @@ def test_malformed_cmake_table(tmp_path: Path, malformed: str, with_decl: bool):
311302
internal AttributeError in the config-setting resolution helpers."""
312303
content = f"[tool.scikit-build]\n{malformed}\n"
313304
if with_decl:
314-
content += PREFIX_DECLARATION
305+
content += ENV_DECLARATION
315306
pyproject_toml = write_pyproject(tmp_path, content)
316307
with pytest.raises(Exception, match="Failed converting") as exc:
317308
SettingsReader.from_file(pyproject_toml, {"zmq.prefix": "/foo"})

0 commit comments

Comments
 (0)