Repository: scikit-build/scikit-build-core
Pull request #548 (2023-11-08 15:29:58)

feat: support if.any
Adds support for using `if.any` in overrides.



Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index 9322ca8..427172f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -601,6 +601,9 @@ if.sys-platform = "darwin"
 cmake.minimum-version = "3.18"
 ```
 
+If you use `if.any` instead of `if`, then the override is true if any one of the
+items in it are true.
+
 ## Full schema
 
 The full schema for the `tool.scikit-build` table is below:
diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json
index 1d2dcb4..7b0e28f 100644
--- a/src/scikit_build_core/resources/scikit-build.schema.json
+++ b/src/scikit_build_core/resources/scikit-build.schema.json
@@ -358,29 +358,23 @@
         "additionalProperties": false,
         "properties": {
           "if": {
-            "type": "object",
-            "minProperties": 1,
-            "additionalProperties": false,
-            "properties": {
-              "python-version": {
-                "type": "string"
-              },
-              "implementation-name": {
-                "type": "string"
-              },
-              "implementation-version": {
-                "type": "string"
-              },
-              "platform-system": {
-                "type": "string"
-              },
-              "platform-machine": {
-                "type": "string"
+            "anyOf": [
+              {
+                "$ref": "#/$defs/if_overrides"
               },
-              "platform-node": {
-                "type": "string"
+              {
+                "type": "object",
+                "properties": {
+                  "any": {
+                    "$ref": "#/$defs/if_overrides"
+                  }
+                },
+                "required": [
+                  "any"
+                ],
+                "additionalProperties": false
               }
-            }
+            ]
           },
           "cmake": {
             "$ref": "#/properties/cmake"
@@ -439,6 +433,31 @@
           "type": "string"
         }
       }
+    },
+    "if_overrides": {
+      "type": "object",
+      "minProperties": 1,
+      "additionalProperties": false,
+      "properties": {
+        "python-version": {
+          "type": "string"
+        },
+        "implementation-name": {
+          "type": "string"
+        },
+        "implementation-version": {
+          "type": "string"
+        },
+        "platform-system": {
+          "type": "string"
+        },
+        "platform-machine": {
+          "type": "string"
+        },
+        "platform-node": {
+          "type": "string"
+        }
+      }
     }
   }
 }
diff --git a/src/scikit_build_core/settings/skbuild_read_settings.py b/src/scikit_build_core/settings/skbuild_read_settings.py
index 871f15e..98c2b0e 100644
--- a/src/scikit_build_core/settings/skbuild_read_settings.py
+++ b/src/scikit_build_core/settings/skbuild_read_settings.py
@@ -47,6 +47,8 @@ def regex_match(value: str, match: str) -> str:
 
 
 def override_match(
+    *,
+    match_all: bool,
     python_version: str | None = None,
     implementation_name: str | None = None,
     implementation_version: str | None = None,
@@ -96,9 +98,14 @@ def override_match(
         msg = "At least one override must be provided"
         raise ValueError(msg)
 
-    matched = all(matches)
-    if matched:
-        logger.info("Overrides {}", " and ".join(matches))
+    if match_all:
+        matched = all(matches)
+        if matched:
+            logger.info("Overrides {}", " and ".join(matches))
+    else:
+        matched = any(matches)
+        if matched:
+            logger.info("Overrides {}", " or ".join([m for m in matches if m]))
     return matched
 
 
@@ -115,8 +122,22 @@ class SettingsReader:
         # Process overrides into the main dictionary if they match
         tool_skb = pyproject.get("tool", {}).get("scikit-build", {})
         for override in tool_skb.pop("overrides", []):
-            select = {k.replace("-", "_"): v for k, v in override.pop("if").items()}
-            if override_match(**select):
+            matched = True
+            if_override = override.pop("if", None)
+            if not if_override:
+                msg = "At least one 'if' override must be provided"
+                raise KeyError(msg)
+            if not isinstance(if_override, dict):
+                msg = "'if' override must be a table"
+                raise TypeError(msg)
+            if "any" in if_override:
+                any_override = if_override.pop("any")
+                select = {k.replace("-", "_"): v for k, v in any_override.items()}
+                matched = override_match(match_all=False, **select)
+            select = {k.replace("-", "_"): v for k, v in if_override.items()}
+            if select:
+                matched = matched and override_match(match_all=True, **select)
+            if matched:
                 for key, value in override.items():
                     if isinstance(value, dict):
                         for key2, value2 in value.items():
diff --git a/src/scikit_build_core/settings/skbuild_schema.py b/src/scikit_build_core/settings/skbuild_schema.py
index 7ef1f60..32ee87e 100644
--- a/src/scikit_build_core/settings/skbuild_schema.py
+++ b/src/scikit_build_core/settings/skbuild_schema.py
@@ -83,6 +83,19 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
     }
 
     props = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
+    schema["$defs"]["if_overrides"] = {
+        "type": "object",
+        "minProperties": 1,
+        "additionalProperties": False,
+        "properties": {
+            "python-version": {"type": "string"},
+            "implementation-name": {"type": "string"},
+            "implementation-version": {"type": "string"},
+            "platform-system": {"type": "string"},
+            "platform-machine": {"type": "string"},
+            "platform-node": {"type": "string"},
+        },
+    }
     schema["properties"]["overrides"] = {
         "type": "array",
         "items": {
@@ -92,17 +105,15 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
             "additionalProperties": False,
             "properties": {
                 "if": {
-                    "type": "object",
-                    "minProperties": 1,
-                    "additionalProperties": False,
-                    "properties": {
-                        "python-version": {"type": "string"},
-                        "implementation-name": {"type": "string"},
-                        "implementation-version": {"type": "string"},
-                        "platform-system": {"type": "string"},
-                        "platform-machine": {"type": "string"},
-                        "platform-node": {"type": "string"},
-                    },
+                    "anyOf": [
+                        {"$ref": "#/$defs/if_overrides"},
+                        {
+                            "type": "object",
+                            "properties": {"any": {"$ref": "#/$defs/if_overrides"}},
+                            "required": ["any"],
+                            "additionalProperties": False,
+                        },
+                    ]
                 },
                 **props,
             },


Test patch:
diff --git a/tests/test_settings_overrides.py b/tests/test_settings_overrides.py
index bd42a51..3941d9a 100644
--- a/tests/test_settings_overrides.py
+++ b/tests/test_settings_overrides.py
@@ -86,6 +86,96 @@ def test_skbuild_overrides_dual(
         assert not settings.install.components
 
 
+@pytest.mark.parametrize("implementation_name", ["cpython", "pypy"])
+@pytest.mark.parametrize("platform_system", ["darwin", "linux"])
+def test_skbuild_overrides_any(
+    implementation_name: str,
+    platform_system: str,
+    tmp_path: Path,
+    monkeypatch: pytest.MonkeyPatch,
+):
+    monkeypatch.setattr(
+        "sys.implementation", type("Mock", (), {"name": implementation_name})
+    )
+    monkeypatch.setattr("sys.platform", platform_system)
+
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.any = {implementation-name = "pypy", platform-system = "darwin"}
+            editable.verbose = false
+            install.components = ["headers"]
+
+            [[tool.scikit-build.overrides]]
+            if.any.implementation-name = "cpython"
+            if.any.platform-system = "darwin"
+            install.components = ["bindings"]
+            """
+        ),
+        encoding="utf-8",
+    )
+
+    settings_reader = SettingsReader.from_file(pyproject_toml, {})
+    settings = settings_reader.settings
+
+    if implementation_name == "cpython" or platform_system == "darwin":
+        assert settings.editable.verbose == (
+            platform_system == "linux" and implementation_name == "cpython"
+        )
+        assert settings.install.components == ["bindings"]
+    elif implementation_name == "pypy" or platform_system == "darwin":
+        assert not settings.editable.verbose
+        assert settings.install.components == ["headers"]
+    else:
+        assert settings.editable.verbose
+        assert not settings.install.components
+
+
+@pytest.mark.parametrize("python_version", ["3.9", "3.10"])
+@pytest.mark.parametrize("implementation_name", ["cpython", "pypy"])
+@pytest.mark.parametrize("platform_system", ["darwin", "linux"])
+def test_skbuild_overrides_any_mixed(
+    implementation_name: str,
+    platform_system: str,
+    python_version: str,
+    tmp_path: Path,
+    monkeypatch: pytest.MonkeyPatch,
+):
+    monkeypatch.setattr(
+        "sys.implementation", type("Mock", (), {"name": implementation_name})
+    )
+    monkeypatch.setattr("sys.platform", platform_system)
+    monkeypatch.setattr("sys.version_info", (*map(int, python_version.split(".")), 0))
+
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [[tool.scikit-build.overrides]]
+            if.any = {implementation-name = "pypy", platform-system = "darwin"}
+            if.python-version = ">=3.10"
+            editable.verbose = false
+            install.components = ["headers"]
+            """
+        ),
+        encoding="utf-8",
+    )
+
+    settings_reader = SettingsReader.from_file(pyproject_toml, {})
+    settings = settings_reader.settings
+
+    if python_version == "3.10" and (
+        implementation_name == "pypy" or platform_system == "darwin"
+    ):
+        assert not settings.editable.verbose
+        assert settings.install.components == ["headers"]
+    else:
+        assert settings.editable.verbose
+        assert not settings.install.components
+
+
 @pytest.mark.parametrize("platform_node", ["thismatch", "matchthat"])
 def test_skbuild_overrides_platnode(
     platform_node: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@@ -164,7 +254,7 @@ def test_skbuild_overrides_no_if(
         encoding="utf-8",
     )
 
-    with pytest.raises(KeyError):
+    with pytest.raises(KeyError, match="At least one 'if' override must be provided"):
         SettingsReader.from_file(pyproject_toml, {})
 
 
@@ -183,7 +273,7 @@ def test_skbuild_overrides_empty_if(
         encoding="utf-8",
     )
 
-    with pytest.raises(ValueError, match="At least one override must be provided"):
+    with pytest.raises(KeyError, match="At least one 'if' override must be provided"):
         SettingsReader.from_file(pyproject_toml, {})