Skip to content

Commit 5e3b4cd

Browse files
authored
refactor: settings (#338)
Working on refactoring settings. Cleanup to prepare for overrides. - fix: raise `TypeError` if item not convertible - refactor: `SourceChain` no longer a `Source` --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent 48a879b commit 5e3b4cd

File tree

1 file changed

+81
-34
lines changed

1 file changed

+81
-34
lines changed

src/scikit_build_core/settings/sources.py

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,39 @@
1+
"""
2+
This is the configuration tooling for scikit-build-core. This is build around
3+
the :class:`Source` Protocol. Sources are created with some input (like a toml
4+
file for the :class:`TOMLSource`). Sources also usually have some prefix (like
5+
``tool.scikit-build``) as well. The :class:`SourceChain` holds a collection of
6+
Sources, and is the primary way to use them.
7+
8+
An end user interacts with :class:`SourceChain` via ``.convert_target``, which
9+
takes a Dataclass class and returns an instance with fields populated.
10+
11+
Example of usage::
12+
13+
sources = SourceChain(TOMLSource("tool", "mypackage", settings=pyproject_dict), ...)
14+
settings = sources.convert_target(SomeSettingsModel)
15+
16+
unrecognized_options = list(source.unrecognized_options(SomeSettingsModel)
17+
18+
19+
Naming conventions:
20+
21+
- ``model`` is the complete Dataclass.
22+
- ``target`` is the type to convert a single item to.
23+
- ``settings`` is the input data source (unless it already has a name, like
24+
``env``).
25+
- ``options`` are the names of the items in the ``model``, formatted in the
26+
style of the current Source.
27+
- ``fields`` are the tuple of strings describing a nested field in the
28+
``model``.
29+
"""
30+
31+
132
from __future__ import annotations
233

334
import dataclasses
435
import os
36+
import typing
537
from collections.abc import Generator, Iterator, Mapping, Sequence
638
from typing import Any, TypeVar, Union
739

@@ -17,16 +49,16 @@ def __dir__() -> list[str]:
1749
return __all__
1850

1951

20-
def _dig_strict(dict_: Mapping[str, Any], *names: str) -> Any:
52+
def _dig_strict(__dict: Mapping[str, Any], *names: str) -> Any:
2153
for name in names:
22-
dict_ = dict_[name]
23-
return dict_
54+
__dict = __dict[name]
55+
return __dict
2456

2557

26-
def _dig_not_strict(dict_: Mapping[str, Any], *names: str) -> Any:
58+
def _dig_not_strict(__dict: Mapping[str, Any], *names: str) -> Any:
2759
for name in names:
28-
dict_ = dict_.get(name, {})
29-
return dict_
60+
__dict = __dict.get(name, {})
61+
return __dict
3062

3163

3264
def _dig_fields(__opt: Any, *names: str) -> Any:
@@ -53,7 +85,8 @@ def __args__(self) -> list[Any]:
5385

5486
def _process_union(target: type[Any]) -> Any:
5587
"""
56-
Selects the non-None item in an Optional or Optional-like Union. Passes through non-Unions.
88+
Selects the non-None item in an Optional or Optional-like Union. Passes
89+
through non-Unions.
5790
"""
5891

5992
if (
@@ -77,8 +110,8 @@ def _process_union(target: type[Any]) -> Any:
77110

78111
def _get_target_raw_type(target: type[Any]) -> type[Any]:
79112
"""
80-
Takes a type like Optional[str] and returns str,
81-
or Optional[Dict[str, int]] and returns dict.
113+
Takes a type like ``Optional[str]`` and returns str,
114+
or ``Optional[Dict[str, int]]`` and returns dict.
82115
"""
83116

84117
target = _process_union(target)
@@ -88,31 +121,31 @@ def _get_target_raw_type(target: type[Any]) -> type[Any]:
88121
return target
89122

90123

91-
def _get_inner_type(target: type[Any]) -> type[Any]:
124+
def _get_inner_type(__target: type[Any]) -> type[Any]:
92125
"""
93-
Takes a types like List[str] and returns str,
94-
or Dict[str, int] and returns int.
126+
Takes a types like ``List[str]`` and returns str,
127+
or ``Dict[str, int]`` and returns int.
95128
"""
96129

97-
raw_target = _get_target_raw_type(target)
98-
target = _process_union(target)
130+
raw_target = _get_target_raw_type(__target)
131+
target = _process_union(__target)
99132
if raw_target == list:
100-
assert isinstance(target, TypeLike)
133+
assert isinstance(__target, TypeLike)
101134
return target.__args__[0]
102135
if raw_target == dict:
103-
assert isinstance(target, TypeLike)
136+
assert isinstance(__target, TypeLike)
104137
return target.__args__[1]
105138
msg = f"Expected a list or dict, got {target!r}"
106139
raise AssertionError(msg)
107140

108141

109-
def _nested_dataclass_to_names(target: type[Any], *inner: str) -> Iterator[list[str]]:
142+
def _nested_dataclass_to_names(__target: type[Any], *inner: str) -> Iterator[list[str]]:
110143
"""
111-
Yields each entry, like ("a", "b", "c") for a.b.c
144+
Yields each entry, like ``("a", "b", "c")`` for ``a.b.c``.
112145
"""
113146

114-
if dataclasses.is_dataclass(target):
115-
for field in dataclasses.fields(target):
147+
if dataclasses.is_dataclass(__target):
148+
for field in dataclasses.fields(__target):
116149
yield from _nested_dataclass_to_names(field.type, *inner, field.name)
117150
else:
118151
yield list(inner)
@@ -121,23 +154,40 @@ def _nested_dataclass_to_names(target: type[Any], *inner: str) -> Iterator[list[
121154
class Source(Protocol):
122155
def has_item(self, *fields: str, is_dict: bool) -> bool:
123156
"""
124-
Check if the source contains a chain of fields. For example, fields =
125-
[Field(name="a"), Field(name="b")] will check if the source contains the
126-
key "a.b".
157+
Check if the source contains a chain of fields. For example, ``fields =
158+
[Field(name="a"), Field(name="b")]`` will check if the source contains the
159+
key "a.b". ``is_dict`` should be set if it can be nested.
127160
"""
128161
...
129162

130163
def get_item(self, *fields: str, is_dict: bool) -> Any:
164+
"""
165+
Select an item from a chain of fields. Raises KeyError if
166+
the there is no item. ``is_dict`` should be set if it can be nested.
167+
"""
131168
...
132169

133170
@classmethod
134171
def convert(cls, item: Any, target: type[Any]) -> object:
172+
"""
173+
Convert an ``item`` from the base representation of the source's source
174+
into a ``target`` type. Raises TypeError if the conversion fails.
175+
"""
135176
...
136177

137178
def unrecognized_options(self, options: object) -> Generator[str, None, None]:
179+
"""
180+
Given a model, produce an iterator of all unrecognized option names.
181+
Empty iterator if this can't be computed for the source (like for
182+
environment variables).
183+
"""
138184
...
139185

140186
def all_option_names(self, target: type[Any]) -> Iterator[str]:
187+
"""
188+
Given a model, produce a list of all possible names (used for producing
189+
suggestions).
190+
"""
141191
...
142192

143193

@@ -185,7 +235,7 @@ def convert(cls, item: str, target: type[Any]) -> object:
185235
if callable(raw_target):
186236
return raw_target(item)
187237
msg = f"Can't convert target {target}"
188-
raise AssertionError(msg)
238+
raise TypeError(msg)
189239

190240
def unrecognized_options(
191241
self, options: object # noqa: ARG002
@@ -294,7 +344,7 @@ def convert(
294344
if callable(raw_target):
295345
return raw_target(item)
296346
msg = f"Can't convert target {target}"
297-
raise AssertionError(msg)
347+
raise TypeError(msg)
298348

299349
def unrecognized_options(self, options: object) -> Generator[str, None, None]:
300350
if not self.verify:
@@ -363,7 +413,7 @@ def convert(cls, item: Any, target: type[Any]) -> object:
363413
if callable(raw_target):
364414
return raw_target(item)
365415
msg = f"Can't convert target {target}"
366-
raise AssertionError(msg)
416+
raise TypeError(msg)
367417

368418
def unrecognized_options(self, options: object) -> Generator[str, None, None]:
369419
yield from _unrecognized_dict(self.settings, options, self.prefixes)
@@ -397,11 +447,6 @@ def get_item(self, *fields: str, is_dict: bool) -> Any:
397447
msg = f"{fields!r} not found in any source"
398448
raise KeyError(msg)
399449

400-
@classmethod
401-
def convert(cls, item: Any, target: type[T]) -> T: # noqa: ARG003
402-
msg = "SourceChain cannot convert items, use the result from has_item"
403-
raise NotImplementedError(msg)
404-
405450
def convert_target(self, target: type[T], *prefixes: str) -> T:
406451
"""
407452
Given a dataclass type, create an object of that dataclass filled
@@ -467,6 +512,8 @@ def unrecognized_options(self, options: object) -> Generator[str, None, None]:
467512
for source in self.sources:
468513
yield from source.unrecognized_options(options)
469514

470-
def all_option_names(self, target: type[Any]) -> Iterator[str]:
471-
for source in self.sources:
472-
yield from source.all_option_names(target)
515+
516+
if typing.TYPE_CHECKING:
517+
_: Source = typing.cast(EnvSource, None)
518+
_ = typing.cast(ConfSource, None)
519+
_ = typing.cast(TOMLSource, None)

0 commit comments

Comments
 (0)