Settings¶
The didactic-settings distribution. See
Guides > Settings for the composition ladder,
config groups, union descent, interpolation and provenance.
Composition¶
didactic.settings.compose ¶
compose(
path: Path | str | None = None,
*,
schema: type[M],
base: Mapping[str, ConfigValue] | None = None,
groups: Mapping[str, str | None] | None = None,
profile: str | Mapping[str, ConfigValue] | None = None,
overlays: Sequence[
Path | str | Mapping[str, ConfigValue]
] = (),
overrides: Sequence[Override] = (),
search_path: Sequence[Path | str] = (),
resolvers: Mapping[str, ResolverFn] | None = None,
) -> M
Compose a validated model from files, fragments, overlays and overrides.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The primary document. Its
TYPE:
|
schema
|
The model to validate against; every layer is checked against it at every depth.
TYPE:
|
base
|
A mapping merged below everything else.
TYPE:
|
groups
|
Config-group selections by slot, applied after the file's
TYPE:
|
profile
|
A profile name (loaded as
TYPE:
|
overlays
|
Documents merged above the profile: file paths or mappings.
TYPE:
|
overrides
|
TYPE:
|
search_path
|
Directories searched, after the primary file's parent, for fragments and profiles.
TYPE:
|
resolvers
|
Interpolation resolvers consulted before the registry, for this call only.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
M
|
The validated instance; :func: |
| RAISES | DESCRIPTION |
|---|---|
ConfigError
|
For any refusal at any depth: an unknown key, an unknown or missing variant, a missing fragment or profile, a malformed override, text the leaf annotation cannot read. |
InterpolationError
|
For an expression that cannot be resolved. |
ValidationError
|
When the resolved tree fails the model's own validation. |
didactic.settings.compose_traced ¶
compose_traced(
path: Path | str | None = None,
*,
schema: type[M],
base: Mapping[str, ConfigValue] | None = None,
groups: Mapping[str, str | None] | None = None,
profile: str | Mapping[str, ConfigValue] | None = None,
overlays: Sequence[
Path | str | Mapping[str, ConfigValue]
] = (),
overrides: Sequence[Override] = (),
search_path: Sequence[Path | str] = (),
resolvers: Mapping[str, ResolverFn] | None = None,
) -> Composed[M]
Compose like :func:compose and return the value with its record.
Parameters are those of :func:compose.
didactic.settings.compose_layers ¶
compose_layers(
*,
schema: type[M],
layers: Sequence[Layer],
resolvers: Mapping[str, ResolverFn] | None = None,
) -> Composed[M]
Merge an assembled ladder, settle, interpolate, validate and record.
| PARAMETER | DESCRIPTION |
|---|---|
schema
|
The model to validate against.
TYPE:
|
layers
|
The layers, lowest precedence first.
TYPE:
|
resolvers
|
Interpolation resolvers consulted before the registry, for this call only.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ConfigError
|
For a refusal during the merge, the settle pass or the check of
the resolved tree, or when the schema declares |
didactic.settings.Composed
dataclass
¶
Composed(
value: M,
tree: dict[str, ConfigValue],
provenance: Provenance,
layers: tuple[Layer, ...],
)
The result of a traced composition.
| PARAMETER | DESCRIPTION |
|---|---|
value
|
The validated model, with the provenance attached as
TYPE:
|
tree
|
The merged, settled and interpolated document: what
TYPE:
|
provenance
|
Exactly one origin per leaf of
TYPE:
|
layers
|
Every layer applied, lowest precedence first.
TYPE:
|
didactic.settings.Layer
dataclass
¶
Layer(
origin: Origin,
document: Mapping[str, ConfigValue],
textual: bool = False,
)
One document to merge, tagged with its origin.
| PARAMETER | DESCRIPTION |
|---|---|
origin
|
The origin every leaf the document sets will carry.
TYPE:
|
document
|
The nested document, already mounted at the root (a group fragment is wrapped under its slot path before it becomes a layer).
TYPE:
|
textual
|
Whether string leaves are text to decode by the leaf's annotation
before the write (environment variables, dotenv lines, CLI
arguments,
TYPE:
|
didactic.settings.strict_merge ¶
strict_merge(
base: Mapping[str, ConfigValue],
overlay: Mapping[str, ConfigValue],
*,
schema: type[Model],
origin: Origin | None = None,
provenance: dict[KeyPath, Origin] | None = None,
) -> dict[str, ConfigValue]
Merge one mapping over another under a schema.
A convenience over :func:merge_layer for callers holding two
documents: the overlay becomes a typed layer with origin
(Origin("overlay") when omitted) and its leaves are recorded in
provenance when one is given.
Class-based settings¶
didactic.settings.Settings ¶
Bases: Model
Base class for application settings.
Subclasses declare fields like any didactic.api.Model,
plus a class-level __sources__ tuple. Call
Settings.load to compose an
instance from a primary file, its config groups, a profile, overlays,
the sources and overrides.
Examples:
>>> import didactic.api as dx
>>> from didactic.settings import Settings, EnvSource
>>>
>>> class App(Settings):
... debug: bool = False
... port: int = 8080
...
... __sources__ = (EnvSource(prefix="APP_"),)
| ATTRIBUTE | DESCRIPTION |
|---|---|
__sources__ |
The sources consulted in declaration order; later sources win. Their names must be distinct.
TYPE:
|
__search_path__ |
Directories searched for fragments and profiles, after the
primary file's parent and the first
TYPE:
|
__provenance__ |
The Provenance of an instance
built by
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
TypeError
|
At class creation, when a field is named after one of |
load
classmethod
¶
load(
path: Path | str | None = None,
*,
profile: str | Mapping[str, ConfigValue] | None = None,
groups: Mapping[str, str | None] | None = None,
overlays: Sequence[
Path | str | Mapping[str, ConfigValue]
] = (),
overrides: Sequence[Override] = (),
search_path: Sequence[Path | str] | None = None,
resolvers: Mapping[str, ResolverFn] | None = None,
**values: ConfigValue,
) -> Self
Compose an instance from the file, groups, profile, sources and overrides.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The primary document; its
TYPE:
|
profile
|
A profile name or mapping.
TYPE:
|
groups
|
Config-group selections by slot.
TYPE:
|
overlays
|
File paths or mappings merged above the profile.
TYPE:
|
overrides
|
TYPE:
|
search_path
|
Directories searched for fragments and profiles;
TYPE:
|
resolvers
|
Interpolation resolvers for this call.
TYPE:
|
**values
|
Typed overrides applied last, keyed by dotted path with
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Settings
|
The validated instance with |
load_traced
classmethod
¶
load_traced(
path: Path | str | None = None,
*,
profile: str | Mapping[str, ConfigValue] | None = None,
groups: Mapping[str, str | None] | None = None,
overlays: Sequence[
Path | str | Mapping[str, ConfigValue]
] = (),
overrides: Sequence[Override] = (),
search_path: Sequence[Path | str] | None = None,
resolvers: Mapping[str, ResolverFn] | None = None,
**values: ConfigValue,
) -> Composed[Self]
Load like :meth:load and return the value with its record and layers.
Parameters are those of :meth:load.
didactic.settings.Source ¶
Base class of every settings source.
Subclasses are frozen dataclasses that declare their own parameters
and a name (recorded as Origin.name on every leaf the source
sets; distinct within one Settings class) and implement
:meth:layer.
didactic.settings.EnvSource
dataclass
¶
Bases: Source
Read settings from environment variables.
Every settable path of the schema (each leaf, and each model, union
and map slot) is looked up as prefix plus the path segments
joined by separator, upper-cased: EnvSource(prefix="APP_")
reads trainer.epochs from APP_TRAINER__EPOCHS and the whole
model.type_encoder slot from APP_MODEL__TYPE_ENCODER (JSON
object text). Below a map slot, variables continuing the slot's name
set entries: APP_PATHS__DATA_DIR sets paths["data_dir"] (the
key is lower-cased, since variable names are upper-cased) and the
segments after the key address the entry's own fields. Variables that
name no path are never read.
| PARAMETER | DESCRIPTION |
|---|---|
prefix
|
Text prepended to every variable name.
TYPE:
|
separator
|
Text joining the path segments.
TYPE:
|
name
|
The source name for provenance.
TYPE:
|
didactic.settings.DotEnvSource
dataclass
¶
DotEnvSource(
path: str | Path = ".env",
prefix: str = "",
*,
separator: str = "__",
name: str = "dotenv",
)
Bases: Source
Read settings from a dotenv file.
Lines are KEY=value, optionally prefixed by export; blank
lines and # comments are skipped; a value wrapped in matching
single or double quotes is unquoted. Keys are looked up exactly as
:class:EnvSource looks up variables. A missing file contributes
nothing.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The dotenv file.
TYPE:
|
prefix
|
Text prepended to every key.
TYPE:
|
separator
|
Text joining the path segments.
TYPE:
|
name
|
The source name for provenance.
TYPE:
|
didactic.settings.FileSource
dataclass
¶
Bases: Source
Read settings from a JSON, TOML or YAML file.
The whole document is the layer, checked against the schema at every
depth like any other file; the file's defaults key is an ordinary
key here, since only the primary path of Settings.load
carries a selection list.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The file; its suffix selects the reader.
TYPE:
|
name
|
The source name for provenance.
TYPE:
|
required
|
Whether a missing file is an error rather than an empty source.
TYPE:
|
didactic.settings.CliSource
dataclass
¶
CliSource(
args: Namespace
| Mapping[str, ConfigValue]
| None = None,
*,
name: str = "cli",
)
Bases: Source
Read settings from parsed command-line arguments.
Keys are dotted paths or paths joined by __; a None value
means the argument was not given and is skipped. String values are
decoded by the leaf annotation; typed values pass through.
| PARAMETER | DESCRIPTION |
|---|---|
args
|
An
TYPE:
|
name
|
The source name for provenance.
TYPE:
|
Provenance¶
didactic.settings.Provenance ¶
Provenance(entries: Mapping[str, Origin])
Bases: Mapping[str, Origin]
Which layer wrote each leaf of a composed model.
An immutable mapping from dotted leaf path to :class:Origin that
iterates in sorted path order and compares by value, so two
compositions from the same layers give equal records. It covers
exactly the leaves of value.model_dump_json(): every leaf no layer
wrote is labelled default. Map keys are recorded verbatim as path
segments, so a key containing . makes its path ambiguous. A list
is one leaf: the elements of a tuple[T, ...] field are checked
against the schema but not attributed individually, so the record
stops at the list boundary.
source_of ¶
source_of(path: str) -> Origin
Return the origin of one leaf.
| RAISES | DESCRIPTION |
|---|---|
ConfigError
|
When no leaf has that path; the message lists the leaves. |
under ¶
under(prefix: str) -> Provenance
Return the entries at or below a dotted prefix, keys kept absolute.
by_layer ¶
Group the leaf paths by Origin.label.
The answer to "what did the profile change": each label maps to the sorted paths it wrote.
to_dict ¶
Render the record as JSON-shaped dicts, for a sidecar file.
didactic.settings.Origin
dataclass
¶
Origin(
kind: OriginKind,
name: str = "",
path: str | None = None,
expression: str | None = None,
)
Where a leaf's value came from.
| PARAMETER | DESCRIPTION |
|---|---|
kind
|
The precedence rung.
TYPE:
|
name
|
What within the rung: the profile name,
TYPE:
|
path
|
The on-disk file the value was read from, for file-backed layers.
TYPE:
|
expression
|
The leaf's source text when it contained
TYPE:
|
didactic.settings.OriginKind ¶
OriginKind = Literal[
"default",
"base",
"group",
"defaults",
"file",
"profile",
"overlay",
"source",
"override",
]
The closed set of precedence rungs, lowest first.
default is written only by provenance completion and by the settle
pass for an injected discriminator; the others name the layer kinds
compose and Settings.load apply.
didactic.settings.provenance_of ¶
provenance_of(model: Model) -> Provenance
Read the record attached to an instance built by the engine.
| RAISES | DESCRIPTION |
|---|---|
ConfigError
|
When the instance was constructed directly or through
|
Interpolation¶
didactic.settings.resolve ¶
resolve(
node: ConfigValue,
*,
root: Mapping[str, ConfigValue],
here: tuple[str | int, ...] = (),
resolvers: Mapping[str, ResolverFn] | None = None,
) -> ConfigValue
Resolve every interpolation in node, returning a new value.
Strings are parsed and evaluated; dicts and lists are descended with
here extended by the key or index. A string that is exactly one
expression substitutes the typed value (resolving "${a.b}" where
a.b is an int gives an int); a substring substitution coerces to
str.
| PARAMETER | DESCRIPTION |
|---|---|
node
|
The value to resolve.
TYPE:
|
root
|
The tree references resolve against.
TYPE:
|
here
|
Path of
TYPE:
|
resolvers
|
Resolvers consulted before the process-wide registry, for this call only.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
InterpolationError
|
For an unresolved reference, an unknown resolver, a resolver that
raised, a cycle or a syntax error. The error's |
didactic.settings.resolve_traced ¶
resolve_traced(
document: Mapping[str, ConfigValue],
*,
root: Mapping[str, ConfigValue] | None = None,
resolvers: Mapping[str, ResolverFn] | None = None,
) -> tuple[dict[str, ConfigValue], dict[KeyPath, str]]
Resolve a whole document and report which leaves held expressions.
| PARAMETER | DESCRIPTION |
|---|---|
document
|
The document whose leaves are resolved.
TYPE:
|
root
|
The tree references resolve against;
TYPE:
|
resolvers
|
Resolvers consulted before the process-wide registry, for this call only.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple
|
The resolved document, and |
didactic.settings.register_resolver ¶
register_resolver(
name: str, fn: ResolverFn, *, replace: bool = False
) -> None
Register a resolver under name in the process-wide registry.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
Resolver name as it appears in
TYPE:
|
fn
|
Function called with the interpolated string arguments. Its return value is substituted in place.
TYPE:
|
replace
|
Whether re-registration of an existing name is allowed. Off by default so accidental shadowing is loud.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
When |
didactic.settings.unregister_resolver ¶
Remove a registered resolver; a no-op when the name is absent.
didactic.settings.list_resolvers ¶
Return the names of every registered resolver, sorted.
didactic.settings.lookup ¶
lookup(path: str) -> ConfigValue
Evaluate a dotted path inside the active evaluation.
Integer segments index lists; the value found is itself interpolated
before it is returned. Because the lookup runs inside the evaluation
state of the enclosing :func:resolve call, a reference cycle that
passes through a resolver is reported as a cycle.
| RAISES | DESCRIPTION |
|---|---|
InterpolationError
|
When called outside an active :func: |
didactic.settings.active_root ¶
active_root() -> Mapping[str, ConfigValue] | None
Return the tree currently being interpolated, or None.
Resolvers that read other parts of the tree should prefer
:func:lookup, which shares cycle detection with the evaluation in
progress; this accessor exposes the raw tree for resolvers that
inspect its shape.
didactic.settings.ResolverFn
module-attribute
¶
ResolverFn = Callable[..., ConfigValue]
A resolver: called with the interpolated string arguments, returns a value.
Arguments are strings; the return value is substituted in place, typed
when the expression is the whole leaf. A resolver may call
:func:lookup to read other parts of the tree being interpolated.
Documents, overrides and scalars¶
didactic.settings.load_document ¶
load_document(path: Path | str) -> dict[str, ConfigValue]
Load one JSON, TOML or YAML file as a document.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
The file; its suffix selects the reader.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict[str, ConfigValue]
|
The document. An empty file gives an empty document. |
| RAISES | DESCRIPTION |
|---|---|
FileNotFoundError
|
When |
ConfigError
|
When the suffix is unsupported, when a YAML file is opened without PyYAML installed, or when the top-level value is not a mapping. |
didactic.settings.nest_override ¶
nest_override(
dotted_key: str, value: ConfigValue
) -> dict[str, ConfigValue]
Wrap a value under a dotted key, one nested dict per segment.
nest_override("model.type_encoder.num_heads", 8) gives
{"model": {"type_encoder": {"num_heads": 8}}}. The key's syntax is
validated by :func:~didactic.settings.parse_override; this function
only builds the tree.
didactic.settings.parse_override ¶
Split a key=value override into its key and raw value text.
The key is validated: it must be non-empty, every dotted segment must
be non-empty, and no segment may be an integer (lists are set whole,
as tags=[...]). The value text is returned untouched; the leaf
annotation decides how it is read.
| RAISES | DESCRIPTION |
|---|---|
OverrideSyntaxError
|
When |
didactic.settings.parse_scalar ¶
parse_scalar(text: str) -> ConfigValue
Read text with the schema-free grammar.
Surrounding whitespace is ignored. null, ~ and the empty
string give None; true and false (any case) give booleans;
an integer literal without a leading zero gives an int; a float
literal (1.5, 1e-3, inf, nan) gives a float; text
starting with [ or { is JSON; a single- or double-quoted
string gives its contents; anything else is the text itself.
| RAISES | DESCRIPTION |
|---|---|
OverrideSyntaxError
|
When text starting with |
didactic.settings.decode_text ¶
decode_text(
text: str,
annotation: object,
*,
path: str,
origin: Origin | None = None,
) -> ConfigValue
Read text for a leaf annotation.
| PARAMETER | DESCRIPTION |
|---|---|
text
|
The text as it arrived from the environment, a dotenv file, the command line or an override string.
TYPE:
|
annotation
|
The leaf's annotation (
TYPE:
|
path
|
Dotted path of the leaf, for the error message.
TYPE:
|
origin
|
The layer that supplied the text, for the error message.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ConfigValue
|
|
| RAISES | DESCRIPTION |
|---|---|
CoercionError
|
When the text cannot be read for the annotation. |
didactic.settings.ConfigValue ¶
ConfigValue = (
str
| int
| float
| bool
| None
| Sequence[ConfigValue]
| Mapping[str, ConfigValue]
)
A JSON-shaped config value: scalars, None, lists and string-keyed dicts.
The container arms are spelled with the covariant Sequence and
Mapping so a narrowly inferred literal ({"a": {"b": "x"}}, typed
dict[str, dict[str, str]]) is accepted wherever a ConfigValue is
expected. At runtime the engine builds and checks for list and
dict.
didactic.settings.KeyPath ¶
A path of dict keys from the document root to a node.
Errors¶
didactic.settings.ConfigError ¶
Bases: ValueError
A configuration is malformed or fails the engine's checks.
| PARAMETER | DESCRIPTION |
|---|---|
message
|
The rendered error text.
TYPE:
|
path
|
Dotted path of the offending leaf, or
TYPE:
|
didactic.settings.UnknownKeyError ¶
UnknownKeyError(
message: str,
*,
path: str,
allowed: tuple[str, ...] = (),
declared_by: tuple[object, ...] = (),
set_by: Origin | None = None,
)
Bases: ConfigError
A layer sets a key the schema does not declare at that path.
| PARAMETER | DESCRIPTION |
|---|---|
message
|
The rendered error text.
TYPE:
|
path
|
Dotted path of the unknown key.
TYPE:
|
allowed
|
Sorted field names accepted at the enclosing node.
TYPE:
|
declared_by
|
Primary tags of the union variants that declare the key, as the live discriminator values; empty when no variant does, or when the node is a plain model.
TYPE:
|
set_by
|
The origin of the layer that set the key.
TYPE:
|
didactic.settings.UnknownVariantError ¶
UnknownVariantError(
message: str,
*,
path: str,
value: object,
registered: tuple[object, ...] = (),
)
Bases: ConfigError
A discriminator names no registered variant, or is not a literal.
| PARAMETER | DESCRIPTION |
|---|---|
message
|
The rendered error text.
TYPE:
|
path
|
Dotted path of the discriminator leaf.
TYPE:
|
value
|
The discriminator value as it arrived.
TYPE:
|
registered
|
The registered tags as the live discriminator values, sorted by type and then by value.
TYPE:
|
didactic.settings.MissingFragmentError ¶
MissingFragmentError(
message: str,
*,
group: str,
name: str,
tried: tuple[Path, ...],
available: tuple[str, ...] = (),
)
Bases: ConfigError
A named fragment, root fragment or profile exists in no search root.
| PARAMETER | DESCRIPTION |
|---|---|
message
|
The rendered error text.
TYPE:
|
group
|
The dotted slot of the config group; the empty string for a
root-mounted
TYPE:
|
name
|
The fragment name that was looked for.
TYPE:
|
tried
|
Every path tried, in the order tried.
TYPE:
|
available
|
Sorted names that do exist for the group, across every root.
TYPE:
|
didactic.settings.OverrideSyntaxError ¶
didactic.settings.CoercionError ¶
Bases: ConfigError
Text from a textual layer cannot be decoded for the leaf's annotation.
| PARAMETER | DESCRIPTION |
|---|---|
message
|
The rendered error text.
TYPE:
|
path
|
Dotted path of the leaf.
TYPE:
|
expected
|
A rendering of the annotation the text had to satisfy.
TYPE:
|
text
|
The text that was refused.
TYPE:
|
didactic.settings.InterpolationError ¶
Bases: ValueError
An interpolation expression cannot be resolved.
Raised for a reference to a path that does not exist in the composed tree, a call to an unregistered resolver, a resolver that raised, a cycle between references, or a syntax error in the expression.
| PARAMETER | DESCRIPTION |
|---|---|
message
|
The rendered error text.
TYPE:
|
path
|
Path of the leaf whose text was being resolved when the error was
raised;
TYPE:
|