whence¶
Typed configuration that remembers where it came from.
Every configuration library can tell you a value. whence can tell you why it
has that value — which file, which line, which profile, and what it overrode.
$ whence myapp explain db.host --profile prod
db.host = 'db.internal'
<- config/app.prod.yaml:4:9 [profile=prod]
shadowed:
overrides - not set
env - not set
dotenv:.env - not found
secrets-dir - not found
config/app.yaml:2:9 = 'localhost'
defaults = 'localhost'
Spring has this (Origin, /actuator/env), HOCON has it (ConfigOrigin), Rust's
figment has it (Metadata). Python has not — so that is what whence is for. It
is a complement to pydantic, not a replacement: point it at a pydantic model
or a frozen dataclass and it handles the layering, the provenance and the
diagnostics.
Install¶
pip install whence # env, .env, TOML, JSON, .properties, XML
pip install whence[yaml] # + YAML
The core has no runtime dependencies. Every format but YAML is parsed on the standard library.
Quick start¶
from dataclasses import dataclass
from whence import Config, Secret, load_settings, settings
@settings(prefix="db")
@dataclass(frozen=True, slots=True)
class Db:
host: str = "localhost"
port: int = 5432
password: Secret | None = None
cfg = Config.load(app="myapp", profiles=["prod"])
db = load_settings(Db, cfg)
Loading is synchronous. Configuration is read once, before the application
runs, so there is no event loop for an await to yield to; a Source that
reaches the network blocks the startup it is already part of.
Receiving configuration¶
The code above fetches: it holds a Config and binds. @from_config is the
other direction, for the edges where a framework calls your function and there
is no call site to thread a Config through:
from typing import Annotated
from whence import Injected, Value, current_config, from_config
@from_config
def connect(
*,
db: Annotated[Db, Injected],
retries: Annotated[int, Value("http.retries")] = 3,
) -> Conn: ...
with current_config(cfg):
connect() # db bound from the `db` subtree, retries from the key
connect(retries=1) # an explicit argument always wins
Both markers live in Annotated, so the annotation stays exact and the default
stays a real default — retries is 3 when nothing sets the key, and a call that
supplies every marked argument needs no ambient configuration at all. The
ambient value is a ContextVar, so it is per-task rather than per-process: one
tenant's configuration per request, concurrently, and nothing leaking into a
worker thread that should have been passed a Config explicitly.
A marked parameter must be keyword-only, which is checked at decoration —
filling happens through kwargs, so an argument passed positionally would be
filled twice.
Precedence¶
Seven named layers, highest first. whence <app> explain <key> prints them.
| Layer | Source |
|---|---|
overrides |
values passed to Config.load(overrides=...) |
cli |
--set a.b=c in sys.argv |
env |
MYAPP_DB__HOST, plus _FILE indirection |
dotenv |
.env.{profile}, then .env |
secrets-dir |
/run/secrets, one file per key |
files |
app.{profile}.{ext}, then app.{ext} |
defaults |
schema defaults |
The secrets directory sits above configuration files, unlike pydantic-settings: a secret an operator deliberately mounted is deployment truth, not a fallback.
Discovery¶
Nothing about where configuration lives is hard-coded. Five steps, each
reported by whence <app> discovery even when it finds nothing:
- an explicit
file=— missing is an error $MYAPP_CONFIG- each root in
path - the per-user configuration directory
pyproject.toml→[tool.myapp]
Discovery(
app="myapp",
path=(Path("config"), Path("/etc/myapp")),
formats=("toml", "yaml"),
prefix="MYAPP_",
search_parents=True,
mode="layer",
)
Two files matching in the same root raise AmbiguousConfigError rather than
resolving by preference — the case Spring left unspecified for years. Across
different roots there is no ambiguity: the earlier root wins, and both appear in
explain.
Profiles¶
Profile files overlay the base file, profile groups expand, and the invariant is that profiles gate documents; they never reorder sources. A profiled key in a low-precedence source can never outrank an unprofiled key in a higher one — the rule that eliminates the "why did dev configuration win in production?" class of incident.
Interpolation¶
${a.b}, ${a.b:-default} (POSIX :-, so ${url:-redis://h:6379} is
unambiguous), ${?a.b} (the key disappears when undefined), ${env:VAR},
${file:/run/secrets/pw}, and \${literal}. Resolved lazily after the merge,
so a base file can reference a key a higher source supplies. There is no global
"ignore unresolvable" switch: that is the setting that turns a startup failure
into a literal ${db.password} reaching a database driver.
Errors¶
Every problem in one run, each carrying its origin:
whence.BindError: 2 errors
Property: db.port
Value: 'eighty'
Origin: config/app.prod.yaml:4:9 [profile=prod]
Reason: could not convert to int
Shadowed: config/app.yaml:5:9 = 5432
Property: db.max_retires
Value: 3
Origin: config/app.yaml:8:3
Reason: no such setting - did you mean 'db.max_retries'?
Action: correct the configuration, or run `whence explain <key>`.
No message ever contains the value it rejected — a rejected value may be a secret, and an exception is the most widely logged object in a program.
Reloading¶
There isn't any. To change configuration, redeploy.
Config is immutable and loaded once. A library that also owns a poll loop owns
a thread or an event loop, and the generational swap it needs is where every
prior art went wrong: .NET's ChangeToken.OnChange has fired twice per save
since 2017 and ships a hash-and-backoff workaround in its own documentation, Go
viper's WatchConfig carries a documented data race, and koanf's Watch() is
not safe against concurrent reads. Each of those is a property of mutating an
object other code is holding — so whence never holds one.
A fresh Config.load(...) does see the current state of the world, including
through a Kubernetes ConfigMap's ..data symlink. kubelet republishes by
pointing that symlink at a new timestamped directory; an inotify watch on the
config file is bound to an inode that has been unlinked and goes permanently
deaf, which is the single most common way this is got wrong — Python's watchdog
fails it silently because it hardcodes IN_DONT_FOLLOW. Reading resolves the
symlink afresh every time. A container test builds kubelet's exact layout with
real symlinks and asserts the swap is seen (make test-docker).
If a running process must pick up a change, call Config.load(...) again on a
schedule you own and swap your own pointer. That is a handful of lines, it lives
where your concurrency model already is, and whence stays out of it.
Cross-platform¶
Configuration directories follow each platform's own convention: XDG on Linux,
~/Library/Application Support on macOS (plus XDG when explicitly set),
%LOCALAPPDATA% then %APPDATA% on Windows. Windows folds environment variable
names to upper case before any library code runs, and whence's naming scheme is
bijective under that fold, so the same code and the same tests behave
identically everywhere. Files are read as UTF-8 with a BOM tolerated, CRLF
parses, and case-insensitive filesystems do not produce phantom ambiguities.
API reference¶
whence ¶
whence -- typed configuration that remembers where it came from.
Every configuration library can tell you a value. whence can tell you why it has that value: which file, which line, which profile, and what it overrode.
>>> from whence import Config
>>> cfg = Config.from_mapping({"db": {"host": "localhost"}})
>>> cfg.get("db.host")
'localhost'
Loading is synchronous and happens once, before the application runs: there is
no event loop to protect at that point, so there is nothing for an await to
yield to. A source that reaches the network simply blocks the startup it is
already part of.
The public API is everything listed in __all__; anything else is internal
and may change without a major version bump.
Injected
module-attribute
¶
Injected = _Inject()
Annotation metadata: bind this parameter's own type from configuration.
Annotated[Db, Injected] binds Db from the subtree its @settings
prefix names. Binding always happens, so a default on such a parameter would be
unreachable -- @from_config rejects one at decoration rather than ignoring
it.
Binder ¶
Bases: Protocol
Turns a subtree of resolved configuration into a typed object.
Source code in src/whence/binding/__init__.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
supports ¶
supports(target: type) -> bool
Report whether this binder handles the target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
type
|
The schema class. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when it does. |
Source code in src/whence/binding/__init__.py
99 100 101 102 103 104 105 106 107 108 | |
declared ¶
declared(
target: type, prefix: KeyPath = ()
) -> set[KeyPath]
List the key paths the target declares.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
type
|
The schema class. |
required |
prefix
|
KeyPath
|
The path it sits at. |
()
|
Returns:
| Type | Description |
|---|---|
set[KeyPath]
|
Every declared key path. |
Source code in src/whence/binding/__init__.py
110 111 112 113 114 115 116 117 118 119 120 | |
bind ¶
bind(
target: type,
values: Mapping[KeyPath, Tracked],
prefix: KeyPath = (),
problems: list[Any] | None = None,
) -> Any
Construct the target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
type
|
The schema class. |
required |
values
|
Mapping[KeyPath, Tracked]
|
Flat resolved values. |
required |
prefix
|
KeyPath
|
The subtree to read. |
()
|
problems
|
list[Any] | None
|
A list to append problems to. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The instance, or |
Source code in src/whence/binding/__init__.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
Problem
dataclass
¶
One thing wrong with the configuration.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
The dotted key. |
value |
Any
|
The offending value, redacted before rendering. |
origin |
Origin | None
|
Where the value came from. |
reason |
str
|
What is wrong with it. |
shadowed |
tuple[str, ...]
|
Entries this value overrode, for context. |
Source code in src/whence/binding/__init__.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
SourceChain ¶
A mutable, ordered collection of named sources, highest precedence first.
Source code in src/whence/chain.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
__init__ ¶
__init__(sources: Sequence[Source] = ()) -> None
Build a chain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sources
|
Sequence[Source]
|
Sources in precedence order, highest first. |
()
|
Source code in src/whence/chain.py
27 28 29 30 31 32 33 | |
__iter__ ¶
__iter__() -> Iterator[Source]
Iterate sources highest precedence first.
Source code in src/whence/chain.py
35 36 37 | |
__len__ ¶
__len__() -> int
Return the number of sources.
Source code in src/whence/chain.py
39 40 41 | |
__contains__ ¶
__contains__(name: object) -> bool
Report whether a source with this name is present.
Source code in src/whence/chain.py
43 44 45 | |
names ¶
names() -> tuple[str, ...]
List source names, highest precedence first.
Returns:
| Type | Description |
|---|---|
tuple[str, ...]
|
The names. |
Source code in src/whence/chain.py
52 53 54 55 56 57 58 | |
add_first ¶
add_first(source: Source) -> SourceChain
Insert a source at the highest precedence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
The source. |
required |
Returns:
| Type | Description |
|---|---|
SourceChain
|
This chain, for chaining. |
Source code in src/whence/chain.py
72 73 74 75 76 77 78 79 80 81 82 | |
add_last ¶
add_last(source: Source) -> SourceChain
Append a source at the lowest precedence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
The source. |
required |
Returns:
| Type | Description |
|---|---|
SourceChain
|
This chain, for chaining. |
Source code in src/whence/chain.py
84 85 86 87 88 89 90 91 92 93 94 | |
add_before ¶
add_before(relative: str, source: Source) -> SourceChain
Insert a source immediately above a named one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
relative
|
str
|
The name to insert above. |
required |
source
|
Source
|
The source. |
required |
Returns:
| Type | Description |
|---|---|
SourceChain
|
This chain, for chaining. |
Source code in src/whence/chain.py
96 97 98 99 100 101 102 103 104 105 106 107 | |
add_after ¶
add_after(relative: str, source: Source) -> SourceChain
Insert a source immediately below a named one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
relative
|
str
|
The name to insert below. |
required |
source
|
Source
|
The source. |
required |
Returns:
| Type | Description |
|---|---|
SourceChain
|
This chain, for chaining. |
Source code in src/whence/chain.py
109 110 111 112 113 114 115 116 117 118 119 120 | |
replace ¶
replace(name: str, source: Source) -> Source
Swap a source in place, keeping its precedence.
This is how a source is decorated rather than displaced -- wrapping the environment source to decrypt values, for instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The source to replace. |
required |
source
|
Source
|
The replacement. |
required |
Returns:
| Type | Description |
|---|---|
Source
|
The source that was removed. |
Source code in src/whence/chain.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
remove ¶
remove(name: str) -> Source
Drop a source by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The source to remove. |
required |
Returns:
| Type | Description |
|---|---|
Source
|
The source that was removed. |
Source code in src/whence/chain.py
140 141 142 143 144 145 146 147 148 149 | |
insert_by_ordinal ¶
insert_by_ordinal(
source: Source, ordinal: int
) -> SourceChain
Insert a source by integer rank, higher winning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
The source, which must carry an |
required |
ordinal
|
int
|
The rank. |
required |
Returns:
| Type | Description |
|---|---|
SourceChain
|
This chain, for chaining. |
Source code in src/whence/chain.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
load ¶
load() -> Resolved
Load every source and merge the layers.
Returns:
| Type | Description |
|---|---|
Resolved
|
The winning values, the shadow chain, and every layer consulted. |
Source code in src/whence/chain.py
169 170 171 172 173 174 175 176 | |
Config ¶
Resolved configuration, plus the provenance of everything in it.
A Config is immutable. Reloading builds a new one and swaps it, which is
the only design that cannot expose a half-updated object: .NET's in-place
IOptionsMonitor fires twice per save and Go viper's watcher carries a
documented data race, and both problems are properties of mutating in place.
Source code in src/whence/config.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | |
values
property
¶
values: Mapping[KeyPath, Tracked]
The winning tracked value for every key.
Returns:
| Type | Description |
|---|---|
Mapping[KeyPath, Tracked]
|
Flat key paths to tracked values. |
__init__ ¶
__init__(
resolved: Resolved,
*,
plan: DiscoveryPlan | None = None,
profiles: Sequence[str] = (),
discovery: Discovery | None = None,
) -> None
Wrap an already-resolved merge.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolved
|
Resolved
|
The merged values and shadow records. |
required |
plan
|
DiscoveryPlan | None
|
The discovery plan that produced the file sources. |
None
|
profiles
|
Sequence[str]
|
The active profiles, in increasing precedence. |
()
|
discovery
|
Discovery | None
|
The discovery settings used. |
None
|
Source code in src/whence/config.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
load
classmethod
¶
load(
app: str | None = None,
*,
discovery: Discovery | None = None,
profiles: Sequence[str] | None = None,
groups: Mapping[str, Sequence[str]] | None = None,
overrides: Mapping[str, Any] | None = None,
argv: Sequence[str] | None = None,
defaults: Mapping[str, Any] | None = None,
environ: Mapping[str, str] | None = None,
aliases: Mapping[str, str] | None = None,
cwd: Path | None = None,
platform: Platform | None = None,
expand: bool = True,
) -> Config
Discover, load and merge every source.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app
|
str | None
|
The application name; shorthand for |
None
|
discovery
|
Discovery | None
|
Full discovery settings, if |
None
|
profiles
|
Sequence[str] | None
|
Active profiles; falls back to the profiles variable. |
None
|
groups
|
Mapping[str, Sequence[str]] | None
|
Profile group definitions. |
None
|
overrides
|
Mapping[str, Any] | None
|
Values that outrank every source. |
None
|
argv
|
Sequence[str] | None
|
Command-line arguments to scan for |
None
|
defaults
|
Mapping[str, Any] | None
|
Values every source outranks. |
None
|
environ
|
Mapping[str, str] | None
|
The environment to read; defaults to |
None
|
aliases
|
Mapping[str, str] | None
|
An explicit map from variable name to dotted key, for the cases convention does not cover. An explicit, greppable table beats implicit name mangling. |
None
|
cwd
|
Path | None
|
The directory discovery starts from. |
None
|
platform
|
Platform | None
|
Override the platform, for cross-platform tests. |
None
|
expand
|
bool
|
Whether to expand |
True
|
Returns:
| Type | Description |
|---|---|
Config
|
The resolved configuration. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If discovery or any source fails. |
Source code in src/whence/config.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
from_mapping
classmethod
¶
from_mapping(
data: Mapping[str, Any], *, name: str = "overrides"
) -> Config
Build a configuration from a single mapping, for tests and defaults.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Mapping[str, Any]
|
Nested or flat configuration values. |
required |
name
|
str
|
The layer name. |
'overrides'
|
Returns:
| Type | Description |
|---|---|
Config
|
The resolved configuration. |
Source code in src/whence/config.py
151 152 153 154 155 156 157 158 159 160 161 162 163 | |
__contains__ ¶
__contains__(key: str) -> bool
Report whether a key has a value.
Source code in src/whence/config.py
180 181 182 | |
__getitem__ ¶
__getitem__(key: str) -> Any
Return a value, raising if it is absent.
Source code in src/whence/config.py
184 185 186 | |
get ¶
get(
key: str, default: Any = None, *, type_: Any = None
) -> Any
Return a value, or a default.
The second positional argument is the default, as it is on dict and
os.environ: cfg.get("db.port", 5432) means what it looks like.
Coercion is the named option, because asking for it is the rarer case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A dotted key. |
required |
default
|
Any
|
Returned when the key is absent. Never coerced -- it is already a Python value. |
None
|
type_
|
Any
|
Coerce to this type when given. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The value, coerced if asked. |
Source code in src/whence/config.py
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
require ¶
require(key: str, *, type_: Any = None) -> Any
Return a value, raising a helpful error if it is absent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A dotted key. |
required |
type_
|
Any
|
Coerce to this type when given. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The value. |
Raises:
| Type | Description |
|---|---|
MissingKeyError
|
If nothing supplies the key. |
Source code in src/whence/config.py
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
origin ¶
origin(key: str) -> Origin | None
Return where a value came from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A dotted key. |
required |
Returns:
| Type | Description |
|---|---|
Origin | None
|
Its origin, or |
Source code in src/whence/config.py
231 232 233 234 235 236 237 238 239 240 241 | |
explain ¶
explain(key: str) -> str
Describe where a value came from and what it overrode.
Every source is listed, including the ones that had nothing to say, because absence is information: "the environment variable is not set" is usually the answer someone is looking for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A dotted key. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A multi-line report. |
Source code in src/whence/config.py
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | |
discovery_report ¶
discovery_report() -> str
Describe how configuration files were searched for.
Returns:
| Type | Description |
|---|---|
str
|
A multi-line report, or a note that discovery was not run. |
Source code in src/whence/config.py
293 294 295 296 297 298 299 300 301 302 303 304 305 | |
dump ¶
dump(*, reveal: bool = False) -> dict[str, Any]
Return every value, redacted unless asked otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reveal
|
bool
|
Skip redaction. Only ever pass this deliberately. |
False
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dotted keys to values. |
Source code in src/whence/config.py
307 308 309 310 311 312 313 314 315 316 317 318 319 320 | |
as_dict ¶
as_dict() -> dict[str, Any]
Return the resolved values as a plain nested dictionary.
Provenance is dropped, which is the point: this is the shape to hand to something that only wants the data, such as a validator whence has no binder for.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Nested plain Python values. |
Source code in src/whence/config.py
322 323 324 325 326 327 328 329 330 331 332 | |
origins ¶
origins() -> dict[str, Origin]
Return the origin of every value.
Returns:
| Type | Description |
|---|---|
dict[str, Origin]
|
Dotted keys to origins. |
Source code in src/whence/config.py
334 335 336 337 338 339 340 | |
bind ¶
bind(
target: type,
*,
prefix: str | KeyPath = (),
strict: bool | None = None,
) -> Any
Bind a subtree onto a typed schema.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
type
|
A dataclass, or a pydantic model when pydantic is installed. |
required |
prefix
|
str | KeyPath
|
The subtree to read; defaults to the schema's own
|
()
|
strict
|
bool | None
|
Whether a key nothing declared is an error. Defaults to
whatever |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
An instance of |
Raises:
| Type | Description |
|---|---|
BindError
|
If anything failed, listing every problem at once. |
Source code in src/whence/config.py
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | |
with_fallback ¶
with_fallback(other: Config) -> Config
Layer another configuration underneath this one.
This is how a library ships defaults that an application can override.
HOCON calls it withFallback, and figment separates it from merge
because "replace" and "fill in the gaps" are genuinely different
operations. whence has both: the source chain merges, and this fills
gaps -- a key already set here keeps its value and its origin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Config
|
The configuration to fall back to. |
required |
Returns:
| Type | Description |
|---|---|
Config
|
A new configuration. |
Source code in src/whence/config.py
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 | |
__repr__ ¶
__repr__() -> str
Summarise without printing any values.
Source code in src/whence/config.py
400 401 402 403 404 405 | |
Value
dataclass
¶
Annotation metadata: fill this parameter from one configuration key.
Written inside :data:~typing.Annotated, so the parameter keeps both its
real type and its real default::
retries: Annotated[int, Value("http.retries")] = 3
A marked parameter with no default is required: a missing key raises
:class:~whence.MissingKeyError rather than passing None on.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
The dotted key to read. |
Source code in src/whence/decorators.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
Discovery
dataclass
¶
Declarative configuration discovery.
Attributes:
| Name | Type | Description |
|---|---|---|
app |
str
|
The application name. Every other default derives from it. |
file |
Path | None
|
An explicit configuration file. Wins outright, and must exist. |
path |
tuple[Path, ...]
|
Search roots, highest precedence first. |
formats |
tuple[str, ...]
|
Suffixes to look for, in preference order. Restricting this is
how a project bans a format: a stray |
prefix |
str | None
|
The environment variable prefix. |
env_var |
str | None
|
The variable naming an explicit config file. |
profiles_var |
str | None
|
The variable listing active profiles. |
pyproject_table |
str | None
|
The dotted table to read from |
user_config |
bool
|
Whether to search the per-user configuration directory. |
search_parents |
bool
|
Walk search roots upward, for monorepos. |
boundary |
tuple[str, ...]
|
Marker names that stop the upward walk. |
mode |
Literal['layer', 'first']
|
|
on_missing |
Literal['ok', 'error']
|
|
dotenv |
tuple[Path, ...]
|
|
secrets_dir |
Path | None
|
A key-per-file secrets directory, or |
Source code in src/whence/discovery.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | |
env_prefix
property
¶
env_prefix: str
The environment prefix, derived from app when unset.
Returns:
| Type | Description |
|---|---|
str
|
|
config_var
property
¶
config_var: str
The variable naming an explicit config file.
Returns:
| Type | Description |
|---|---|
str
|
|
profiles_env
property
¶
profiles_env: str
The variable listing active profiles.
Returns:
| Type | Description |
|---|---|
str
|
|
table
property
¶
table: str
The pyproject.toml table to read.
Returns:
| Type | Description |
|---|---|
str
|
|
__post_init__ ¶
__post_init__() -> None
Validate the prefix before anything can depend on it.
Source code in src/whence/discovery.py
140 141 142 | |
with_ ¶
with_(**changes: object) -> Discovery
Return a copy with fields replaced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**changes
|
object
|
Fields to override. |
{}
|
Returns:
| Type | Description |
|---|---|
Discovery
|
A new discovery object. |
Source code in src/whence/discovery.py
180 181 182 183 184 185 186 187 188 189 | |
roots ¶
roots(cwd: Path | None = None) -> tuple[Path, ...]
Resolve the search roots, expanding the upward walk.
With search_parents, each relative root is also looked for in every
parent directory up to and including the first one holding a
boundary marker. In a uv workspace, where a test run may start in
libs/<member>/ while the configuration sits at the repository root,
this is the difference between working and not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cwd
|
Path | None
|
The directory to start from; defaults to the process's. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Path, ...]
|
Roots, nearest first. |
Source code in src/whence/discovery.py
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
stems ¶
stems(
profiles: Sequence[str] = (),
) -> tuple[tuple[str, str | None], ...]
List the file stems to look for, highest precedence first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profiles
|
Sequence[str]
|
Active profiles, in increasing precedence. |
()
|
Returns:
| Type | Description |
|---|---|
tuple[str, str | None]
|
|
...
|
every profile outranks the base file. |
Source code in src/whence/discovery.py
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | |
plan ¶
plan(
*,
profiles: Sequence[str] = (),
environ: Mapping[str, str] | None = None,
cwd: Path | None = None,
platform: Platform | None = None,
) -> DiscoveryPlan
Run the five-step chain and report exactly what happened.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profiles
|
Sequence[str]
|
Active profiles, in increasing precedence. |
()
|
environ
|
Mapping[str, str] | None
|
The environment to consult. |
None
|
cwd
|
Path | None
|
The directory to start from. |
None
|
platform
|
Platform | None
|
Override the platform, for cross-platform tests. |
None
|
Returns:
| Type | Description |
|---|---|
DiscoveryPlan
|
The plan: every step, and every file found. |
Raises:
| Type | Description |
|---|---|
MissingConfigError
|
If an explicitly named file is absent, or
|
AmbiguousConfigError
|
If one root holds two matching files. |
Source code in src/whence/discovery.py
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 | |
file_sources ¶
file_sources(plan: DiscoveryPlan) -> list[FileSource]
Turn a plan into file sources, highest precedence first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plan
|
DiscoveryPlan
|
A plan produced by :meth: |
required |
Returns:
| Type | Description |
|---|---|
list[FileSource]
|
One source per discovered file. |
Source code in src/whence/discovery.py
366 367 368 369 370 371 372 373 374 375 | |
DiscoveryPlan
dataclass
¶
The outcome of running discovery: what was searched, and what was found.
Attributes:
| Name | Type | Description |
|---|---|---|
steps |
tuple[Step, ...]
|
Every step, in order, including the ones that found nothing. |
files |
tuple[tuple[Path, str | None], ...]
|
Every discovered file, highest precedence first. |
Source code in src/whence/discovery.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
render ¶
render() -> str
Format the plan the way whence explain --discovery prints it.
Returns:
| Type | Description |
|---|---|
str
|
A multi-line report. |
Source code in src/whence/discovery.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
PyProjectSource ¶
Reads a dotted table out of pyproject.toml.
The lowest file layer: a project's own committed defaults, which every other source is entitled to override.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path | str
|
The |
'pyproject.toml'
|
table
|
str
|
The dotted table name, such as |
''
|
name
|
str
|
The layer name. |
'pyproject'
|
Source code in src/whence/discovery.py
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 | |
__init__ ¶
__init__(
path: Path | str = "pyproject.toml",
table: str = "",
*,
name: str = "pyproject",
) -> None
Build a pyproject source.
Source code in src/whence/discovery.py
390 391 392 393 394 395 396 | |
load ¶
load() -> Layer
Read the table, if the file and the table both exist.
Returns:
| Type | Description |
|---|---|
Layer
|
The layer, with |
Raises:
| Type | Description |
|---|---|
FormatError
|
If |
Source code in src/whence/discovery.py
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 | |
Step
dataclass
¶
One step of the discovery chain, and what it found.
Attributes:
| Name | Type | Description |
|---|---|---|
index |
int
|
The step number, 1 through 5. |
label |
str
|
A human-readable description of where it looked. |
paths |
tuple[Path, ...]
|
Files it contributed, highest precedence first. |
note |
str
|
Why it contributed nothing, when it did not. |
Source code in src/whence/discovery.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
found
property
¶
found: bool
Whether this step contributed anything.
Returns:
| Type | Description |
|---|---|
bool
|
True when at least one file was found. |
AmbiguousConfigError ¶
Bases: ConfigError
Two files in one search root both satisfy the same logical name.
Raised rather than resolved by preference, because silently picking one is the failure mode that took Spring Boot years to specify.
Source code in src/whence/errors.py
35 36 37 38 39 40 | |
BindError ¶
Bases: ConfigError
One or more values could not be bound to the target schema.
Source code in src/whence/errors.py
63 64 | |
ConfigError ¶
Bases: WhenceError
The configuration itself is wrong: bad discovery, bad wiring, bad source.
Source code in src/whence/errors.py
27 28 | |
FormatError ¶
Bases: ConfigError
A configuration file could not be parsed.
Source code in src/whence/errors.py
43 44 | |
InterpolationError ¶
Bases: ConfigError
A ${...} placeholder could not be resolved.
Source code in src/whence/errors.py
55 56 | |
MissingConfigError ¶
Bases: ConfigError
A configuration file that was named explicitly does not exist.
Source code in src/whence/errors.py
31 32 | |
MissingKeyError ¶
Bases: WhenceError, KeyError
A required key is absent from every source.
Source code in src/whence/errors.py
47 48 49 50 51 52 | |
__str__ ¶
__str__() -> str
Render without KeyError's surrounding quotes.
Source code in src/whence/errors.py
50 51 52 | |
SecretError ¶
Bases: WhenceError
A secret was read outside an explicit unlock_secrets() scope.
Source code in src/whence/errors.py
67 68 | |
UnboundKeyError ¶
Bases: ConfigError
Configuration supplied a key that the target schema does not declare.
Source code in src/whence/errors.py
59 60 | |
WhenceError ¶
Bases: Exception
Base class for every error whence raises.
Source code in src/whence/errors.py
23 24 | |
Origin
dataclass
¶
Where a single configuration value came from.
Attributes:
| Name | Type | Description |
|---|---|---|
source |
str
|
The name of the source that produced it, such as |
locator |
str
|
The concrete location within that source: a file path, an
environment variable name, or |
line |
int | None
|
One-based line number, when the parser reports positions. |
column |
int | None
|
One-based column number, when the parser reports positions. |
profile |
str | None
|
The profile whose overlay contributed the value, if any. |
parent |
Origin | None
|
The origin this one was derived from -- the variable behind a
|
Source code in src/whence/origin.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
__str__ ¶
__str__() -> str
Render as locator:line:column [profile=...].
Source code in src/whence/origin.py
38 39 40 41 42 43 44 45 46 47 48 49 | |
derived ¶
derived(**changes: Any) -> Origin
Return a copy that records this origin as its parent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**changes
|
Any
|
Fields to override on the copy. |
{}
|
Returns:
| Type | Description |
|---|---|
Origin
|
A new origin whose |
Source code in src/whence/origin.py
51 52 53 54 55 56 57 58 59 60 | |
RelativePath ¶
Bases: Path
A path resolved against the file that declared it, not the process cwd.
cert = "./ca.pem" inside deploy/app.yaml means deploy/ca.pem,
wherever the program happens to be run from. That is only expressible
because the value remembers where it came from; figment calls the same idea
RelativePathBuf and nothing in Python offers it.
An absolute value is left alone, and a value from a source with no file behind it -- an environment variable, say -- is resolved against the cwd, because there is nothing better to resolve it against.
Source code in src/whence/origin.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
resolve_against
classmethod
¶
resolve_against(
value: object, origin: Origin | None
) -> Path
Resolve a path value against its origin's directory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The raw path value. |
required |
origin
|
Origin | None
|
Where the value came from. |
required |
Returns:
| Type | Description |
|---|---|
Path
|
An absolute path where the origin names a file, and the value |
Path
|
unchanged otherwise. |
Source code in src/whence/origin.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
Tracked
dataclass
¶
A value paired with its origin.
Equality and hashing delegate to the wrapped value on purpose. That is what
makes provenance retrofittable instead of viral: every consumer that
compares, hashes or formats a value keeps working unchanged, and only code
that explicitly asks for .origin pays any attention to it. A sidecar
{key: origin} map loses provenance at every transformation, and a str
subclass loses it at the first f-string.
Attributes:
| Name | Type | Description |
|---|---|---|
value |
Any
|
The underlying value. |
origin |
Origin
|
Where it came from. |
Source code in src/whence/origin.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
__eq__ ¶
__eq__(other: object) -> bool
Compare against the wrapped value, tracked or not.
Source code in src/whence/origin.py
82 83 84 | |
__hash__ ¶
__hash__() -> int
Hash as the wrapped value does.
Source code in src/whence/origin.py
86 87 88 | |
__str__ ¶
__str__() -> str
Render as the wrapped value does.
Source code in src/whence/origin.py
90 91 92 | |
__repr__ ¶
__repr__() -> str
Show the value and its origin.
Source code in src/whence/origin.py
94 95 96 | |
__bool__ ¶
__bool__() -> bool
Report the truthiness of the wrapped value.
Source code in src/whence/origin.py
98 99 100 | |
Secret ¶
A string that will not reveal itself by accident.
repr and str both render the mask, so a secret cannot reach a log
line, a traceback or an f-string through inattention. Reading the real value
needs :func:unlock_secrets, which makes the intent explicit and greppable.
Source code in src/whence/secret.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
__init__ ¶
__init__(value: str) -> None
Wrap a string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
The sensitive value. |
required |
Source code in src/whence/secret.py
51 52 53 54 55 56 57 | |
reveal ¶
reveal() -> str
Return the underlying string.
Returns:
| Type | Description |
|---|---|
str
|
The real value. |
Raises:
| Type | Description |
|---|---|
SecretError
|
If called outside an |
Source code in src/whence/secret.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
__repr__ ¶
__repr__() -> str
Render the mask, never the value.
Source code in src/whence/secret.py
76 77 78 | |
__str__ ¶
__str__() -> str
Render the mask, never the value.
Source code in src/whence/secret.py
80 81 82 | |
__eq__ ¶
__eq__(other: object) -> bool
Compare two secrets without revealing either.
Source code in src/whence/secret.py
84 85 86 87 88 | |
__hash__ ¶
__hash__() -> int
Hash the underlying value.
Source code in src/whence/secret.py
90 91 92 | |
__bool__ ¶
__bool__() -> bool
Report whether the underlying string is non-empty.
Source code in src/whence/secret.py
94 95 96 | |
ArgvSource ¶
Reads --set key=value pairs out of an argument list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
argv
|
Sequence[str] | None
|
The arguments to scan; defaults to |
None
|
name
|
str
|
The layer name. |
'cli'
|
flag
|
str
|
The flag to recognise, if |
FLAG
|
Source code in src/whence/sources/argv.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
__init__ ¶
__init__(
argv: Sequence[str] | None = None,
*,
name: str = "cli",
flag: str = FLAG,
) -> None
Build a command-line source.
Source code in src/whence/sources/argv.py
33 34 35 36 37 38 39 40 41 42 43 | |
load ¶
load() -> Layer
Collect every --set pair.
Returns:
| Type | Description |
|---|---|
Layer
|
The layer, with |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If a pair is malformed. |
Source code in src/whence/sources/argv.py
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
DotEnvSource ¶
Reads a .env file and maps its variables like the environment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path | str
|
The file to read. |
required |
prefix
|
str
|
The environment prefix, applied exactly as |
''
|
name
|
str
|
The layer name. |
'dotenv'
|
aliases
|
Mapping[str, str] | None
|
Explicit variable-to-key overrides. |
None
|
Source code in src/whence/sources/dotenv.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | |
__init__ ¶
__init__(
path: Path | str,
prefix: str = "",
*,
name: str = "dotenv",
aliases: Mapping[str, str] | None = None,
) -> None
Build a .env source.
Source code in src/whence/sources/dotenv.py
143 144 145 146 147 148 149 150 151 152 153 154 155 | |
load ¶
load() -> Layer
Read the file, if it exists.
Returns:
| Type | Description |
|---|---|
Layer
|
The layer, with |
Raises:
| Type | Description |
|---|---|
FormatError
|
If the file exists but cannot be parsed. |
Source code in src/whence/sources/dotenv.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | |
EnvSource ¶
Reads configuration from environment variables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
The literal prefix every variable carries. Applied to the
environment only -- files are never prefixed, matching Spring's
|
''
|
environ
|
Mapping[str, str] | None
|
The environment to read; defaults to |
None
|
name
|
str
|
The layer name. |
'env'
|
aliases
|
Mapping[str, str] | None
|
An explicit map from variable name to dotted key, for the cases
convention does not cover ( |
None
|
read_files
|
bool
|
Whether to honour the |
True
|
exclude
|
Collection[str]
|
Variables that control how configuration is found rather than
carrying any of it -- |
()
|
Source code in src/whence/sources/env.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
__init__ ¶
__init__(
prefix: str = "",
*,
environ: Mapping[str, str] | None = None,
name: str = "env",
aliases: Mapping[str, str] | None = None,
read_files: bool = True,
exclude: Collection[str] = (),
) -> None
Build an environment source.
Source code in src/whence/sources/env.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
name_for ¶
name_for(path: KeyPath) -> str
Return the one variable name that maps to a key path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
KeyPath
|
The canonical key path. |
required |
Returns:
| Type | Description |
|---|---|
str
|
|
Source code in src/whence/sources/env.py
74 75 76 77 78 79 80 81 82 83 | |
load ¶
load() -> Layer
Read every matching variable, resolving _FILE indirection.
Returns:
| Type | Description |
|---|---|
Layer
|
The layer. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If both |
Source code in src/whence/sources/env.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | |
FileSource ¶
Loads one configuration file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path | str
|
The file to read. |
required |
name
|
str | None
|
The layer name; defaults to the path itself, which is what makes
|
None
|
optional
|
bool
|
When false, a missing file raises. An explicitly named file that does not exist is always a mistake; a discovered one is not. |
True
|
profile
|
str | None
|
The profile this file belongs to, recorded in every origin it produces. |
None
|
loaders
|
Mapping[str, Loader] | None
|
A suffix-to-loader table; defaults to the built-ins. |
None
|
Source code in src/whence/sources/files.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
__init__ ¶
__init__(
path: Path | str,
*,
name: str | None = None,
optional: bool = True,
profile: str | None = None,
loaders: Mapping[str, Loader] | None = None,
) -> None
Build a file source.
Source code in src/whence/sources/files.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | |
load ¶
load() -> Layer
Read and parse the file.
Returns:
| Type | Description |
|---|---|
Layer
|
The layer, with |
Raises:
| Type | Description |
|---|---|
MissingConfigError
|
If a required file does not exist. |
FormatError
|
If the file cannot be parsed. |
Source code in src/whence/sources/files.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
MappingSource ¶
A layer built from a Python mapping.
Used for the two ends of the chain -- explicit overrides at the top and
schema defaults at the bottom. Defaults are a source rather than a property
of the target object on purpose: it means get, dump and explain
all agree about what the configuration says. Spring's equivalent split,
where @Value cannot see a bean's own defaults, is a genuine bug.
Source code in src/whence/sources/mapping.py
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
__init__ ¶
__init__(
data: Mapping[str, object] | Mapping[KeyPath, object],
*,
name: str = "overrides",
locator: str | None = None,
) -> None
Build a source from a flat or nested mapping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Mapping[str, object] | Mapping[KeyPath, object]
|
Either nested ( |
required |
name
|
str
|
The layer name, used verbatim by |
'overrides'
|
locator
|
str | None
|
What origins should report; defaults to |
None
|
Source code in src/whence/sources/mapping.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | |
load ¶
load() -> Layer
Return the mapping as a layer.
Returns:
| Type | Description |
|---|---|
Layer
|
The layer, marked found when the mapping was non-empty. |
Source code in src/whence/sources/mapping.py
45 46 47 48 49 50 51 | |
SecretsDirSource ¶
Reads one file per key from a directory.
A file named db.password or db__password becomes the key
db.password; both spellings work because Kubernetes secret keys cannot
always contain dots.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path | str
|
The directory to read. |
'/run/secrets'
|
name
|
str
|
The layer name. |
'secrets-dir'
|
Source code in src/whence/sources/secrets.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
__init__ ¶
__init__(
path: Path | str = "/run/secrets",
*,
name: str = "secrets-dir",
) -> None
Build a secrets-directory source.
Source code in src/whence/sources/secrets.py
38 39 40 41 | |
load ¶
load() -> Layer
Read every regular file in the directory.
Returns:
| Type | Description |
|---|---|
Layer
|
The layer, with |
Source code in src/whence/sources/secrets.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
Source ¶
Bases: Protocol
Something that can contribute one layer of configuration.
Source code in src/whence/sources/__init__.py
36 37 38 39 40 41 42 43 44 45 46 47 48 | |
load ¶
load() -> Layer
Read everything this source has to offer.
Returns:
| Type | Description |
|---|---|
Layer
|
One layer, possibly empty, named after this source. |
Source code in src/whence/sources/__init__.py
42 43 44 45 46 47 48 | |
Layer
dataclass
¶
Everything one source contributed, under its own name.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The source's name in the chain, used verbatim by |
entries |
Mapping[KeyPath, Tracked]
|
Flat canonical key paths to tracked values. |
found |
bool
|
Whether the source produced anything. A source that was consulted
and found nothing still appears in |
Source code in src/whence/tree.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
Resolved
dataclass
¶
The outcome of merging layers, with the losers kept.
Attributes:
| Name | Type | Description |
|---|---|---|
values |
Mapping[KeyPath, Tracked]
|
The winning value for every key. |
shadowed |
Mapping[KeyPath, tuple[tuple[str, Tracked], ...]]
|
Per key, the |
layers |
tuple[Layer, ...]
|
Every layer consulted, highest precedence first, including the ones that contributed nothing. |
Source code in src/whence/tree.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
keys ¶
keys() -> Iterable[KeyPath]
Return every key that has a winning value.
Returns:
| Type | Description |
|---|---|
Iterable[KeyPath]
|
The resolved key paths. |
Source code in src/whence/tree.py
60 61 62 63 64 65 66 | |
dotted ¶
dotted() -> dict[str, Tracked]
Return the resolved values keyed by dotted name.
Returns:
| Type | Description |
|---|---|
dict[str, Tracked]
|
A mapping from |
Source code in src/whence/tree.py
68 69 70 71 72 73 74 | |
bind ¶
bind(
target: type,
values: Mapping[KeyPath, Tracked],
*,
prefix: KeyPath = (),
shadowed: Mapping[
KeyPath, tuple[tuple[str, Tracked], ...]
]
| None = None,
strict: bool = True,
) -> Any
Bind resolved configuration onto a schema, reporting every problem.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
type
|
The schema class. |
required |
values
|
Mapping[KeyPath, Tracked]
|
Flat resolved values. |
required |
prefix
|
KeyPath
|
The subtree to bind. |
()
|
shadowed
|
Mapping[KeyPath, tuple[tuple[str, Tracked], ...]] | None
|
Shadow records, used to enrich error messages. |
None
|
strict
|
bool
|
Whether a key nothing declared is an error. |
True
|
Returns:
| Type | Description |
|---|---|
Any
|
An instance of |
Raises:
| Type | Description |
|---|---|
BindError
|
If anything failed, with every problem in one message. |
Source code in src/whence/binding/__init__.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
render_problems ¶
render_problems(problems: Sequence[Problem]) -> str
Format problems the way whence reports them.
The four-field Property / Value / Origin / Reason block plus an
Action line is Spring Boot's failure-analysis layout, which is the best
in the field and costs nothing to adopt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problems
|
Sequence[Problem]
|
The problems to render. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A multi-line message. |
Source code in src/whence/binding/__init__.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
configure ¶
configure(config: Config | None) -> None
Set the ambient configuration for this context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
Config | None
|
The configuration, or |
required |
Source code in src/whence/decorators.py
102 103 104 105 106 107 108 | |
current_config ¶
current_config(config: Config) -> Generator[Config]
Use a configuration for the duration of a block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
Config
|
The configuration to install. |
required |
Yields:
| Type | Description |
|---|---|
Generator[Config]
|
The same configuration. |
Source code in src/whence/decorators.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
from_config ¶
from_config(fn: Callable[P, R]) -> Callable[..., R]
Fill parameters annotated :data:Injected or :class:Value from configuration.
The wrapper is typed Callable[..., R] rather than Callable[P, R],
and that is the one real cost of putting the markers in Annotated. A
marked parameter with no default is required in the signature a type
checker reads, so connect() would be an error at every call site even
though filling it is the entire point. Nothing in the type system can say
"these particular keyword parameters are now optional", so the return type
says the honest thing instead: after decoration, the arguments are whichever
subset the caller chooses to pass. The return type is still checked, and the
runtime signature is untouched -- inspect.signature and every framework
that reads it see the parameters exactly as written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
Callable[P, R]
|
The function to wrap; sync or async. |
required |
Returns:
| Type | Description |
|---|---|
Callable[..., R]
|
The wrapped function, accepting any subset of its own arguments. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If a marked parameter is not keyword-only, carries two
markers, or combines |
Source code in src/whence/decorators.py
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | |
load_settings ¶
load_settings(
cls: type[T],
config: Config | None = None,
*,
strict: bool | None = None,
) -> T
Bind a @settings class from a configuration.
The typed spelling of config.bind(cls), and the only one: @settings
attaches no methods to the class it decorates, because an attribute added by
a decorator is invisible to a type checker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[T]
|
A class decorated with :func: |
required |
config
|
Config | None
|
The configuration; defaults to the ambient one. |
None
|
strict
|
bool | None
|
Override the strictness |
None
|
Returns:
| Type | Description |
|---|---|
T
|
An instance of |
Source code in src/whence/decorators.py
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
settings ¶
settings(prefix: type[T]) -> type[T]
settings(
prefix: str = "", *, strict: bool = True
) -> Callable[[type[T]], type[T]]
settings(prefix: Any = '', *, strict: bool = True) -> Any
Mark a class as binding to a subtree of configuration.
Usable bare or called, like :func:~dataclasses.dataclass: @settings
binds the whole tree, @settings("db") binds one subtree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
Any
|
The dotted subtree, such as |
''
|
strict
|
bool
|
Whether a key under |
True
|
Returns:
| Type | Description |
|---|---|
Any
|
The decorated class when used bare, otherwise a class decorator. |
Source code in src/whence/decorators.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
canonical ¶
canonical(key: str | Iterable[str]) -> KeyPath
Normalise a dotted key or a sequence of segments into a key path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str | Iterable[str]
|
|
required |
Returns:
| Type | Description |
|---|---|
KeyPath
|
The canonical path, lowercase with |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If the key is empty or contains an empty segment. |
Source code in src/whence/keys.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
env_name ¶
env_name(path: KeyPath, prefix: str = '') -> str
Render the one environment variable name that maps to a key path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
KeyPath
|
The canonical key path. |
required |
prefix
|
str
|
The environment prefix, already validated. |
''
|
Returns:
| Type | Description |
|---|---|
str
|
|
Source code in src/whence/keys.py
113 114 115 116 117 118 119 120 121 122 123 | |
key_from_env ¶
key_from_env(name: str, prefix: str = '') -> KeyPath | None
Map an environment variable name back to a key path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The variable name as it appears in the environment. |
required |
prefix
|
str
|
The environment prefix, already validated. |
''
|
Returns:
| Type | Description |
|---|---|
KeyPath | None
|
The key path, or |
KeyPath | None
|
an empty segment. |
Source code in src/whence/keys.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
origin_of ¶
origin_of(obj: object) -> Origin | None
Return the origin of a value, or None if it carries none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
object
|
Any value. |
required |
Returns:
| Type | Description |
|---|---|
Origin | None
|
The origin, when |
Source code in src/whence/origin.py
123 124 125 126 127 128 129 130 131 132 | |
unwrap ¶
unwrap(obj: object) -> Any
Strip tracking from a value, recursively through containers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
object
|
A tracked value, a plain value, or a container of either. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The same shape with every :class: |
Source code in src/whence/origin.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
active_profiles ¶
active_profiles(
explicit: Sequence[str] | None = None,
*,
environ: Mapping[str, str] | None = None,
var: str = "",
groups: Mapping[str, Sequence[str]] | None = None,
default: Sequence[str] = (),
) -> tuple[str, ...]
Decide which profiles are active.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
explicit
|
Sequence[str] | None
|
Profiles passed in code; wins over the environment. |
None
|
environ
|
Mapping[str, str] | None
|
The environment to consult. |
None
|
var
|
str
|
The variable holding a comma-separated list. |
''
|
groups
|
Mapping[str, Sequence[str]] | None
|
Group definitions to expand. |
None
|
default
|
Sequence[str]
|
Profiles to use when nothing else says. |
()
|
Returns:
| Type | Description |
|---|---|
tuple[str, ...]
|
Active profiles in increasing precedence, so the last one wins. |
Source code in src/whence/profiles.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
expand_groups ¶
expand_groups(
profiles: Sequence[str],
groups: Mapping[str, Sequence[str]] | None = None,
_depth: int = 0,
) -> tuple[str, ...]
Expand profile groups into the profiles they stand for.
groups={"staging": ["cloud", "readonly-db"]} turns ["staging"] into
["cloud", "readonly-db", "staging"]. Spring added groups in 2.4 to
replace chained include directives, and they are considerably easier to
read than the chain they replaced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profiles
|
Sequence[str]
|
The requested profiles, in increasing precedence. |
required |
groups
|
Mapping[str, Sequence[str]] | None
|
Group definitions. |
None
|
_depth
|
int
|
Recursion guard. |
0
|
Returns:
| Type | Description |
|---|---|
tuple[str, ...]
|
The expanded profiles, deduplicated, in increasing precedence. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If groups reference each other in a cycle. |
Source code in src/whence/profiles.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
is_sensitive ¶
is_sensitive(
key: str, patterns: Sequence[str] = SENSITIVE
) -> bool
Report whether a key name looks like it holds a secret.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
A dotted key name. |
required |
patterns
|
Sequence[str]
|
Glob patterns to match against, case-insensitively. |
SENSITIVE
|
Returns:
| Type | Description |
|---|---|
bool
|
True when any pattern matches. |
Source code in src/whence/secret.py
116 117 118 119 120 121 122 123 124 125 126 127 | |
sanitize ¶
sanitize(
key: str,
value: Any,
patterns: Sequence[str] = SENSITIVE,
) -> Any
Redact a value if its key or its type says it is sensitive.
A :class:Secret is masked whatever it is called, and a value is masked
when its key matches. Both directions matter: convict masks by declared
sensitivity, Spring masks by key pattern, and each misses what the other
catches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The dotted key name. |
required |
value
|
Any
|
The value. |
required |
patterns
|
Sequence[str]
|
Key patterns to treat as sensitive. |
SENSITIVE
|
Returns:
| Type | Description |
|---|---|
Any
|
Either the value or :data: |
Source code in src/whence/secret.py
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
unlock_secrets ¶
unlock_secrets() -> Generator[None]
Permit :meth:Secret.reveal for the duration of the block.
The flag lives in a :class:~contextvars.ContextVar, so it does not leak
across tasks and a concurrent request cannot ride on another's unlock.
Yields:
| Type | Description |
|---|---|
Generator[None]
|
Nothing. |
Source code in src/whence/secret.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |