Skip to content

Commit 01342be

Browse files
authored
feat: if.scikit-build-version (#851)
This adds an `if.scikit-build-version`, and makes sure it can be used to gate features not yet implemented. This should help with #769 by providing a way to support old scikit-build-core's on Python 3.7 even after we drop support. Also fixes auto minimum-version to respect markers. --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent 873a321 commit 01342be

File tree

8 files changed

+242
-16
lines changed

8 files changed

+242
-16
lines changed

docs/changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ New features:
1717
* Adding `if.system-cmake` and `if.cmake-wheel` by @henryiii in #826
1818
* Add `if.from-sdist` for overrides by @henryiii in #812
1919
* Add `if.failed` (retry) by @henryiii in #820
20+
* Add `if.scikit-build-version` by @henryiii in #851
2021
* Packages can also be specified via a table by @henryiii in #841
2122
* Move `cmake.targets` and `cmake.verbose` to `build.targets` and `build.verbose` by @henryiii in #793
2223
* Support multipart regex using `result=` by @henryiii in #818

docs/overrides.md

+10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ At least one must be provided. Then you can specify any collection of valid
2121
options, and those will override if all the items in the `if` are true. They
2222
will match top to bottom, overriding previous matches.
2323

24+
If an override does not match, it's contents are ignored, including invalid
25+
options. Combined with the `if.scikit-build-version` override, this allows using overrides to
26+
support a range of scikit-build-core versions that added settings you want to
27+
use.
28+
29+
### `scikit-build-version` (version)
30+
31+
The version of scikit-build-core itself. Takes a specifier set. If this is
32+
provided, unknown overrides will not be validated unless it's a match.
33+
2434
### `python-version` (version)
2535

2636
The two-digit Python version. Takes a specifier set.

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

+4
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,10 @@
635635
"minProperties": 1,
636636
"additionalProperties": false,
637637
"properties": {
638+
"scikit-build-version": {
639+
"type": "string",
640+
"description": "The version of scikit-build-version. Takes a specifier set."
641+
},
638642
"python-version": {
639643
"type": "string",
640644
"description": "The two-digit Python version. Takes a specifier set."

src/scikit_build_core/settings/auto_requires.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ def get_min_requires(package: str, reqlist: Iterable[str]) -> Version | None:
2929

3030
requires = [Requirement(req) for req in reqlist]
3131

32-
for req in requires:
33-
if canonicalize_name(req.name) == norm_package:
34-
specset = req.specifier
35-
versions = (min_from_spec(v) for v in specset)
36-
return min((v for v in versions if v is not None), default=None)
37-
38-
return None
32+
versions = (
33+
min_from_spec(v)
34+
for req in requires
35+
if canonicalize_name(req.name) == norm_package
36+
and (req.marker is None or req.marker.evaluate())
37+
for v in req.specifier
38+
)
39+
return min((v for v in versions if v is not None), default=None)
3940

4041

4142
def min_from_spec(spec: Specifier) -> Version | None:

src/scikit_build_core/settings/skbuild_overrides.py

+41-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import packaging.tags
1111
from packaging.specifiers import SpecifierSet
1212

13+
from .. import __version__
1314
from .._compat import tomllib
1415
from .._logging import logger
1516
from ..builder.sysconfig import get_abi_flags
@@ -78,10 +79,12 @@ def override_match(
7879
system_cmake: str | None = None,
7980
cmake_wheel: bool | None = None,
8081
abi_flags: str | None = None,
81-
) -> tuple[dict[str, str], set[str]]:
82+
scikit_build_version: str | None = None,
83+
**unknown: Any,
84+
) -> tuple[dict[str, str], set[str], dict[str, Any]]:
8285
"""
83-
Check if the current environment matches the overrides. Returns a dict
84-
of passed matches, with reasons for values, and a set of non-matches.
86+
Check if the current environment matches the overrides. Returns a dict of
87+
passed matches, with reasons for values, and a set of non-matches.
8588
"""
8689

8790
passed_dict = {}
@@ -90,6 +93,16 @@ def override_match(
9093
if current_env is None:
9194
current_env = os.environ
9295

96+
if scikit_build_version is not None:
97+
current_version = __version__
98+
match_msg = version_match(
99+
current_version, scikit_build_version, "scikit-build-core"
100+
)
101+
if match_msg:
102+
passed_dict["scikit-build-version"] = match_msg
103+
else:
104+
failed_set.add("scikit-build-version")
105+
93106
if python_version is not None:
94107
current_python_version = ".".join(str(x) for x in sys.version_info[:2])
95108
match_msg = version_match(current_python_version, python_version, "Python")
@@ -219,11 +232,11 @@ def override_match(
219232
else:
220233
failed_set.add(f"env.{key}")
221234

222-
if not passed_dict and not failed_set:
235+
if len(passed_dict) + len(failed_set) + len(unknown) < 1:
223236
msg = "At least one override must be provided"
224237
raise ValueError(msg)
225238

226-
return passed_dict, failed_set
239+
return passed_dict, failed_set, unknown
227240

228241

229242
def inherit_join(
@@ -261,7 +274,9 @@ def process_overides(
261274
for override in tool_skb.pop("overrides", []):
262275
passed_any: dict[str, str] | None = None
263276
passed_all: dict[str, str] | None = None
264-
failed: set[str] = set()
277+
unknown: set[str] = set()
278+
failed_any: set[str] = set()
279+
failed_all: set[str] = set()
265280
if_override = override.pop("if", None)
266281
if not if_override:
267282
msg = "At least one 'if' override must be provided"
@@ -272,13 +287,14 @@ def process_overides(
272287
if "any" in if_override:
273288
any_override = if_override.pop("any")
274289
select = {k.replace("-", "_"): v for k, v in any_override.items()}
275-
passed_any, _ = override_match(
290+
passed_any, failed_any, unknown_any = override_match(
276291
current_env=env,
277292
current_state=state,
278293
has_dist_info=has_dist_info,
279294
retry=retry,
280295
**select,
281296
)
297+
unknown |= set(unknown_any)
282298

283299
inherit_override = override.pop("inherit", {})
284300
if not isinstance(inherit_override, dict):
@@ -287,20 +303,32 @@ def process_overides(
287303

288304
select = {k.replace("-", "_"): v for k, v in if_override.items()}
289305
if select:
290-
passed_all, failed = override_match(
306+
passed_all, failed_all, unknown_all = override_match(
291307
current_env=env,
292308
current_state=state,
293309
has_dist_info=has_dist_info,
294310
retry=retry,
295311
**select,
296312
)
313+
unknown |= set(unknown_all)
314+
315+
# Verify no unknown options are present unless scikit-build-version is specified
316+
passed_or_failed = {
317+
*(passed_all or {}),
318+
*(passed_any or {}),
319+
*failed_all,
320+
*failed_any,
321+
}
322+
if "scikit-build-version" not in passed_or_failed and unknown:
323+
msg = f"Unknown overrides: {', '.join(unknown)}"
324+
raise TypeError(msg)
297325

298326
# If no overrides are passed, do nothing
299327
if passed_any is None and passed_all is None:
300328
continue
301329

302330
# If normal overrides are passed and one or more fails, do nothing
303-
if passed_all is not None and failed:
331+
if passed_all is not None and failed_all:
304332
continue
305333

306334
# If any is passed, at least one always needs to pass.
@@ -310,6 +338,10 @@ def process_overides(
310338
local_matched = set(passed_any or []) | set(passed_all or [])
311339
global_matched |= local_matched
312340
if local_matched:
341+
if unknown:
342+
msg = f"Unknown overrides: {', '.join(unknown)}"
343+
raise TypeError(msg)
344+
313345
all_str = " and ".join(
314346
[
315347
*(passed_all or {}).values(),

src/scikit_build_core/settings/skbuild_schema.py

+4
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
8888
"minProperties": 1,
8989
"additionalProperties": False,
9090
"properties": {
91+
"scikit-build-version": {
92+
"type": "string",
93+
"description": "The version of scikit-build-version. Takes a specifier set.",
94+
},
9195
"python-version": {
9296
"type": "string",
9397
"description": "The two-digit Python version. Takes a specifier set.",

tests/test_auto.py

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ def test_auto_requires_pkg_version(spec: str, version: Version):
3030
assert get_min_requires("scikit-build-core", reqlist) == version
3131

3232

33+
def test_auto_requires_with_marker():
34+
reqlist = [
35+
"scikit_build_core>=0.1; python_version < '3.7'",
36+
"scikit_build_core>=0.2; python_version >= '3.7'",
37+
]
38+
39+
assert get_min_requires("scikit-build-core", reqlist) == Version("0.2")
40+
41+
3342
@pytest.mark.parametrize(
3443
("expr", "answer"),
3544
[

tests/test_settings_overrides.py

+165
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import pytest
99

10+
import scikit_build_core.settings.skbuild_overrides
1011
from scikit_build_core.settings.skbuild_overrides import regex_match
1112
from scikit_build_core.settings.skbuild_read_settings import SettingsReader
1213

@@ -688,3 +689,167 @@ def test_free_threaded_override(tmp_path: Path):
688689
settings_reader = SettingsReader.from_file(pyproject_toml, state="wheel")
689690
settings = settings_reader.settings
690691
assert settings.wheel.cmake == bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
692+
693+
694+
@pytest.mark.parametrize("version", ["0.9", "0.10"])
695+
def test_skbuild_overrides_version(
696+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, version: str
697+
):
698+
monkeypatch.setattr(
699+
scikit_build_core.settings.skbuild_overrides, "__version__", version
700+
)
701+
pyproject_toml = tmp_path / "pyproject.toml"
702+
pyproject_toml.write_text(
703+
dedent(
704+
"""\
705+
[tool.scikit-build]
706+
wheel.cmake = false
707+
708+
[[tool.scikit-build.overrides]]
709+
if.scikit-build-version = ">=0.10"
710+
wheel.cmake = true
711+
"""
712+
)
713+
)
714+
715+
settings_reader = SettingsReader.from_file(pyproject_toml, state="wheel")
716+
settings = settings_reader.settings
717+
if version == "0.10":
718+
assert settings.wheel.cmake
719+
else:
720+
assert not settings.wheel.cmake
721+
722+
723+
def test_skbuild_overrides_unmatched_version(
724+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
725+
):
726+
monkeypatch.setattr(
727+
scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
728+
)
729+
pyproject_toml = tmp_path / "pyproject.toml"
730+
pyproject_toml.write_text(
731+
dedent(
732+
"""\
733+
[[tool.scikit-build.overrides]]
734+
if.scikit-build-version = "<0.10"
735+
if.is-not-real = true
736+
also-not-real = true
737+
"""
738+
)
739+
)
740+
741+
settings = SettingsReader.from_file(pyproject_toml)
742+
settings.validate_may_exit()
743+
744+
745+
def test_skbuild_overrides_matched_version_if(
746+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
747+
):
748+
monkeypatch.setattr(
749+
scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
750+
)
751+
pyproject_toml = tmp_path / "pyproject.toml"
752+
pyproject_toml.write_text(
753+
dedent(
754+
"""\
755+
[[tool.scikit-build.overrides]]
756+
if.scikit-build-version = ">=0.10"
757+
if.is-not-real = true
758+
"""
759+
)
760+
)
761+
762+
with pytest.raises(TypeError, match="is_not_real"):
763+
SettingsReader.from_file(pyproject_toml)
764+
765+
766+
def test_skbuild_overrides_matched_version_extra(
767+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
768+
):
769+
monkeypatch.setattr(
770+
scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
771+
)
772+
pyproject_toml = tmp_path / "pyproject.toml"
773+
pyproject_toml.write_text(
774+
dedent(
775+
"""\
776+
[[tool.scikit-build.overrides]]
777+
if.scikit-build-version = ">=0.10"
778+
not-real = true
779+
"""
780+
)
781+
)
782+
783+
settings = SettingsReader.from_file(pyproject_toml)
784+
with pytest.raises(SystemExit):
785+
settings.validate_may_exit()
786+
787+
assert "not-real" in capsys.readouterr().out
788+
789+
790+
def test_skbuild_overrides_matched_version_if_any(
791+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
792+
):
793+
monkeypatch.setattr(
794+
scikit_build_core.settings.skbuild_overrides, "__version__", "0.9"
795+
)
796+
pyproject_toml = tmp_path / "pyproject.toml"
797+
pyproject_toml.write_text(
798+
dedent(
799+
"""\
800+
[[tool.scikit-build.overrides]]
801+
if.any.scikit-build-version = ">=0.10"
802+
if.any.not-real = true
803+
also-not-real = true
804+
"""
805+
)
806+
)
807+
808+
settings = SettingsReader.from_file(pyproject_toml)
809+
settings.validate_may_exit()
810+
811+
812+
def test_skbuild_overrides_matched_version_if_any_dual(
813+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
814+
):
815+
monkeypatch.setattr(
816+
scikit_build_core.settings.skbuild_overrides, "__version__", "0.9"
817+
)
818+
pyproject_toml = tmp_path / "pyproject.toml"
819+
pyproject_toml.write_text(
820+
dedent(
821+
"""\
822+
[[tool.scikit-build.overrides]]
823+
if.scikit-build-version = ">=0.10"
824+
if.any.not-real = true
825+
if.any.python-version = ">=3.7"
826+
also-not-real = true
827+
"""
828+
)
829+
)
830+
831+
settings = SettingsReader.from_file(pyproject_toml)
832+
settings.validate_may_exit()
833+
834+
835+
def test_skbuild_overrides_matched_version_if_any_match(
836+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
837+
):
838+
monkeypatch.setattr(
839+
scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
840+
)
841+
pyproject_toml = tmp_path / "pyproject.toml"
842+
pyproject_toml.write_text(
843+
dedent(
844+
"""\
845+
[[tool.scikit-build.overrides]]
846+
if.any.scikit-build-version = ">=0.10"
847+
if.any.not-real = true
848+
if.python-version = ">=3.7"
849+
experimental = true
850+
"""
851+
)
852+
)
853+
854+
with pytest.raises(TypeError, match="not_real"):
855+
SettingsReader.from_file(pyproject_toml)

0 commit comments

Comments
 (0)