Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/api/scikit_build_core.metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ scikit\_build\_core.metadata.fancy\_pypi\_readme module
:undoc-members:
:show-inheritance:

scikit\_build\_core.metadata.regex module
-----------------------------------------

.. automodule:: scikit_build_core.metadata.regex
:members:
:undoc-members:
:show-inheritance:

scikit\_build\_core.metadata.setuptools\_scm module
---------------------------------------------------

Expand Down
25 changes: 25 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,31 @@ metadata.readme.provider = "scikit_build_core.metadata.fancy_pypi_readme"

:::

:::{tab} Regex

If you want to pull a string-valued expression (usually version) from an
existing file, you can the integrated `regex` plugin to pull the information.

```toml
name = "mypackage"
dynamic = ["version"]

[tool.scikit-build.metadata.version]
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,
`'(?i)^(__version__|VERSION) *= *([\'"])v?(?P<value>.+?)\2'`.

```{versionadded} 0.5

```

:::

## Editable installs

Experimental support for editable installs is provided, with some caveats and
Expand Down
20 changes: 9 additions & 11 deletions src/scikit_build_core/metadata/fancy_pypi_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@

from .._compat import tomllib

__all__ = ["dynamic_metadata"]
__all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"]


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


def dynamic_metadata(
fields: frozenset[str],
field: str,
settings: dict[str, list[str] | str] | None = None,
) -> dict[str, str | dict[str, str | None]]:
) -> str | dict[str, str]:
from hatch_fancy_pypi_readme._builder import build_text
from hatch_fancy_pypi_readme._config import load_and_validate_config

if fields != {"readme"}:
if field != "readme":
msg = "Only the 'readme' field is supported"
raise ValueError(msg)

Expand All @@ -35,13 +35,11 @@ def dynamic_metadata(

# Version 22.3 does not have fragment support
return {
"readme": {
"content-type": config.content_type,
"text": build_text(config.fragments, config.substitutions)
if hasattr(config, "substitutions")
# pylint: disable-next=no-value-for-parameter
else build_text(config.fragments), # type: ignore[call-arg]
}
"content-type": config.content_type,
"text": build_text(config.fragments, config.substitutions)
if hasattr(config, "substitutions")
# pylint: disable-next=no-value-for-parameter
else build_text(config.fragments), # type: ignore[call-arg]
}


Expand Down
48 changes: 48 additions & 0 deletions src/scikit_build_core/metadata/regex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

import re
from collections.abc import Mapping
from pathlib import Path
from typing import Any

__all__ = ["dynamic_metadata"]


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


def dynamic_metadata(
field: str,
settings: Mapping[str, Any],
) -> str:
# Input validation
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"
raise RuntimeError(msg)
if "input" not in settings:
msg = "Must contain the 'input' setting to perform a regex on"
raise RuntimeError(msg)
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)

input_filename = settings["input"]
regex = settings.get(
"regex", r'(?i)^(__version__|VERSION) *= *([\'"])v?(?P<value>.+?)\2'
)

with Path(input_filename).open(encoding="utf-8") as f:
match = re.search(regex, f.read(), re.MULTILINE)

if not match:
msg = f"Couldn't find {regex!r} in {input_filename}"
raise RuntimeError(msg)

return match.group("value")
10 changes: 5 additions & 5 deletions src/scikit_build_core/metadata/setuptools_scm.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from __future__ import annotations

__all__ = ["dynamic_metadata"]
__all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"]


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


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

if fields != {"version"}:
if field != "version":
msg = "Only the 'version' field is supported"
raise ValueError(msg)

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

return {"version": version}
return version


def get_requires_for_dynamic_metadata(
Expand Down
42 changes: 38 additions & 4 deletions src/scikit_build_core/settings/_load_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import importlib
import sys
from collections.abc import Iterable
from collections.abc import Generator, Iterable, Mapping
from pathlib import Path
from typing import Any
from typing import Any, Union

from .._compat.typing import Protocol

__all__ = ["load_provider"]
__all__ = ["load_provider", "load_dynamic_metadata"]


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


class DynamicMetadataWheelProtocol(DynamicMetadataProtocol, Protocol):
def dynamic_wheel(
self, field: str, settings: Mapping[str, Any] | None = None
) -> bool:
...


class DynamicMetadataRequirementsWheelProtocol(
DynamicMetadataRequirementsProtocol, DynamicMetadataWheelProtocol, Protocol
):
...


DMProtocols = Union[
DynamicMetadataProtocol,
DynamicMetadataRequirementsProtocol,
DynamicMetadataWheelProtocol,
DynamicMetadataRequirementsWheelProtocol,
]


def load_provider(
provider: str,
provider_path: str | None = None,
) -> DynamicMetadataProtocol | DynamicMetadataRequirementsProtocol:
) -> DMProtocols:
if provider_path is None:
return importlib.import_module(provider)

Expand All @@ -43,3 +64,16 @@ def load_provider(
return importlib.import_module(provider)
finally:
sys.path.pop(0)


def load_dynamic_metadata(
metadata: Mapping[str, Mapping[str, str]]
) -> Generator[tuple[str, DMProtocols | None, dict[str, str]], None, None]:
for field, orig_config in metadata.items():
if "provider" in orig_config:
config = dict(orig_config)
provider = config.pop("provider")
provider_path = config.pop("provider-path", None)
yield field, load_provider(provider, provider_path), config
else:
yield field, None, dict(orig_config)
36 changes: 12 additions & 24 deletions src/scikit_build_core/settings/metadata.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from packaging.version import Version
from pyproject_metadata import StandardMetadata

from ..settings.skbuild_model import ScikitBuildSettings
from ._load_provider import load_provider
from ._load_provider import load_dynamic_metadata

__all__ = ["get_standard_metadata"]

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

# If pyproject-metadata eventually supports updates, this can be simplified
def get_standard_metadata(
pyproject_dict: dict[str, Any],
pyproject_dict: Mapping[str, Any],
settings: ScikitBuildSettings,
) -> StandardMetadata:
new_pyproject_dict = dict(pyproject_dict)
# Handle any dynamic metadata
calls: dict[frozenset[tuple[str, Any]], set[str]] = {}
for field, raw_settings in settings.metadata.items():
if field not in pyproject_dict.get("project", {}).get("dynamic", []):
msg = f"{field} is not in project.dynamic"
raise KeyError(msg)
if "provider" not in raw_settings:
for field, provider, config in load_dynamic_metadata(settings.metadata):
if provider is None:
msg = f"{field} is missing provider"
raise KeyError(msg)
calls.setdefault(frozenset(raw_settings.items()), set()).add(field)

for call, fields in calls.items():
args = dict(call)
provider = args.pop("provider")
provider_path = args.pop("provider-path", None)
computed = load_provider(provider, provider_path).dynamic_metadata(
frozenset(fields), args
)
if set(computed) != fields:
msg = f"{provider} did not return requested fields"
if field not in pyproject_dict.get("project", {}).get("dynamic", []):
msg = f"{field} is not in project.dynamic"
raise KeyError(msg)
pyproject_dict["project"].update(computed)
for field in fields:
pyproject_dict["project"]["dynamic"].remove(field)
new_pyproject_dict["project"][field] = provider.dynamic_metadata(field, config)
new_pyproject_dict["project"]["dynamic"].remove(field)

metadata = StandardMetadata.from_pyproject(pyproject_dict)
metadata = StandardMetadata.from_pyproject(new_pyproject_dict)
# pyproject-metadata normalizes the name - see https://github.com/FFY00/python-pyproject-metadata/pull/65
# For scikit-build-core 0.5+, we keep the un-normalized name, and normalize it when using it for filenames
if settings.minimum_version is None or settings.minimum_version >= Version("0.5"):
metadata.name = pyproject_dict["project"]["name"]
metadata.name = new_pyproject_dict["project"]["name"]
return metadata
2 changes: 1 addition & 1 deletion tests/packages/custom_cmake/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15...3.26)
project(
${SKBUILD_PROJECT_NAME}
LANGUAGES
VERSION ${SKBUILD_PROJECT_VERSION})
VERSION 2.3.4)

find_package(ExamplePkg REQUIRED)

Expand Down
11 changes: 10 additions & 1 deletion tests/packages/custom_cmake/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@ build-backend = "scikit_build_core.build"

[project]
name = "custom_modules"
version = "0.0.1"
dynamic = ["version"]

[tool.scikit-build]
wheel.packages = []
wheel.license-files = []

[tool.scikit-build.metadata.version]
provider = "scikit_build_core.metadata.regex"
input = "CMakeLists.txt"
regex = 'project\([^)]+ VERSION (?P<value>[0-9.]+)'
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ def __dir__() -> list[str]:


def dynamic_metadata(
fields: frozenset[str],
field: str,
settings: dict[str, object] | None = None,
) -> dict[str, str | dict[str, str | None]]:
if fields != {"version"}:
) -> str:
if field != "version":
msg = "Only the 'version' field is supported"
raise ValueError(msg)

if settings:
msg = "No inline configuration is supported"
raise ValueError(msg)

return {"version": "3.2.1"}
return "3.2.1"
8 changes: 8 additions & 0 deletions tests/test_custom_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ def test_ep(isolated):
pysys = isolated.execute("import sys; print(sys.executable)").strip()
contents = Path(script).read_text()
assert contents.startswith(f"#!{pysys}")

if sys.version_info >= (3, 8):
assert (
isolated.execute(
"from importlib import metadata; print(metadata.version('custom_modules'), end='')"
)
== "2.3.4"
)
Loading