Source code for loom.core.config.configurable
"""Declarative config bindings for runtime behaviour classes."""
from __future__ import annotations
import msgspec
from loom.core.model import LoomFrozenStruct
[docs]
class ConfigBinding(LoomFrozenStruct, frozen=True):
"""Deferred binding between a class and a config section.
The binding is a declaration only. It does not read files, access global
config state, or instantiate ``target``. Compiler/bootstrap code resolves
it later using an explicit runtime config object.
Args:
target: Runtime behaviour class to instantiate later.
config_path: Dot-separated config path. Empty means no YAML section is
required and only ``overrides`` are applied.
overrides: Explicit keyword overrides. These win over YAML values when
the binding is resolved.
"""
target: type[object]
config_path: str = ""
overrides: dict[str, object] = msgspec.field(default_factory=dict)
def __post_init__(self) -> None:
"""Validate the config path."""
if self.config_path and not self.config_path.strip():
raise ValueError("ConfigBinding.config_path must not be blank.")
[docs]
class Configurable:
"""Mixin for classes that can be declared from config paths.
The mixin returns :class:`ConfigBinding` declarations. It does not impose a
constructor shape and does not resolve configuration itself.
"""
[docs]
@classmethod
def from_config(cls, config_path: str, **overrides: object) -> ConfigBinding:
"""Declare this class as configured from a YAML section.
Args:
config_path: Dot-separated config path resolved later by compiler
or bootstrap code.
**overrides: Explicit keyword overrides applied after YAML values.
Returns:
Deferred config binding for this class.
Raises:
ValueError: If ``config_path`` is empty or blank.
"""
if not config_path.strip():
raise ValueError("config_path must not be empty.")
return ConfigBinding(
target=cls,
config_path=config_path,
overrides=dict(overrides),
)
__all__ = ["ConfigBinding", "Configurable"]