Repository: scikit-build/scikit-build-core
Pull request #818 (2024-07-19 18:30:15)

feat: support multipart regex
First 3 lines of #765. Makes `"{value}"` configurable.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/818?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 `70.00000%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 83.20%. Comparing base [(`2b3e4d2`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/2b3e4d2a6114c42ed3838f7b19e17a8fc48e97f3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`79c9b00`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/79c9b001bac620b0fd3110ef89a722d55cd314ff?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/818?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/metadata/regex.py](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/818?src=pr&el=tree&filepath=src%2Fscikit_build_core%2Fmetadata%2Fregex.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL21ldGFkYXRhL3JlZ2V4LnB5) | 70.00% | [3 Missing :warning: ](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/818?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     #818      +/-   ##
==========================================
+ Coverage   83.18%   83.20%   +0.01%     
==========================================
  Files          73       73              
  Lines        4068     4072       +4     
==========================================
+ Hits         3384     3388       +4     
  Misses        684      684              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/818?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/configuration.md b/docs/configuration.md
index 982e44b..ed758f3 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -561,10 +561,23 @@ provider = "scikit_build_core.metadata.regex"
 input = "src/mypackage/__init__.py"
 ```
 
-You can set a custom regex with `regex=`; use `(?P<value>...)` to capture the
-value you want to use. By default when targeting version, you get a reasonable
-regex for python files,
+You can set a custom regex with `regex=`. By default when targeting version, you
+get a reasonable regex for python files,
 `'(?i)^(__version__|VERSION)(?: ?\: ?str)? *= *([\'"])v?(?P<value>.+?)\2'`.
+You can set `result` to a format string to process the matches; the default is
+`"{value}"`. A more complex example:
+
+```toml
+[tool.scikit-build.metadata.version]
+provider = "scikit_build_core.metadata.regex"
+input = "src/mypackage/version.hpp"
+regex = '''(?sx)
+\#define \w+ VERSION_MAJOR \w+ (?P<major>\d+) .*?
+\#define \w+ VERSION_MINOR \w+ (?P<minor>\d+) .*?
+\#define \w+ VERSION_PATCH \w+ (?P<patch>\d+)
+'''
+result = "{major}.{minor}.{patch}"
+```
 
 ```{versionadded} 0.5
 
diff --git a/docs/projects.md b/docs/projects.md
index e57078b..b0c42a0 100644
--- a/docs/projects.md
+++ b/docs/projects.md
@@ -53,3 +53,10 @@ In addition, most of the [RAPIDSAI](https://github.com/rapidsai) projects use sc
 * CuSpatial ([source](https://github.com/rapidsai/cuspatial/blob/HEAD/python/cuspatial/pyproject.toml))
 * RMM ([source](https://github.com/rapidsai/rmm/blob/HEAD/python/rmm/pyproject.toml))
 * Raft ([source](https://github.com/rapidsai/raft/blob/HEAD/python/pylibraft/pyproject.toml))
+
+The [Insight Toolkit (ITK)](https://docs.itk.org), the initial target project
+for scikit-build classic, has [transitioned to
+sckit-build-core](https://github.com/InsightSoftwareConsortium/ITKPythonPackage/blob/master/scripts/pyproject.toml.in).
+ITK currently provides one example of a production SWIG-based deployment. In
+addition, dozens of [ITK-based extension packages are configured with
+scikit-build-core](https://github.com/topics/itk-module).
diff --git a/src/scikit_build_core/metadata/regex.py b/src/scikit_build_core/metadata/regex.py
index cc10851..6963498 100644
--- a/src/scikit_build_core/metadata/regex.py
+++ b/src/scikit_build_core/metadata/regex.py
@@ -14,6 +14,9 @@ def __dir__() -> list[str]:
     return __all__
 
 
+KEYS = {"input", "regex", "result"}
+
+
 def dynamic_metadata(
     field: str,
     settings: Mapping[str, Any],
@@ -22,8 +25,8 @@ def dynamic_metadata(
     if field not in {"version", "description", "requires-python"}:
         msg = "Only string fields supported by this plugin"
         raise RuntimeError(msg)
-    if settings.keys() > {"input", "regex"}:
-        msg = "Only 'input' and 'regex' settings allowed by this plugin"
+    if settings.keys() > KEYS:
+        msg = f"Only {KEYS} settings allowed by this plugin"
         raise RuntimeError(msg)
     if "input" not in settings:
         msg = "Must contain the 'input' setting to perform a regex on"
@@ -31,15 +34,18 @@ def dynamic_metadata(
     if field != "version" and "regex" not in settings:
         msg = "Must contain the 'regex' setting if not getting version"
         raise RuntimeError(msg)
-    if not all(isinstance(x, str) for x in settings.values()):
-        msg = "Must set 'input' and/or 'regex' to strings"
-        raise RuntimeError(msg)
+    for key in KEYS:
+        if key in settings and not isinstance(settings[key], str):
+            msg = f"Setting {key!r} must be a string"
+            raise RuntimeError(msg)
 
     input_filename = settings["input"]
     regex = settings.get(
         "regex",
         r'(?i)^(__version__|VERSION)(?: ?\: ?str)? *= *([\'"])v?(?P<value>.+?)\2',
     )
+    result = settings.get("result", "{value}")
+    assert isinstance(result, str)
 
     with Path(input_filename).open(encoding="utf-8") as f:
         match = re.search(regex, f.read(), re.MULTILINE)
@@ -48,4 +54,4 @@ def dynamic_metadata(
         msg = f"Couldn't find {regex!r} in {input_filename}"
         raise RuntimeError(msg)
 
-    return match.group("value")
+    return result.format(*match.groups(), **match.groupdict())


Test patch:
diff --git a/tests/test_dynamic_metadata.py b/tests/test_dynamic_metadata.py
index e23c26a..c3bc9ab 100644
--- a/tests/test_dynamic_metadata.py
+++ b/tests/test_dynamic_metadata.py
@@ -4,6 +4,7 @@ import importlib
 import shutil
 import subprocess
 import sys
+import textwrap
 import types
 import zipfile
 from pathlib import Path
@@ -274,3 +275,36 @@ def test_regex_errors() -> None:
         regex.dynamic_metadata("version", {})
     with pytest.raises(RuntimeError, match="Only string fields supported"):
         regex.dynamic_metadata("author", {"input": "x", "regex": "x"})
+
+
+def test_multipart_regex(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    d = tmp_path / "test_multi_regex"
+    d.mkdir()
+    monkeypatch.chdir(d)
+
+    with Path("version.hpp").open("w") as f:
+        f.write(
+            textwrap.dedent(
+                """\
+            #define VERSION_MAJOR 1
+            // Comment
+            #define VERSION_MINOR 2
+            #define VERSION_PATCH 3dev1
+            """
+            )
+        )
+
+    version = regex.dynamic_metadata(
+        "version",
+        {
+            "input": "version.hpp",
+            "regex": r"""(?sx)
+            \#define \s+ VERSION_MAJOR \s+ (?P<major>\d+) .*?
+            \#define \s+ VERSION_MINOR \s+ (?P<minor>\d+) .*?
+            \#define \s+ VERSION_PATCH \s+ (?P<patch>\w+)
+            """,
+            "result": "{major}.{minor}.{patch}",
+        },
+    )
+
+    assert version == "1.2.3dev1"