Skip to content

Commit

Permalink
feat: if.scikit-build-version (#851)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
henryiii authored Aug 6, 2024
1 parent 873a321 commit 01342be
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 16 deletions.
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/scikit_build_core/resources/scikit-build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
15 changes: 8 additions & 7 deletions src/scikit_build_core/settings/auto_requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
50 changes: 41 additions & 9 deletions src/scikit_build_core/settings/skbuild_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -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(),
Expand Down
4 changes: 4 additions & 0 deletions src/scikit_build_core/settings/skbuild_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
9 changes: 9 additions & 0 deletions tests/test_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
[
Expand Down
165 changes: 165 additions & 0 deletions tests/test_settings_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

0 comments on commit 01342be

Please sign in to comment.