Repository: scikit-build/scikit-build-core
Pull request #852 (2024-08-06 00:23:09)

chore(deps): update pre-commit hooks
<!--pre-commit.ci start-->
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.6)
- [github.com/pre-commit/mirrors-mypy: v1.10.1 → v1.11.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.1...v1.11.1)
- [github.com/henryiii/validate-pyproject-schema-store: 2024.07.01 → 2024.07.29](https://github.com/henryiii/validate-pyproject-schema-store/compare/2024.07.01...2024.07.29)
- [github.com/python-jsonschema/check-jsonschema: 0.28.6 → 0.29.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.6...0.29.1)
<!--pre-commit.ci end-->

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/852?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.75%. Comparing base [(`873a321`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/873a321689f75834898020641aae460dfc78f1ed?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`1f7f1ec`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/1f7f1ec495a308b368291ef4ea190595172ac3cd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #852      +/-   ##
==========================================
- Coverage   83.87%   83.75%   -0.12%     
==========================================
  Files          74       74              
  Lines        4347     4347              
==========================================
- Hits         3646     3641       -5     
- Misses        701      706       +5     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/852?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index db73127..4f886a1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -25,7 +25,7 @@ repos:
         exclude: "^tests"
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.5.0
+    rev: v0.5.6
     hooks:
       - id: ruff
         args: ["--fix", "--show-fixes"]
@@ -60,7 +60,7 @@ repos:
         stages: [manual]
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.10.1
+    rev: v1.11.1
     hooks:
       - id: mypy
         exclude: |
@@ -130,12 +130,12 @@ repos:
         additional_dependencies: [cogapp]
 
   - repo: https://github.com/henryiii/validate-pyproject-schema-store
-    rev: 2024.07.01
+    rev: 2024.07.29
     hooks:
       - id: validate-pyproject
 
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.28.6
+    rev: 0.29.1
     hooks:
       - id: check-dependabot
       - id: check-github-workflows
diff --git a/docs/changelog.md b/docs/changelog.md
index 9bee9ec..dc2884c 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -17,6 +17,7 @@ New features:
 * Adding `if.system-cmake` and `if.cmake-wheel` by @henryiii in #826
 * Add `if.from-sdist` for overrides by @henryiii in #812
 * Add `if.failed` (retry) by @henryiii in #820
+* Add `if.scikit-build-version` by @henryiii in #851
 * Packages can also be specified via a table by @henryiii in #841
 * Move `cmake.targets` and `cmake.verbose` to `build.targets` and `build.verbose` by @henryiii in #793
 * Support multipart regex using `result=` by @henryiii in #818
diff --git a/docs/overrides.md b/docs/overrides.md
index d70b159..1cac13b 100644
--- a/docs/overrides.md
+++ b/docs/overrides.md
@@ -21,6 +21,16 @@ At least one must be provided. Then you can specify any collection of valid
 options, and those will override if all the items in the `if` are true. They
 will match top to bottom, overriding previous matches.
 
+If an override does not match, it's contents are ignored, including invalid
+options. Combined with the `if.scikit-build-version` override, this allows using overrides to
+support a range of scikit-build-core versions that added settings you want to
+use.
+
+### `scikit-build-version` (version)
+
+The version of scikit-build-core itself. Takes a specifier set. If this is
+provided, unknown overrides will not be validated unless it's a match.
+
 ### `python-version` (version)
 
 The two-digit Python version. Takes a specifier set.
diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json
index f9401d8..6cfaa49 100644
--- a/src/scikit_build_core/resources/scikit-build.schema.json
+++ b/src/scikit_build_core/resources/scikit-build.schema.json
@@ -635,6 +635,10 @@
       "minProperties": 1,
       "additionalProperties": false,
       "properties": {
+        "scikit-build-version": {
+          "type": "string",
+          "description": "The version of scikit-build-version. Takes a specifier set."
+        },
         "python-version": {
           "type": "string",
           "description": "The two-digit Python version. Takes a specifier set."
diff --git a/src/scikit_build_core/settings/auto_requires.py b/src/scikit_build_core/settings/auto_requires.py
index 6195abe..83436ea 100644
--- a/src/scikit_build_core/settings/auto_requires.py
+++ b/src/scikit_build_core/settings/auto_requires.py
@@ -29,13 +29,14 @@ def get_min_requires(package: str, reqlist: Iterable[str]) -> Version | None:
 
     requires = [Requirement(req) for req in reqlist]
 
-    for req in requires:
-        if canonicalize_name(req.name) == norm_package:
-            specset = req.specifier
-            versions = (min_from_spec(v) for v in specset)
-            return min((v for v in versions if v is not None), default=None)
-
-    return None
+    versions = (
+        min_from_spec(v)
+        for req in requires
+        if canonicalize_name(req.name) == norm_package
+        and (req.marker is None or req.marker.evaluate())
+        for v in req.specifier
+    )
+    return min((v for v in versions if v is not None), default=None)
 
 
 def min_from_spec(spec: Specifier) -> Version | None:
diff --git a/src/scikit_build_core/settings/skbuild_overrides.py b/src/scikit_build_core/settings/skbuild_overrides.py
index 3c73f61..5315210 100644
--- a/src/scikit_build_core/settings/skbuild_overrides.py
+++ b/src/scikit_build_core/settings/skbuild_overrides.py
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
 import packaging.tags
 from packaging.specifiers import SpecifierSet
 
+from .. import __version__
 from .._compat import tomllib
 from .._logging import logger
 from ..builder.sysconfig import get_abi_flags
@@ -78,10 +79,12 @@ def override_match(
     system_cmake: str | None = None,
     cmake_wheel: bool | None = None,
     abi_flags: str | None = None,
-) -> tuple[dict[str, str], set[str]]:
+    scikit_build_version: str | None = None,
+    **unknown: Any,
+) -> tuple[dict[str, str], set[str], dict[str, Any]]:
     """
-    Check if the current environment matches the overrides. Returns a dict
-    of passed matches, with reasons for values, and a set of non-matches.
+    Check if the current environment matches the overrides. Returns a dict of
+    passed matches, with reasons for values, and a set of non-matches.
     """
 
     passed_dict = {}
@@ -90,6 +93,16 @@ def override_match(
     if current_env is None:
         current_env = os.environ
 
+    if scikit_build_version is not None:
+        current_version = __version__
+        match_msg = version_match(
+            current_version, scikit_build_version, "scikit-build-core"
+        )
+        if match_msg:
+            passed_dict["scikit-build-version"] = match_msg
+        else:
+            failed_set.add("scikit-build-version")
+
     if python_version is not None:
         current_python_version = ".".join(str(x) for x in sys.version_info[:2])
         match_msg = version_match(current_python_version, python_version, "Python")
@@ -219,11 +232,11 @@ def override_match(
                 else:
                     failed_set.add(f"env.{key}")
 
-    if not passed_dict and not failed_set:
+    if len(passed_dict) + len(failed_set) + len(unknown) < 1:
         msg = "At least one override must be provided"
         raise ValueError(msg)
 
-    return passed_dict, failed_set
+    return passed_dict, failed_set, unknown
 
 
 def inherit_join(
@@ -261,7 +274,9 @@ def process_overides(
     for override in tool_skb.pop("overrides", []):
         passed_any: dict[str, str] | None = None
         passed_all: dict[str, str] | None = None
-        failed: set[str] = set()
+        unknown: set[str] = set()
+        failed_any: set[str] = set()
+        failed_all: set[str] = set()
         if_override = override.pop("if", None)
         if not if_override:
             msg = "At least one 'if' override must be provided"
@@ -272,13 +287,14 @@ def process_overides(
         if "any" in if_override:
             any_override = if_override.pop("any")
             select = {k.replace("-", "_"): v for k, v in any_override.items()}
-            passed_any, _ = override_match(
+            passed_any, failed_any, unknown_any = override_match(
                 current_env=env,
                 current_state=state,
                 has_dist_info=has_dist_info,
                 retry=retry,
                 **select,
             )
+            unknown |= set(unknown_any)
 
         inherit_override = override.pop("inherit", {})
         if not isinstance(inherit_override, dict):
@@ -287,20 +303,32 @@ def process_overides(
 
         select = {k.replace("-", "_"): v for k, v in if_override.items()}
         if select:
-            passed_all, failed = override_match(
+            passed_all, failed_all, unknown_all = override_match(
                 current_env=env,
                 current_state=state,
                 has_dist_info=has_dist_info,
                 retry=retry,
                 **select,
             )
+            unknown |= set(unknown_all)
+
+        # Verify no unknown options are present unless scikit-build-version is specified
+        passed_or_failed = {
+            *(passed_all or {}),
+            *(passed_any or {}),
+            *failed_all,
+            *failed_any,
+        }
+        if "scikit-build-version" not in passed_or_failed and unknown:
+            msg = f"Unknown overrides: {', '.join(unknown)}"
+            raise TypeError(msg)
 
         # If no overrides are passed, do nothing
         if passed_any is None and passed_all is None:
             continue
 
         # If normal overrides are passed and one or more fails, do nothing
-        if passed_all is not None and failed:
+        if passed_all is not None and failed_all:
             continue
 
         # If any is passed, at least one always needs to pass.
@@ -310,6 +338,10 @@ def process_overides(
         local_matched = set(passed_any or []) | set(passed_all or [])
         global_matched |= local_matched
         if local_matched:
+            if unknown:
+                msg = f"Unknown overrides: {', '.join(unknown)}"
+                raise TypeError(msg)
+
             all_str = " and ".join(
                 [
                     *(passed_all or {}).values(),
diff --git a/src/scikit_build_core/settings/skbuild_schema.py b/src/scikit_build_core/settings/skbuild_schema.py
index 081f57c..d5151f7 100644
--- a/src/scikit_build_core/settings/skbuild_schema.py
+++ b/src/scikit_build_core/settings/skbuild_schema.py
@@ -88,6 +88,10 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
         "minProperties": 1,
         "additionalProperties": False,
         "properties": {
+            "scikit-build-version": {
+                "type": "string",
+                "description": "The version of scikit-build-version. Takes a specifier set.",
+            },
             "python-version": {
                 "type": "string",
                 "description": "The two-digit Python version. Takes a specifier set.",


Test patch:
diff --git a/tests/test_auto.py b/tests/test_auto.py
index 3133a7a..f4453e8 100644
--- a/tests/test_auto.py
+++ b/tests/test_auto.py
@@ -30,6 +30,15 @@ def test_auto_requires_pkg_version(spec: str, version: Version):
     assert get_min_requires("scikit-build-core", reqlist) == version
 
 
+def test_auto_requires_with_marker():
+    reqlist = [
+        "scikit_build_core>=0.1; python_version < '3.7'",
+        "scikit_build_core>=0.2; python_version >= '3.7'",
+    ]
+
+    assert get_min_requires("scikit-build-core", reqlist) == Version("0.2")
+
+
 @pytest.mark.parametrize(
     ("expr", "answer"),
     [
diff --git a/tests/test_settings_overrides.py b/tests/test_settings_overrides.py
index 645fd70..39f484a 100644
--- a/tests/test_settings_overrides.py
+++ b/tests/test_settings_overrides.py
@@ -7,6 +7,7 @@ from textwrap import dedent
 
 import pytest
 
+import scikit_build_core.settings.skbuild_overrides
 from scikit_build_core.settings.skbuild_overrides import regex_match
 from scikit_build_core.settings.skbuild_read_settings import SettingsReader
 
@@ -688,3 +689,167 @@ def test_free_threaded_override(tmp_path: Path):
     settings_reader = SettingsReader.from_file(pyproject_toml, state="wheel")
     settings = settings_reader.settings
     assert settings.wheel.cmake == bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
+
+
+@pytest.mark.parametrize("version", ["0.9", "0.10"])
+def test_skbuild_overrides_version(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, version: str
+):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_overrides, "__version__", version
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [tool.scikit-build]
+            wheel.cmake = false
+
+            [[tool.scikit-build.overrides]]
+            if.scikit-build-version = ">=0.10"
+            wheel.cmake = true
+            """
+        )
+    )
+
+    settings_reader = SettingsReader.from_file(pyproject_toml, state="wheel")
+    settings = settings_reader.settings
+    if version == "0.10":
+        assert settings.wheel.cmake
+    else:
+        assert not settings.wheel.cmake
+
+
+def test_skbuild_overrides_unmatched_version(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.scikit-build-version = "<0.10"
+            if.is-not-real = true
+            also-not-real = true
+            """
+        )
+    )
+
+    settings = SettingsReader.from_file(pyproject_toml)
+    settings.validate_may_exit()
+
+
+def test_skbuild_overrides_matched_version_if(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.scikit-build-version = ">=0.10"
+            if.is-not-real = true
+            """
+        )
+    )
+
+    with pytest.raises(TypeError, match="is_not_real"):
+        SettingsReader.from_file(pyproject_toml)
+
+
+def test_skbuild_overrides_matched_version_extra(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.scikit-build-version = ">=0.10"
+            not-real = true
+            """
+        )
+    )
+
+    settings = SettingsReader.from_file(pyproject_toml)
+    with pytest.raises(SystemExit):
+        settings.validate_may_exit()
+
+    assert "not-real" in capsys.readouterr().out
+
+
+def test_skbuild_overrides_matched_version_if_any(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_overrides, "__version__", "0.9"
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.any.scikit-build-version = ">=0.10"
+            if.any.not-real = true
+            also-not-real = true
+            """
+        )
+    )
+
+    settings = SettingsReader.from_file(pyproject_toml)
+    settings.validate_may_exit()
+
+
+def test_skbuild_overrides_matched_version_if_any_dual(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_overrides, "__version__", "0.9"
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.scikit-build-version = ">=0.10"
+            if.any.not-real = true
+            if.any.python-version = ">=3.7"
+            also-not-real = true
+            """
+        )
+    )
+
+    settings = SettingsReader.from_file(pyproject_toml)
+    settings.validate_may_exit()
+
+
+def test_skbuild_overrides_matched_version_if_any_match(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_overrides, "__version__", "0.10"
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.any.scikit-build-version = ">=0.10"
+            if.any.not-real = true
+            if.python-version = ">=3.7"
+            experimental = true
+            """
+        )
+    )
+
+    with pytest.raises(TypeError, match="not_real"):
+        SettingsReader.from_file(pyproject_toml)