diff --git a/docs/changelog.md b/docs/changelog.md index dc411bac..a2275d00 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 by @henryiii in #818 diff --git a/docs/overrides.md b/docs/overrides.md index d70b159b..1cac13b9 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 f9401d84..6cfaa493 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 6195abe6..83436ea8 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 3c73f619..53152108 100644 --- a/src/scikit_build_core/settings/skbuild_overrides.py +++ b/src/scikit_build_core/settings/skbuild_overrides.py @@ -10,6 +10,7 @@ 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 081f57c2..d5151f75 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.", diff --git a/tests/test_auto.py b/tests/test_auto.py index 3133a7ab..f4453e86 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 645fd705..39f484a0 100644 --- a/tests/test_settings_overrides.py +++ b/tests/test_settings_overrides.py @@ -7,6 +7,7 @@ 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)