Repository: scikit-build/scikit-build-core
Pull request #600 (2024-01-02 22:32:14)

feat: add if.state to overrides
This is the easier of the two suggestions in #591.



Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index 8f06dfe..957dbe1 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -625,7 +625,7 @@ experimental = true
 
 Scikit-build-core has an override system, similar to cibuildwheel and mypy. You
 specify a `tool.scikit-build.overrides` array with an `if` key. That if key can
-take several values, based on [PEP 508][]:
+take several values, including several based on [PEP 508][]:
 
 - `python-version`: The two-digit Python version. Takes a specifier set.
 - `platform-system`: The value of `sys.platform`. Takes a regex.
@@ -637,6 +637,8 @@ take several values, based on [PEP 508][]:
 - `env`: A table of environment variables mapped to either string regexs, or
   booleans. Valid "truthy" environment variables are case insensitive `true`,
   `on`, `yes`, `y`, `t`, or a number more than 0.
+- `state`: The state of the build, one of `sdist`, `wheel`, `editable`,
+  `metadata_wheel`, and `metadata_editable`. Takes a regex.
 
 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
diff --git a/src/scikit_build_core/build/sdist.py b/src/scikit_build_core/build/sdist.py
index 9813a7f..eadd73d 100644
--- a/src/scikit_build_core/build/sdist.py
+++ b/src/scikit_build_core/build/sdist.py
@@ -100,7 +100,7 @@ def build_sdist(
     with Path("pyproject.toml").open("rb") as f:
         pyproject = tomllib.load(f)
 
-    settings_reader = SettingsReader(pyproject, config_settings or {})
+    settings_reader = SettingsReader(pyproject, config_settings or {}, state="sdist")
     settings = settings_reader.settings
     setup_logging(settings.logging.level)
 
diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py
index 5b4cf34..4ef5e7a 100644
--- a/src/scikit_build_core/build/wheel.py
+++ b/src/scikit_build_core/build/wheel.py
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING
 
 from .. import __version__
 from .._compat import tomllib
-from .._compat.typing import assert_never
+from .._compat.typing import Literal, assert_never
 from .._logging import logger, rich_print
 from .._shutil import fix_win_37_all_permissions
 from ..builder.builder import Builder, archs_to_tags, get_archs
@@ -121,11 +121,19 @@ def _build_wheel_impl(
     """
     Build a wheel or just prepare metadata (if wheel dir is None). Can be editable.
     """
+    state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"]
+    if exit_after_config:
+        state = "sdist"
+    elif wheel_directory is None:
+        state = "metadata_editable" if editable else "metadata_wheel"
+    else:
+        state = "editable" if editable else "wheel"
+
     pyproject_path = Path("pyproject.toml")
     with pyproject_path.open("rb") as ft:
         pyproject = tomllib.load(ft)
 
-    settings_reader = SettingsReader(pyproject, config_settings or {})
+    settings_reader = SettingsReader(pyproject, config_settings or {}, state=state)
     settings = settings_reader.settings
     setup_logging(settings.logging.level)
 
@@ -139,12 +147,6 @@ def _build_wheel_impl(
 
     normalized_name = metadata.name.replace("-", "_").replace(".", "_")
 
-    state = "editable" if editable else "wheel"
-    if wheel_directory is None:
-        state = f"metadata_{state}"
-    if exit_after_config:
-        state = "sdist"
-
     if settings.wheel.cmake:
         cmake = CMake.default_search(minimum_version=settings.cmake.minimum_version)
         cmake_msg = [f"using [blue]CMake {cmake.version}[/blue]"]
@@ -213,10 +215,16 @@ def _build_wheel_impl(
         else:
             install_dir = wheel_dirs[targetlib] / settings.wheel.install_dir
 
+        # Include the metadata license.file entry if provided
+        license_file_globs = list(settings.wheel.license_files)
+        if metadata.license and metadata.license.file:
+            license_file_globs.append(str(metadata.license.file))
+
         license_files = {
             x: x.read_bytes()
-            for y in settings.wheel.license_files
+            for y in license_file_globs
             for x in Path().glob(y)
+            if x.is_file()
         }
         if settings.wheel.license_files and not license_files:
             logger.warning(
diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json
index bf00708..568efa8 100644
--- a/src/scikit_build_core/resources/scikit-build.schema.json
+++ b/src/scikit_build_core/resources/scikit-build.schema.json
@@ -366,6 +366,7 @@
     },
     "overrides": {
       "type": "array",
+      "description": "A list of overrides to apply to the settings, based on the `if` selector.",
       "items": {
         "type": "object",
         "required": [
@@ -457,22 +458,32 @@
       "additionalProperties": false,
       "properties": {
         "python-version": {
-          "type": "string"
+          "type": "string",
+          "description": "The two-digit Python version. Takes a specifier set."
         },
         "implementation-name": {
-          "type": "string"
+          "type": "string",
+          "description": "The value of `sys.implementation.name`. Takes a regex"
         },
         "implementation-version": {
-          "type": "string"
+          "type": "string",
+          "description": "Derived from `sys.implementation.version`, following PEP 508. Takes a specifier set."
         },
         "platform-system": {
-          "type": "string"
+          "type": "string",
+          "description": "The value of `sys.platform`. Takes a regex."
         },
         "platform-machine": {
-          "type": "string"
+          "type": "string",
+          "description": "The value of `platform.machine()`. Takes a regex."
         },
         "platform-node": {
-          "type": "string"
+          "type": "string",
+          "description": "The value of `platform.node()`. Takes a regex."
+        },
+        "state": {
+          "type": "string",
+          "description": "The state of the build, one of `sdist`, `wheel`, `editable`, `metadata_wheel`, and `metadata_editable`. Takes a regex."
         },
         "env": {
           "type": "object",
@@ -489,7 +500,8 @@
             }
           },
           "additionalProperties": false,
-          "minProperties": 1
+          "minProperties": 1,
+          "description": "A table of environment variables mapped to either string regexs, or booleans. Valid 'truthy' environment variables are case insensitive `true`, `on`, `yes`, `y`, `t`, or a number more than 0."
         }
       }
     }
diff --git a/src/scikit_build_core/settings/skbuild_read_settings.py b/src/scikit_build_core/settings/skbuild_read_settings.py
index 810e64b..3b6e466 100644
--- a/src/scikit_build_core/settings/skbuild_read_settings.py
+++ b/src/scikit_build_core/settings/skbuild_read_settings.py
@@ -22,6 +22,8 @@ from .sources import ConfSource, EnvSource, SourceChain, TOMLSource
 if TYPE_CHECKING:
     from collections.abc import Generator, Mapping
 
+    from .._compat.typing import Literal
+
 __all__ = ["SettingsReader"]
 
 
@@ -60,6 +62,9 @@ def override_match(
     *,
     match_all: bool,
     current_env: Mapping[str, str] | None,
+    current_state: Literal[
+        "sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"
+    ],
     python_version: str | None = None,
     implementation_name: str | None = None,
     implementation_version: str | None = None,
@@ -67,6 +72,7 @@ def override_match(
     platform_machine: str | None = None,
     platform_node: str | None = None,
     env: dict[str, str] | None = None,
+    state: str | None = None,
 ) -> bool:
     matches = []
     if current_env is None:
@@ -108,6 +114,10 @@ def override_match(
         match_msg = regex_match(current_platform_node, platform_node)
         matches.append(match_msg)
 
+    if state is not None:
+        match_msg = regex_match(current_state, state)
+        matches.append(match_msg)
+
     if env:
         for key, value in env.items():
             if isinstance(value, bool):
@@ -144,6 +154,9 @@ class SettingsReader:
         pyproject: dict[str, Any],
         config_settings: Mapping[str, str | list[str]],
         *,
+        state: Literal[
+            "sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"
+        ],
         verify_conf: bool = True,
         env: Mapping[str, str] | None = None,
     ) -> None:
@@ -163,11 +176,13 @@ class SettingsReader:
             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, current_env=env, **select)
+                matched = override_match(
+                    match_all=False, current_env=env, current_state=state, **select
+                )
             select = {k.replace("-", "_"): v for k, v in if_override.items()}
             if select:
                 matched = matched and override_match(
-                    match_all=True, current_env=env, **select
+                    match_all=True, current_env=env, current_state=state, **select
                 )
             if matched:
                 for key, value in override.items():
@@ -301,9 +316,14 @@ class SettingsReader:
         pyproject_path: os.PathLike[str] | str,
         config_settings: Mapping[str, str | list[str]] | None,
         *,
+        state: Literal[
+            "sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"
+        ] = "sdist",
         verify_conf: bool = True,
     ) -> SettingsReader:
         with Path(pyproject_path).open("rb") as f:
             pyproject = tomllib.load(f)
 
-        return cls(pyproject, config_settings or {}, verify_conf=verify_conf)
+        return cls(
+            pyproject, config_settings or {}, verify_conf=verify_conf, state=state
+        )
diff --git a/src/scikit_build_core/settings/skbuild_schema.py b/src/scikit_build_core/settings/skbuild_schema.py
index b388c31..53d67eb 100644
--- a/src/scikit_build_core/settings/skbuild_schema.py
+++ b/src/scikit_build_core/settings/skbuild_schema.py
@@ -88,12 +88,34 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
         "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"},
+            "python-version": {
+                "type": "string",
+                "description": "The two-digit Python version. Takes a specifier set.",
+            },
+            "implementation-name": {
+                "type": "string",
+                "description": "The value of `sys.implementation.name`. Takes a regex",
+            },
+            "implementation-version": {
+                "type": "string",
+                "description": "Derived from `sys.implementation.version`, following PEP 508. Takes a specifier set.",
+            },
+            "platform-system": {
+                "type": "string",
+                "description": "The value of `sys.platform`. Takes a regex.",
+            },
+            "platform-machine": {
+                "type": "string",
+                "description": "The value of `platform.machine()`. Takes a regex.",
+            },
+            "platform-node": {
+                "type": "string",
+                "description": "The value of `platform.node()`. Takes a regex.",
+            },
+            "state": {
+                "type": "string",
+                "description": "The state of the build, one of `sdist`, `wheel`, `editable`, `metadata_wheel`, and `metadata_editable`. Takes a regex.",
+            },
             "env": {
                 "type": "object",
                 "patternProperties": {
@@ -101,11 +123,13 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
                 },
                 "additionalProperties": False,
                 "minProperties": 1,
+                "description": "A table of environment variables mapped to either string regexs, or booleans. Valid 'truthy' environment variables are case insensitive `true`, `on`, `yes`, `y`, `t`, or a number more than 0.",
             },
         },
     }
     schema["properties"]["overrides"] = {
         "type": "array",
+        "description": "A list of overrides to apply to the settings, based on the `if` selector.",
         "items": {
             "type": "object",
             "required": ["if"],


Test patch:
diff --git a/tests/test_dynamic_metadata.py b/tests/test_dynamic_metadata.py
index 1359a30..99a3efc 100644
--- a/tests/test_dynamic_metadata.py
+++ b/tests/test_dynamic_metadata.py
@@ -97,7 +97,7 @@ def mock_entry_points(monkeypatch):
 def test_dynamic_metadata():
     with Path("pyproject.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {})
+    settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")
     settings = settings_reader.settings
 
     settings_reader.validate_may_exit()
@@ -131,7 +131,7 @@ def test_plugin_metadata():
 
     with Path("pyproject.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {})
+    settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")
     settings = settings_reader.settings
 
     settings_reader.validate_may_exit()
@@ -153,7 +153,7 @@ def test_plugin_metadata():
 def test_faulty_metadata():
     with Path("faulty_project.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {})
+    settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")
     settings = settings_reader.settings
 
     settings_reader.validate_may_exit()
@@ -166,7 +166,7 @@ def test_faulty_metadata():
 def test_local_plugin_metadata():
     with Path("local_pyproject.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {})
+    settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")
     settings = settings_reader.settings
 
     settings_reader.validate_may_exit()
@@ -179,7 +179,7 @@ def test_local_plugin_metadata():
 def test_warn_metadata():
     with Path("warn_project.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {})
+    settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")
     settings = settings_reader.settings
 
     settings_reader.validate_may_exit()
@@ -192,7 +192,9 @@ def test_warn_metadata():
 def test_fail_experimental_metadata():
     with Path("warn_project.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {"experimental": "false"})
+    settings_reader = SettingsReader(
+        pyproject, {"experimental": "false"}, state="metadata_wheel"
+    )
 
     with pytest.raises(SystemExit) as exc:
         settings_reader.validate_may_exit()
@@ -204,7 +206,7 @@ def test_fail_experimental_metadata():
 def test_dual_metadata():
     with Path("dual_project.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {})
+    settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")
     settings = settings_reader.settings
 
     settings_reader.validate_may_exit()
@@ -215,7 +217,7 @@ def test_dual_metadata():
 
     with Path("faulty_dual_project.toml").open("rb") as ft:
         pyproject = tomllib.load(ft)
-    settings_reader = SettingsReader(pyproject, {})
+    settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel")
     settings = settings_reader.settings
 
     settings_reader.validate_may_exit()
diff --git a/tests/test_settings_overrides.py b/tests/test_settings_overrides.py
index 53988ee..36e133c 100644
--- a/tests/test_settings_overrides.py
+++ b/tests/test_settings_overrides.py
@@ -402,3 +402,26 @@ def test_skbuild_env_bool_all_any(
         assert settings.sdist.cmake
     else:
         assert not settings.sdist.cmake
+
+
+@pytest.mark.parametrize("state", ["wheel", "sdist"])
+def test_skbuild_overrides_state(state: str, tmp_path: Path):
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            f"""\
+            [[tool.scikit-build.overrides]]
+            if.state = "{state}"
+            experimental = true
+            """
+        ),
+        encoding="utf-8",
+    )
+
+    settings_reader = SettingsReader.from_file(pyproject_toml, {}, state="wheel")
+    settings = settings_reader.settings
+
+    if state == "wheel":
+        assert settings.experimental
+    else:
+        assert not settings.experimental