Skip to content

Commit b96eac9

Browse files
authored
feat: regex and dynamic-metadata rewrite (#457)
Based on scikit-build/dynamic-metadata#1 (with some updates). Fixes #435. This doesn't start using `tool.dynamic-metadata` yet, to give us room to change that. Plus we have to back-compat support `tool.scikit-build.metadata`, so might as well keep that as the only way to set this for now. Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
1 parent 73359d7 commit b96eac9

File tree

12 files changed

+208
-71
lines changed

12 files changed

+208
-71
lines changed

docs/api/scikit_build_core.metadata.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ scikit\_build\_core.metadata.fancy\_pypi\_readme module
1717
:undoc-members:
1818
:show-inheritance:
1919

20+
scikit\_build\_core.metadata.regex module
21+
-----------------------------------------
22+
23+
.. automodule:: scikit_build_core.metadata.regex
24+
:members:
25+
:undoc-members:
26+
:show-inheritance:
27+
2028
scikit\_build\_core.metadata.setuptools\_scm module
2129
---------------------------------------------------
2230

docs/configuration.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,31 @@ metadata.readme.provider = "scikit_build_core.metadata.fancy_pypi_readme"
445445

446446
:::
447447

448+
:::{tab} Regex
449+
450+
If you want to pull a string-valued expression (usually version) from an
451+
existing file, you can the integrated `regex` plugin to pull the information.
452+
453+
```toml
454+
name = "mypackage"
455+
dynamic = ["version"]
456+
457+
[tool.scikit-build.metadata.version]
458+
provider = "scikit_build_core.metadata.regex"
459+
input = "src/mypackage/__init__.py"
460+
```
461+
462+
You can set a custom regex with `regex=`; use `(?p<value>...)` to capture the
463+
value you want to use. By default when targeting version, you get a reasonable
464+
regex for python files,
465+
`'(?i)^(__version__|VERSION) *= *([\'"])v?(?P<value>.+?)\2'`.
466+
467+
```{versionadded} 0.5
468+
469+
```
470+
471+
:::
472+
448473
## Editable installs
449474

450475
Experimental support for editable installs is provided, with some caveats and

src/scikit_build_core/metadata/fancy_pypi_readme.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@
44

55
from .._compat import tomllib
66

7-
__all__ = ["dynamic_metadata"]
7+
__all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"]
88

99

1010
def __dir__() -> list[str]:
1111
return __all__
1212

1313

1414
def dynamic_metadata(
15-
fields: frozenset[str],
15+
field: str,
1616
settings: dict[str, list[str] | str] | None = None,
17-
) -> dict[str, str | dict[str, str | None]]:
17+
) -> str | dict[str, str]:
1818
from hatch_fancy_pypi_readme._builder import build_text
1919
from hatch_fancy_pypi_readme._config import load_and_validate_config
2020

21-
if fields != {"readme"}:
21+
if field != "readme":
2222
msg = "Only the 'readme' field is supported"
2323
raise ValueError(msg)
2424

@@ -35,13 +35,11 @@ def dynamic_metadata(
3535

3636
# Version 22.3 does not have fragment support
3737
return {
38-
"readme": {
39-
"content-type": config.content_type,
40-
"text": build_text(config.fragments, config.substitutions)
41-
if hasattr(config, "substitutions")
42-
# pylint: disable-next=no-value-for-parameter
43-
else build_text(config.fragments), # type: ignore[call-arg]
44-
}
38+
"content-type": config.content_type,
39+
"text": build_text(config.fragments, config.substitutions)
40+
if hasattr(config, "substitutions")
41+
# pylint: disable-next=no-value-for-parameter
42+
else build_text(config.fragments), # type: ignore[call-arg]
4543
}
4644

4745

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from collections.abc import Mapping
5+
from pathlib import Path
6+
from typing import Any
7+
8+
__all__ = ["dynamic_metadata"]
9+
10+
11+
def __dir__() -> list[str]:
12+
return __all__
13+
14+
15+
def dynamic_metadata(
16+
field: str,
17+
settings: Mapping[str, Any],
18+
) -> str:
19+
# Input validation
20+
if field not in {"version", "description", "requires-python"}:
21+
msg = "Only string fields supported by this plugin"
22+
raise RuntimeError(msg)
23+
if settings.keys() > {"input", "regex"}:
24+
msg = "Only 'input' and 'regex' settings allowed by this plugin"
25+
raise RuntimeError(msg)
26+
if "input" not in settings:
27+
msg = "Must contain the 'input' setting to perform a regex on"
28+
raise RuntimeError(msg)
29+
if field != "version" and "regex" not in settings:
30+
msg = "Must contain the 'regex' setting if not getting version"
31+
raise RuntimeError(msg)
32+
if not all(isinstance(x, str) for x in settings.values()):
33+
msg = "Must set 'input' and/or 'regex' to strings"
34+
raise RuntimeError(msg)
35+
36+
input_filename = settings["input"]
37+
regex = settings.get(
38+
"regex", r'(?i)^(__version__|VERSION) *= *([\'"])v?(?P<value>.+?)\2'
39+
)
40+
41+
with Path(input_filename).open(encoding="utf-8") as f:
42+
match = re.search(regex, f.read(), re.MULTILINE)
43+
44+
if not match:
45+
msg = f"Couldn't find {regex!r} in {input_filename}"
46+
raise RuntimeError(msg)
47+
48+
return match.group("value")

src/scikit_build_core/metadata/setuptools_scm.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
from __future__ import annotations
22

3-
__all__ = ["dynamic_metadata"]
3+
__all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"]
44

55

66
def __dir__() -> list[str]:
77
return __all__
88

99

1010
def dynamic_metadata(
11-
fields: frozenset[str],
11+
field: str,
1212
settings: dict[str, object] | None = None,
13-
) -> dict[str, str | dict[str, str | None]]:
13+
) -> str:
1414
# this is a classic implementation, waiting for the release of
1515
# vcs-versioning and an improved public interface
1616

17-
if fields != {"version"}:
17+
if field != "version":
1818
msg = "Only the 'version' field is supported"
1919
raise ValueError(msg)
2020

@@ -27,7 +27,7 @@ def dynamic_metadata(
2727
config = Configuration.from_file("pyproject.toml")
2828
version: str = _get_version(config)
2929

30-
return {"version": version}
30+
return version
3131

3232

3333
def get_requires_for_dynamic_metadata(

src/scikit_build_core/settings/_load_provider.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
import importlib
44
import sys
5-
from collections.abc import Iterable
5+
from collections.abc import Generator, Iterable, Mapping
66
from pathlib import Path
7-
from typing import Any
7+
from typing import Any, Union
88

99
from .._compat.typing import Protocol
1010

11-
__all__ = ["load_provider"]
11+
__all__ = ["load_provider", "load_dynamic_metadata"]
1212

1313

1414
def __dir__() -> list[str]:
@@ -27,10 +27,31 @@ def get_requires_for_dynamic_metadata(self, settings: dict[str, Any]) -> list[st
2727
...
2828

2929

30+
class DynamicMetadataWheelProtocol(DynamicMetadataProtocol, Protocol):
31+
def dynamic_wheel(
32+
self, field: str, settings: Mapping[str, Any] | None = None
33+
) -> bool:
34+
...
35+
36+
37+
class DynamicMetadataRequirementsWheelProtocol(
38+
DynamicMetadataRequirementsProtocol, DynamicMetadataWheelProtocol, Protocol
39+
):
40+
...
41+
42+
43+
DMProtocols = Union[
44+
DynamicMetadataProtocol,
45+
DynamicMetadataRequirementsProtocol,
46+
DynamicMetadataWheelProtocol,
47+
DynamicMetadataRequirementsWheelProtocol,
48+
]
49+
50+
3051
def load_provider(
3152
provider: str,
3253
provider_path: str | None = None,
33-
) -> DynamicMetadataProtocol | DynamicMetadataRequirementsProtocol:
54+
) -> DMProtocols:
3455
if provider_path is None:
3556
return importlib.import_module(provider)
3657

@@ -43,3 +64,16 @@ def load_provider(
4364
return importlib.import_module(provider)
4465
finally:
4566
sys.path.pop(0)
67+
68+
69+
def load_dynamic_metadata(
70+
metadata: Mapping[str, Mapping[str, str]]
71+
) -> Generator[tuple[str, DMProtocols | None, dict[str, str]], None, None]:
72+
for field, orig_config in metadata.items():
73+
if "provider" in orig_config:
74+
config = dict(orig_config)
75+
provider = config.pop("provider")
76+
provider_path = config.pop("provider-path", None)
77+
yield field, load_provider(provider, provider_path), config
78+
else:
79+
yield field, None, dict(orig_config)
Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
22

3+
from collections.abc import Mapping
34
from typing import Any
45

56
from packaging.version import Version
67
from pyproject_metadata import StandardMetadata
78

89
from ..settings.skbuild_model import ScikitBuildSettings
9-
from ._load_provider import load_provider
10+
from ._load_provider import load_dynamic_metadata
1011

1112
__all__ = ["get_standard_metadata"]
1213

@@ -17,37 +18,24 @@ def __dir__() -> list[str]:
1718

1819
# If pyproject-metadata eventually supports updates, this can be simplified
1920
def get_standard_metadata(
20-
pyproject_dict: dict[str, Any],
21+
pyproject_dict: Mapping[str, Any],
2122
settings: ScikitBuildSettings,
2223
) -> StandardMetadata:
24+
new_pyproject_dict = dict(pyproject_dict)
2325
# Handle any dynamic metadata
24-
calls: dict[frozenset[tuple[str, Any]], set[str]] = {}
25-
for field, raw_settings in settings.metadata.items():
26-
if field not in pyproject_dict.get("project", {}).get("dynamic", []):
27-
msg = f"{field} is not in project.dynamic"
28-
raise KeyError(msg)
29-
if "provider" not in raw_settings:
26+
for field, provider, config in load_dynamic_metadata(settings.metadata):
27+
if provider is None:
3028
msg = f"{field} is missing provider"
3129
raise KeyError(msg)
32-
calls.setdefault(frozenset(raw_settings.items()), set()).add(field)
33-
34-
for call, fields in calls.items():
35-
args = dict(call)
36-
provider = args.pop("provider")
37-
provider_path = args.pop("provider-path", None)
38-
computed = load_provider(provider, provider_path).dynamic_metadata(
39-
frozenset(fields), args
40-
)
41-
if set(computed) != fields:
42-
msg = f"{provider} did not return requested fields"
30+
if field not in pyproject_dict.get("project", {}).get("dynamic", []):
31+
msg = f"{field} is not in project.dynamic"
4332
raise KeyError(msg)
44-
pyproject_dict["project"].update(computed)
45-
for field in fields:
46-
pyproject_dict["project"]["dynamic"].remove(field)
33+
new_pyproject_dict["project"][field] = provider.dynamic_metadata(field, config)
34+
new_pyproject_dict["project"]["dynamic"].remove(field)
4735

48-
metadata = StandardMetadata.from_pyproject(pyproject_dict)
36+
metadata = StandardMetadata.from_pyproject(new_pyproject_dict)
4937
# pyproject-metadata normalizes the name - see https://github.com/FFY00/python-pyproject-metadata/pull/65
5038
# For scikit-build-core 0.5+, we keep the un-normalized name, and normalize it when using it for filenames
5139
if settings.minimum_version is None or settings.minimum_version >= Version("0.5"):
52-
metadata.name = pyproject_dict["project"]["name"]
40+
metadata.name = new_pyproject_dict["project"]["name"]
5341
return metadata

tests/packages/custom_cmake/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15...3.26)
33
project(
44
${SKBUILD_PROJECT_NAME}
55
LANGUAGES
6-
VERSION ${SKBUILD_PROJECT_VERSION})
6+
VERSION 2.3.4)
77

88
find_package(ExamplePkg REQUIRED)
99

tests/packages/custom_cmake/pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,13 @@ build-backend = "scikit_build_core.build"
44

55
[project]
66
name = "custom_modules"
7-
version = "0.0.1"
7+
dynamic = ["version"]
8+
9+
[tool.scikit-build]
10+
wheel.packages = []
11+
wheel.license-files = []
12+
13+
[tool.scikit-build.metadata.version]
14+
provider = "scikit_build_core.metadata.regex"
15+
input = "CMakeLists.txt"
16+
regex = 'project\([^)]+ VERSION (?P<value>[0-9.]+)'

tests/packages/dynamic_metadata/plugins/local/version/nested/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ def __dir__() -> list[str]:
88

99

1010
def dynamic_metadata(
11-
fields: frozenset[str],
11+
field: str,
1212
settings: dict[str, object] | None = None,
13-
) -> dict[str, str | dict[str, str | None]]:
14-
if fields != {"version"}:
13+
) -> str:
14+
if field != "version":
1515
msg = "Only the 'version' field is supported"
1616
raise ValueError(msg)
1717

1818
if settings:
1919
msg = "No inline configuration is supported"
2020
raise ValueError(msg)
2121

22-
return {"version": "3.2.1"}
22+
return "3.2.1"

0 commit comments

Comments
 (0)