diff --git a/docs/api/scikit_build_core.rst b/docs/api/scikit_build_core.rst index d926008bf..e1d25de28 100644 --- a/docs/api/scikit_build_core.rst +++ b/docs/api/scikit_build_core.rst @@ -41,6 +41,14 @@ scikit\_build\_core.errors module :show-inheritance: :undoc-members: +scikit\_build\_core.format module +--------------------------------- + +.. automodule:: scikit_build_core.format + :members: + :show-inheritance: + :undoc-members: + scikit\_build\_core.program\_search module ------------------------------------------ diff --git a/docs/configuration/formatted.md b/docs/configuration/formatted.md new file mode 100644 index 000000000..bc19d04c4 --- /dev/null +++ b/docs/configuration/formatted.md @@ -0,0 +1,27 @@ +# Formattable fields + +The following configure keys are formatted as Python f-strings: + +- `build-dir` +- `build.requires` + +The available variables are documented in the members of +{py:class}`scikit_build_core.format.PyprojectFormatter` copied here for +visibility + +```{eval-rst} +.. autoattribute:: scikit_build_core.format.PyprojectFormatter.build_type + :no-index: + +.. autoattribute:: scikit_build_core.format.PyprojectFormatter.cache_tag + :no-index: + +.. autoattribute:: scikit_build_core.format.PyprojectFormatter.root + :no-index: + +.. autoattribute:: scikit_build_core.format.PyprojectFormatter.state + :no-index: + +.. autoattribute:: scikit_build_core.format.PyprojectFormatter.wheel_tag + :no-index: +``` diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 2a0e57468..ac01a8295 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -643,13 +643,8 @@ speedups. ``` -There are several values you can access through Python's formatting syntax: - -- `cache_tag`: `sys.implementation.cache_tag` -- `wheel_tag`: The tags as computed for the wheel -- `build_type`: The current build type (`Release` by default) -- `state`: The current run state, `sdist`, `wheel`, `editable`, - `metadata_wheel`, and `metadata_editable` +There are several values you can access through Python's formatting syntax. See +[](./formatted.md). Scikit-build-core also strictly validates configuration; if you need to disable this, you can: diff --git a/docs/index.md b/docs/index.md index 89d81b1d0..eef8f0a0c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,6 +43,7 @@ guide/faqs configuration/index configuration/overrides configuration/dynamic +configuration/formatted ``` ```{toctree} diff --git a/pyproject.toml b/pyproject.toml index d5cfe2d8b..f4f279f2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -290,9 +290,6 @@ known-local-folder = ["pathutils"] "typing.Self".msg = "Use scikit_build_core._compat.typing.Self instead." "typing_extensions.Self".msg = "Use scikit_build_core._compat.typing.Self instead." "typing.Final".msg = "Add scikit_build_core._compat.typing.Final instead." -"typing.NotRequired".msg = "Add scikit_build_core._compat.typing.NotRequired instead." -"typing.OrderedDict".msg = "Add scikit_build_core._compat.typing.OrderedDict instead." -"typing.TypedDict".msg = "Add scikit_build_core._compat.typing.TypedDict instead." "typing.assert_never".msg = "Add scikit_build_core._compat.typing.assert_never instead." "tomli".msg = "Use scikit_build_core._compat.tomllib instead." "tomllib".msg = "Use scikit_build_core._compat.tomllib instead." diff --git a/src/scikit_build_core/build/_file_processor.py b/src/scikit_build_core/build/_file_processor.py index 4825b28cb..f084d3a4b 100644 --- a/src/scikit_build_core/build/_file_processor.py +++ b/src/scikit_build_core/build/_file_processor.py @@ -7,6 +7,8 @@ import pathspec +from scikit_build_core.format import pyproject_format + if TYPE_CHECKING: from collections.abc import Generator, Sequence @@ -51,12 +53,7 @@ def each_unignored_file( if p != Path(".gitignore") } - exclude_build_dir = build_dir.format( - cache_tag="*", - wheel_tag="*", - build_type="*", - state="*", - ) + exclude_build_dir = build_dir.format(**pyproject_format(dummy=True)) exclude_lines = ( [*EXCLUDE_LINES, exclude_build_dir] if exclude_build_dir else EXCLUDE_LINES diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py index c3e9a394d..47e223bb6 100644 --- a/src/scikit_build_core/build/wheel.py +++ b/src/scikit_build_core/build/wheel.py @@ -3,7 +3,6 @@ import dataclasses import os import shutil -import sys import sysconfig import tempfile from collections.abc import Mapping @@ -20,6 +19,7 @@ from ..builder.wheel_tag import WheelTag from ..cmake import CMake, CMaker from ..errors import FailedLiveProcessError +from ..format import pyproject_format from ..settings.skbuild_read_settings import SettingsReader from ._editable import editable_redirect, libdir_to_installed, mapping_to_modules from ._init import setup_logging @@ -279,10 +279,11 @@ def _build_wheel_impl_impl( build_dir = ( Path( settings.build_dir.format( - cache_tag=sys.implementation.cache_tag, - wheel_tag=str(tags), - build_type=settings.cmake.build_type, - state=state, + **pyproject_format( + settings=settings, + tags=tags, + state=state, + ) ) ) if settings.build_dir diff --git a/src/scikit_build_core/builder/get_requires.py b/src/scikit_build_core/builder/get_requires.py index 26b6d7c4b..492aa1ec7 100644 --- a/src/scikit_build_core/builder/get_requires.py +++ b/src/scikit_build_core/builder/get_requires.py @@ -5,13 +5,13 @@ import importlib.util import os import sysconfig -from pathlib import Path from typing import TYPE_CHECKING, Literal from packaging.tags import sys_tags from .._compat import tomllib from .._logging import logger +from ..format import pyproject_format from ..program_search import ( best_program, get_cmake_programs, @@ -67,28 +67,6 @@ def _load_scikit_build_settings( return SettingsReader.from_file("pyproject.toml", config_settings).settings -@dataclasses.dataclass() -class RootPathResolver: - """Handle ``{root:uri}`` like formatting similar to ``hatchling``.""" - - path: Path = dataclasses.field(default_factory=Path) - - def __post_init__(self) -> None: - self.path = self.path.resolve() - - def __format__(self, fmt: str) -> str: - command, _, rest = fmt.partition(":") - if command == "parent": - parent = RootPathResolver(self.path.parent) - return parent.__format__(rest) - if command == "uri" and rest == "": - return self.path.as_uri() - if command == "" and rest == "": - return str(self) - msg = f"Could not handle format: {fmt}" - raise ValueError(msg) - - @dataclasses.dataclass(frozen=True) class GetRequires: settings: ScikitBuildSettings = dataclasses.field( @@ -164,7 +142,11 @@ def dynamic_metadata(self) -> Generator[str, None, None]: return for build_require in self.settings.build.requires: - yield build_require.format(root=RootPathResolver()) + yield build_require.format( + **pyproject_format( + settings=self.settings, + ) + ) for dynamic_metadata in self.settings.metadata.values(): if "provider" in dynamic_metadata: diff --git a/src/scikit_build_core/format.py b/src/scikit_build_core/format.py new file mode 100644 index 000000000..a942ad8fc --- /dev/null +++ b/src/scikit_build_core/format.py @@ -0,0 +1,113 @@ +"""Format variables available in the ``pyproject.toml`` evaluation""" + +from __future__ import annotations + +import dataclasses +import sys +import typing +from pathlib import Path +from typing import TYPE_CHECKING, TypedDict + +if TYPE_CHECKING: + from typing import Literal + + from scikit_build_core.builder.wheel_tag import WheelTag + from scikit_build_core.settings.skbuild_model import ScikitBuildSettings + +__all__ = [ + "PyprojectFormatter", + "RootPathResolver", + "pyproject_format", +] + + +def __dir__() -> list[str]: + return __all__ + + +class PyprojectFormatter(TypedDict, total=False): + """Format helper for pyproject.toml. + + Stores all known variables that can be used for evaluating a formatted string + in the pyproject.toml config file. + """ + + cache_tag: str + """Tag used by the import machinery in the filenames of cached modules, i.e. ``sys.implementation.cache_tag``.""" + wheel_tag: str + """The tags as computed for the wheel.""" + build_type: str + """Build type passed as ``cmake.build_type``.""" + state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"] + """The state of the build.""" + root: RootPathResolver + """Root path of the current project.""" + + +@typing.overload +def pyproject_format( + *, + settings: ScikitBuildSettings, + state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"] + | None = ..., + tags: WheelTag | None = ..., +) -> PyprojectFormatter: ... + + +@typing.overload +def pyproject_format(*, dummy: Literal[True]) -> dict[str, str]: ... + + +def pyproject_format( + *, + settings: ScikitBuildSettings | None = None, + state: ( + Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"] + | None + ) = None, + tags: WheelTag | None = None, + dummy: bool = False, +) -> PyprojectFormatter | dict[str, str]: + """Generate :py:class:`PyprojectFormatter` dictionary to use in f-string format.""" + if dummy: + # Return a dict with all the known keys but with values replaced with dummy values + return {key: "*" for key in PyprojectFormatter.__annotations__} + + assert settings is not None + # First set all known values + res = PyprojectFormatter( + cache_tag=sys.implementation.cache_tag, + # We are assuming the Path.cwd always evaluates to the folder containing pyproject.toml + # as part of PEP517 standard. + root=RootPathResolver(), + build_type=settings.cmake.build_type, + ) + # Then compute all optional keys depending on the function input + if tags is not None: + res["wheel_tag"] = str(tags) + if state is not None: + res["state"] = state + # Construct the final dict including the always known keys + return res + + +@dataclasses.dataclass() +class RootPathResolver: + """Handle ``{root:uri}`` like formatting similar to ``hatchling``.""" + + path: Path = dataclasses.field(default_factory=Path) + + def __post_init__(self) -> None: + self.path = self.path.resolve() + + def __format__(self, fmt: str) -> str: + command, _, rest = fmt.partition(":") + if command == "parent": + parent = RootPathResolver(self.path.parent) + return parent.__format__(rest) + if command == "uri" and rest == "": + return self.path.as_uri() + if command == "" and rest == "": + return str(self) + msg = f"Could not handle format: {fmt}" + raise ValueError(msg) diff --git a/src/scikit_build_core/hatch/plugin.py b/src/scikit_build_core/hatch/plugin.py index 2af6bbb80..7cde735bd 100644 --- a/src/scikit_build_core/hatch/plugin.py +++ b/src/scikit_build_core/hatch/plugin.py @@ -6,7 +6,6 @@ import importlib.metadata import os import shutil -import sys import sysconfig import tempfile import typing @@ -24,6 +23,7 @@ from ..builder.get_requires import GetRequires from ..builder.wheel_tag import WheelTag from ..cmake import CMake, CMaker +from ..format import pyproject_format from ..settings.skbuild_read_settings import SettingsReader __all__ = ["ScikitBuildHook"] @@ -162,10 +162,11 @@ def _initialize(self, *, build_data: dict[str, Any]) -> None: build_dir = ( Path( settings.build_dir.format( - cache_tag=sys.implementation.cache_tag, - wheel_tag=str(tags), - build_type=settings.cmake.build_type, - state=state, + **pyproject_format( + settings=settings, + tags=tags, + state=state, + ) ) ) if settings.build_dir