Repository: scikit-build/scikit-build-core
Pull request #798 (2024-07-06 13:30:31)

feat: auto min version
First part of #777.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/798?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
Attention: Patch coverage is `87.87879%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 82.05%. Comparing base [(`3287d2c`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/3287d2c0ede18902e0bf459932c05db4433677e7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`bfdd3df`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/bfdd3dfdb1d89ab0d99354b3056a1feba17ea029?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

| [Files](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/798?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) | Patch % | Lines |
|---|---|---|
| [src/scikit\_build\_core/settings/auto\_requires.py](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/798?src=pr&el=tree&filepath=src%2Fscikit_build_core%2Fsettings%2Fauto_requires.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL3NldHRpbmdzL2F1dG9fcmVxdWlyZXMucHk=) | 91.66% | [2 Missing :warning: ](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/798?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) |
| [...cikit\_build\_core/settings/skbuild\_read\_settings.py](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/798?src=pr&el=tree&filepath=src%2Fscikit_build_core%2Fsettings%2Fskbuild_read_settings.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL3NldHRpbmdzL3NrYnVpbGRfcmVhZF9zZXR0aW5ncy5weQ==) | 77.77% | [2 Missing :warning: ](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/798?src=pr&el=tree&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     #798      +/-   ##
==========================================
+ Coverage   82.01%   82.05%   +0.04%     
==========================================
  Files          68       69       +1     
  Lines        3958     3991      +33     
==========================================
+ Hits         3246     3275      +29     
- Misses        712      716       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/798?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/docs/api/scikit_build_core.settings.rst b/docs/api/scikit_build_core.settings.rst
index 0d73fc3..3277a0e 100644
--- a/docs/api/scikit_build_core.settings.rst
+++ b/docs/api/scikit_build_core.settings.rst
@@ -9,6 +9,14 @@ scikit\_build\_core.settings package
 Submodules
 ----------
 
+scikit\_build\_core.settings.auto\_requires module
+--------------------------------------------------
+
+.. automodule:: scikit_build_core.settings.auto_requires
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 scikit\_build\_core.settings.documentation module
 -------------------------------------------------
 
diff --git a/docs/configuration.md b/docs/configuration.md
index 90aefa8..67d3574 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -101,6 +101,19 @@ probably keep in sync with your build-system requirements.
 minimum-version = "0.2"
 ```
 
+In your `pyproject.toml`, you can specify the special string
+`"build-system.requires"`, which will read the minimum version from your
+build-system requirements directly; you must specify a minimum there to use this
+automatic feature.
+
+```toml
+[build-system]
+requires = ["scikit-build-core>=0.10"]
+
+[tool.scikit-build]
+minimum-version = "build-system.requires"
+```
+
 :::{warning}
 
 The following behaviors are affected by `minimum-version`:
@@ -110,6 +123,8 @@ The following behaviors are affected by `minimum-version`:
 - `minimum-version` 0.5+ (or unset) strips binaries by default.
 - `minimum-version` 0.8+ (or unset) `cmake.minimum-version` and
   `ninja.minimum-version` are replaced with `cmake.version` and `ninja.version`.
+- `minimum-version` 0.10+ (or unset) `cmake.targets` and
+  `cmake.verbose` are replaced with `build.targets` and `build.verbose`.
 
 :::
 
diff --git a/pyproject.toml b/pyproject.toml
index 0fadfa4..1084561 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -203,6 +203,8 @@ report.exclude_lines = [
     'pragma: no cover',
     '\.\.\.',
     'if typing.TYPE_CHECKING:',
+    'if TYPE_CHECKING:',
+    'def __repr__',
 ]
 
 
diff --git a/src/scikit_build_core/settings/auto_requires.py b/src/scikit_build_core/settings/auto_requires.py
new file mode 100644
index 0000000..6195abe
--- /dev/null
+++ b/src/scikit_build_core/settings/auto_requires.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from packaging.requirements import Requirement
+from packaging.utils import canonicalize_name
+from packaging.version import Version
+
+if TYPE_CHECKING:
+    from collections.abc import Iterable
+
+    from packaging.specifiers import Specifier
+
+__all__ = ["get_min_requires"]
+
+
+def __dir__() -> list[str]:
+    return __all__
+
+
+def get_min_requires(package: str, reqlist: Iterable[str]) -> Version | None:
+    """
+    Read the build requirements from a pyproject.toml file
+    and return the minimum version required for the given package.
+    Returns None if no minimum version is discovered.
+    """
+
+    norm_package = canonicalize_name(package)
+
+    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
+
+
+def min_from_spec(spec: Specifier) -> Version | None:
+    """
+    Return the minimum version from a specifier.
+
+    Returns None if no minimum version is discovered.
+    ">" technically doesn't include the minimum, but any
+    larger version is acceptable, so it is treated as the
+    minimum.
+    """
+
+    if spec.operator in {">=", ">", "==", "~="}:
+        return Version(spec.version)
+    return None
diff --git a/src/scikit_build_core/settings/skbuild_read_settings.py b/src/scikit_build_core/settings/skbuild_read_settings.py
index 6ca0013..55115d1 100644
--- a/src/scikit_build_core/settings/skbuild_read_settings.py
+++ b/src/scikit_build_core/settings/skbuild_read_settings.py
@@ -17,6 +17,7 @@ from .. import __version__
 from .._compat import tomllib
 from .._logging import logger, rich_print
 from ..errors import CMakeConfigError
+from .auto_requires import get_min_requires
 from .skbuild_model import CMakeSettings, NinjaSettings, ScikitBuildSettings
 from .sources import ConfSource, EnvSource, SourceChain, TOMLSource
 
@@ -327,8 +328,25 @@ class SettingsReader:
     ) -> None:
         self.state = state
 
+        # Handle overrides
         pyproject = copy.deepcopy(pyproject)
         process_overides(pyproject.get("tool", {}).get("scikit-build", {}), state, env)
+
+        # Support for minimum-version='build-system.requires'
+        tmp_min_v = (
+            pyproject.get("tool", {})
+            .get("scikit-build", {})
+            .get("minimum-version", None)
+        )
+        if tmp_min_v == "build-system.requires":
+            reqlist = pyproject["build-system"]["requires"]
+            min_v = get_min_requires("scikit-build-core", reqlist)
+            if min_v is None:
+                rich_print(
+                    "[red][bold]ERROR:[/bold] scikit-build-core needs a min version in build-system.requires to use minimum-version='build-system.requires'"
+                )
+                raise SystemExit(7)
+            pyproject["tool"]["scikit-build"]["minimum-version"] = str(min_v)
         toml_srcs = [TOMLSource("tool", "scikit-build", settings=pyproject)]
 
         if extra_settings is not None:


Test patch:
diff --git a/tests/test_auto.py b/tests/test_auto.py
new file mode 100644
index 0000000..d3b3abb
--- /dev/null
+++ b/tests/test_auto.py
@@ -0,0 +1,27 @@
+import pytest
+from packaging.version import Version
+
+from scikit_build_core.settings.auto_requires import get_min_requires
+
+
+def test_auto_requires_pkg_no_spec():
+    reqlist = ["scikit-build-core"]
+
+    assert get_min_requires("scikit-build-core", reqlist) is None
+    assert get_min_requires("other", reqlist) is None
+
+
+@pytest.mark.parametrize(
+    ("spec", "version"),
+    [
+        ("==1.0", Version("1.0")),
+        (">=1.0", Version("1.0")),
+        (">1.0", Version("1.0")),
+        ("~=1.0", Version("1.0")),
+        (">=0.3,<0.4", Version("0.3")),
+        ("", None),
+    ],
+)
+def test_auto_requires_pkg_version(spec: str, version: Version):
+    reqlist = [f"scikit_build_core{spec}"]
+    assert get_min_requires("scikit-build-core", reqlist) == version
diff --git a/tests/test_skbuild_settings.py b/tests/test_skbuild_settings.py
index 5cb9025..4fc8be4 100644
--- a/tests/test_skbuild_settings.py
+++ b/tests/test_skbuild_settings.py
@@ -577,3 +577,25 @@ def test_backcompat_cmake_build_both_specified(
 
     with pytest.raises(SystemExit):
         SettingsReader.from_file(pyproject_toml, {"build.verbose": "1"})
+
+
+def test_auto_minimum_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_read_settings, "__version__", "0.10.0"
+    )
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        textwrap.dedent(
+            """\
+            [build-system]
+            requires = ["scikit-build-core>=0.8"]
+
+            [tool.scikit-build]
+            minimum-version = "build-system.requires"
+            """
+        ),
+        encoding="utf-8",
+    )
+
+    reader = SettingsReader.from_file(pyproject_toml, {})
+    assert reader.settings.minimum_version == Version("0.8")