Skip to content

Commit 8f79c17

Browse files
LecrisUThenryiii
andauthored
refactor: add a format module that is used in the expansion of pyproject.toml (#998)
Related to #992 formatter, I think it would be good to have a common place where to define all the format variables. Not sure on the implementation though if it should be a `TypedDict`, simple `Mapping` wrapper or anything else. --------- Signed-off-by: Cristian Le <[email protected]> Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Henry Schreiner <[email protected]>
1 parent 297bd90 commit 8f79c17

File tree

10 files changed

+172
-50
lines changed

10 files changed

+172
-50
lines changed

docs/api/scikit_build_core.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ scikit\_build\_core.errors module
4141
:show-inheritance:
4242
:undoc-members:
4343

44+
scikit\_build\_core.format module
45+
---------------------------------
46+
47+
.. automodule:: scikit_build_core.format
48+
:members:
49+
:show-inheritance:
50+
:undoc-members:
51+
4452
scikit\_build\_core.program\_search module
4553
------------------------------------------
4654

docs/configuration/formatted.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Formattable fields
2+
3+
The following configure keys are formatted as Python f-strings:
4+
5+
- `build-dir`
6+
- `build.requires`
7+
8+
The available variables are documented in the members of
9+
{py:class}`scikit_build_core.format.PyprojectFormatter` copied here for
10+
visibility
11+
12+
```{eval-rst}
13+
.. autoattribute:: scikit_build_core.format.PyprojectFormatter.build_type
14+
:no-index:
15+
16+
.. autoattribute:: scikit_build_core.format.PyprojectFormatter.cache_tag
17+
:no-index:
18+
19+
.. autoattribute:: scikit_build_core.format.PyprojectFormatter.root
20+
:no-index:
21+
22+
.. autoattribute:: scikit_build_core.format.PyprojectFormatter.state
23+
:no-index:
24+
25+
.. autoattribute:: scikit_build_core.format.PyprojectFormatter.wheel_tag
26+
:no-index:
27+
```

docs/configuration/index.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -643,13 +643,8 @@ speedups.
643643
644644
```
645645

646-
There are several values you can access through Python's formatting syntax:
647-
648-
- `cache_tag`: `sys.implementation.cache_tag`
649-
- `wheel_tag`: The tags as computed for the wheel
650-
- `build_type`: The current build type (`Release` by default)
651-
- `state`: The current run state, `sdist`, `wheel`, `editable`,
652-
`metadata_wheel`, and `metadata_editable`
646+
There are several values you can access through Python's formatting syntax. See
647+
[](./formatted.md).
653648

654649
Scikit-build-core also strictly validates configuration; if you need to disable
655650
this, you can:

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ guide/faqs
4343
configuration/index
4444
configuration/overrides
4545
configuration/dynamic
46+
configuration/formatted
4647
```
4748

4849
```{toctree}

pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,6 @@ known-local-folder = ["pathutils"]
291291
"typing.Self".msg = "Use scikit_build_core._compat.typing.Self instead."
292292
"typing_extensions.Self".msg = "Use scikit_build_core._compat.typing.Self instead."
293293
"typing.Final".msg = "Add scikit_build_core._compat.typing.Final instead."
294-
"typing.NotRequired".msg = "Add scikit_build_core._compat.typing.NotRequired instead."
295-
"typing.OrderedDict".msg = "Add scikit_build_core._compat.typing.OrderedDict instead."
296-
"typing.TypedDict".msg = "Add scikit_build_core._compat.typing.TypedDict instead."
297294
"typing.assert_never".msg = "Add scikit_build_core._compat.typing.assert_never instead."
298295
"tomli".msg = "Use scikit_build_core._compat.tomllib instead."
299296
"tomllib".msg = "Use scikit_build_core._compat.tomllib instead."

src/scikit_build_core/build/_file_processor.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import pathspec
99

10+
from scikit_build_core.format import pyproject_format
11+
1012
if TYPE_CHECKING:
1113
from collections.abc import Generator, Sequence
1214

@@ -51,12 +53,7 @@ def each_unignored_file(
5153
if p != Path(".gitignore")
5254
}
5355

54-
exclude_build_dir = build_dir.format(
55-
cache_tag="*",
56-
wheel_tag="*",
57-
build_type="*",
58-
state="*",
59-
)
56+
exclude_build_dir = build_dir.format(**pyproject_format(dummy=True))
6057

6158
exclude_lines = (
6259
[*EXCLUDE_LINES, exclude_build_dir] if exclude_build_dir else EXCLUDE_LINES

src/scikit_build_core/build/wheel.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import dataclasses
44
import os
55
import shutil
6-
import sys
76
import sysconfig
87
import tempfile
98
from collections.abc import Mapping
@@ -20,6 +19,7 @@
2019
from ..builder.wheel_tag import WheelTag
2120
from ..cmake import CMake, CMaker
2221
from ..errors import FailedLiveProcessError
22+
from ..format import pyproject_format
2323
from ..settings.skbuild_read_settings import SettingsReader
2424
from ._editable import editable_redirect, libdir_to_installed, mapping_to_modules
2525
from ._init import setup_logging
@@ -279,10 +279,11 @@ def _build_wheel_impl_impl(
279279
build_dir = (
280280
Path(
281281
settings.build_dir.format(
282-
cache_tag=sys.implementation.cache_tag,
283-
wheel_tag=str(tags),
284-
build_type=settings.cmake.build_type,
285-
state=state,
282+
**pyproject_format(
283+
settings=settings,
284+
tags=tags,
285+
state=state,
286+
)
286287
)
287288
)
288289
if settings.build_dir

src/scikit_build_core/builder/get_requires.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import importlib.util
66
import os
77
import sysconfig
8-
from pathlib import Path
98
from typing import TYPE_CHECKING, Literal
109

1110
from packaging.tags import sys_tags
1211

1312
from .._compat import tomllib
1413
from .._logging import logger
14+
from ..format import pyproject_format
1515
from ..program_search import (
1616
best_program,
1717
get_cmake_programs,
@@ -67,28 +67,6 @@ def _load_scikit_build_settings(
6767
return SettingsReader.from_file("pyproject.toml", config_settings).settings
6868

6969

70-
@dataclasses.dataclass()
71-
class RootPathResolver:
72-
"""Handle ``{root:uri}`` like formatting similar to ``hatchling``."""
73-
74-
path: Path = dataclasses.field(default_factory=Path)
75-
76-
def __post_init__(self) -> None:
77-
self.path = self.path.resolve()
78-
79-
def __format__(self, fmt: str) -> str:
80-
command, _, rest = fmt.partition(":")
81-
if command == "parent":
82-
parent = RootPathResolver(self.path.parent)
83-
return parent.__format__(rest)
84-
if command == "uri" and rest == "":
85-
return self.path.as_uri()
86-
if command == "" and rest == "":
87-
return str(self)
88-
msg = f"Could not handle format: {fmt}"
89-
raise ValueError(msg)
90-
91-
9270
@dataclasses.dataclass(frozen=True)
9371
class GetRequires:
9472
settings: ScikitBuildSettings = dataclasses.field(
@@ -164,7 +142,11 @@ def dynamic_metadata(self) -> Generator[str, None, None]:
164142
return
165143

166144
for build_require in self.settings.build.requires:
167-
yield build_require.format(root=RootPathResolver())
145+
yield build_require.format(
146+
**pyproject_format(
147+
settings=self.settings,
148+
)
149+
)
168150

169151
for dynamic_metadata in self.settings.metadata.values():
170152
if "provider" in dynamic_metadata:

src/scikit_build_core/format.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Format variables available in the ``pyproject.toml`` evaluation"""
2+
3+
from __future__ import annotations
4+
5+
import dataclasses
6+
import sys
7+
import typing
8+
from pathlib import Path
9+
from typing import TYPE_CHECKING, TypedDict
10+
11+
if TYPE_CHECKING:
12+
from typing import Literal
13+
14+
from scikit_build_core.builder.wheel_tag import WheelTag
15+
from scikit_build_core.settings.skbuild_model import ScikitBuildSettings
16+
17+
__all__ = [
18+
"PyprojectFormatter",
19+
"RootPathResolver",
20+
"pyproject_format",
21+
]
22+
23+
24+
def __dir__() -> list[str]:
25+
return __all__
26+
27+
28+
class PyprojectFormatter(TypedDict, total=False):
29+
"""Format helper for pyproject.toml.
30+
31+
Stores all known variables that can be used for evaluating a formatted string
32+
in the pyproject.toml config file.
33+
"""
34+
35+
cache_tag: str
36+
"""Tag used by the import machinery in the filenames of cached modules, i.e. ``sys.implementation.cache_tag``."""
37+
wheel_tag: str
38+
"""The tags as computed for the wheel."""
39+
build_type: str
40+
"""Build type passed as ``cmake.build_type``."""
41+
state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"]
42+
"""The state of the build."""
43+
root: RootPathResolver
44+
"""Root path of the current project."""
45+
46+
47+
@typing.overload
48+
def pyproject_format(
49+
*,
50+
settings: ScikitBuildSettings,
51+
state: Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"]
52+
| None = ...,
53+
tags: WheelTag | None = ...,
54+
) -> PyprojectFormatter: ...
55+
56+
57+
@typing.overload
58+
def pyproject_format(*, dummy: Literal[True]) -> dict[str, str]: ...
59+
60+
61+
def pyproject_format(
62+
*,
63+
settings: ScikitBuildSettings | None = None,
64+
state: (
65+
Literal["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"]
66+
| None
67+
) = None,
68+
tags: WheelTag | None = None,
69+
dummy: bool = False,
70+
) -> PyprojectFormatter | dict[str, str]:
71+
"""Generate :py:class:`PyprojectFormatter` dictionary to use in f-string format."""
72+
if dummy:
73+
# Return a dict with all the known keys but with values replaced with dummy values
74+
return {key: "*" for key in PyprojectFormatter.__annotations__}
75+
76+
assert settings is not None
77+
# First set all known values
78+
res = PyprojectFormatter(
79+
cache_tag=sys.implementation.cache_tag,
80+
# We are assuming the Path.cwd always evaluates to the folder containing pyproject.toml
81+
# as part of PEP517 standard.
82+
root=RootPathResolver(),
83+
build_type=settings.cmake.build_type,
84+
)
85+
# Then compute all optional keys depending on the function input
86+
if tags is not None:
87+
res["wheel_tag"] = str(tags)
88+
if state is not None:
89+
res["state"] = state
90+
# Construct the final dict including the always known keys
91+
return res
92+
93+
94+
@dataclasses.dataclass()
95+
class RootPathResolver:
96+
"""Handle ``{root:uri}`` like formatting similar to ``hatchling``."""
97+
98+
path: Path = dataclasses.field(default_factory=Path)
99+
100+
def __post_init__(self) -> None:
101+
self.path = self.path.resolve()
102+
103+
def __format__(self, fmt: str) -> str:
104+
command, _, rest = fmt.partition(":")
105+
if command == "parent":
106+
parent = RootPathResolver(self.path.parent)
107+
return parent.__format__(rest)
108+
if command == "uri" and rest == "":
109+
return self.path.as_uri()
110+
if command == "" and rest == "":
111+
return str(self)
112+
msg = f"Could not handle format: {fmt}"
113+
raise ValueError(msg)

src/scikit_build_core/hatch/plugin.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import importlib.metadata
77
import os
88
import shutil
9-
import sys
109
import sysconfig
1110
import tempfile
1211
import typing
@@ -24,6 +23,7 @@
2423
from ..builder.get_requires import GetRequires
2524
from ..builder.wheel_tag import WheelTag
2625
from ..cmake import CMake, CMaker
26+
from ..format import pyproject_format
2727
from ..settings.skbuild_read_settings import SettingsReader
2828

2929
__all__ = ["ScikitBuildHook"]
@@ -162,10 +162,11 @@ def _initialize(self, *, build_data: dict[str, Any]) -> None:
162162
build_dir = (
163163
Path(
164164
settings.build_dir.format(
165-
cache_tag=sys.implementation.cache_tag,
166-
wheel_tag=str(tags),
167-
build_type=settings.cmake.build_type,
168-
state=state,
165+
**pyproject_format(
166+
settings=settings,
167+
tags=tags,
168+
state=state,
169+
)
169170
)
170171
)
171172
if settings.build_dir

0 commit comments

Comments
 (0)