Loader API¶
DraconLoader¶
The main entry point for loading and composing configurations.
Constructor¶
DraconLoader(
custom_loaders: Dict[str, Callable] = None,
capture_globals: bool = True,
base_dict_type: Type[DictLike] = dracontainer.Mapping,
base_list_type: Type[ListLike] = dracontainer.Sequence,
enable_interpolation: bool = True,
interpolation_engine: Literal['asteval', 'eval'] = 'asteval',
context: Dict[str, Any] = None,
deferred_paths: list[KeyPath | str] = None,
enable_shorthand_vars: bool = True,
use_cache: bool = True,
trace: bool = True,
symbol_sources: Sequence[SymbolSource] | None = None,
)
| Parameter | Description |
|---|---|
custom_loaders |
Scheme-to-loader mappings, merged on top of built-in loaders (file, pkg, env, var, raw, rawpkg, cascade). |
capture_globals |
Whether to capture global variables into context. |
base_dict_type |
Container type for mappings. Default dracontainer.Mapping (a dict-like with metadata support). Use dict for plain output. |
base_list_type |
Container type for sequences. Default dracontainer.Sequence. Use list for plain output. |
enable_interpolation |
Enable ${...} expression evaluation. When False, interpolation strings are kept as literals. |
interpolation_engine |
'asteval' (safe sandbox, default) or 'eval' (full Python eval, unsafe). |
context |
Initial interpolation context. Keys become variables available in ${...} expressions. |
deferred_paths |
List of keypaths (strings or KeyPath objects) that are forced to DeferredNode during composition. Supports wildcards. |
enable_shorthand_vars |
When True, bare $VAR tokens are converted to ${VAR} before interpolation. |
use_cache |
LRU cache (128 items) for parsed YAML strings. Disable for mutation-heavy workflows. |
trace |
Enable composition tracing. Also enabled when DRACON_TRACE=1 is set. |
symbol_sources |
Ordered chain of SymbolSource records the loader's SymbolTable consults on tag-resolution miss. Default chain ends with make_dynamic_import_source() (the importlib.import_module fallback). Pass an explicit list (without the dynamic-import source) for sandboxed runtimes -- the loader then refuses unknown tags instead of importing them. |
Trust zones via symbol_sources¶
The default loader has a dynamic_import source on its SymbolTable that calls importlib.import_module when a tag isn't in the explicit vocabulary. For sandboxed runtimes (Pyodide previews, untrusted-agent vocabularies, browser previews) you usually want the opposite: refuse anything that isn't pre-registered.
from dracon import DraconLoader, SymbolSource, SymbolEntry, SymbolTable, CallableSymbol
# trusted, explicit vocabulary
explicit = SymbolTable()
explicit.define(SymbolEntry(name="Foo", symbol=CallableSymbol(Foo, name="Foo")))
src = SymbolSource(name="user_vocab", lookup=explicit.__getitem__,
identify=explicit.identify, canonical_for_identify=True)
# sandboxed loader: only `!Foo` is resolvable, `!some.unknown.thing` errors
loader = DraconLoader(symbol_sources=[src])
Reverse identify() walks only sources where canonical_for_identify=True; the dynamic-import source defaults to False, so ad-hoc imports never pollute round-trip identity.
Methods¶
load(config_paths, merge_key="<<{<+}[<~]")¶
Load configuration from one or more file paths. Multiple paths are merged left to right.
Returns the fully constructed Python object (dict, Pydantic model, etc.).
loader = DraconLoader()
config = loader.load("file:config.yaml")
config = loader.load(["base.yaml", "overrides.yaml"])
loads(content: str)¶
Load configuration from a YAML string.
compose(config_paths, merge_key="<<{<+}[<~]")¶
Compose without constructing. Returns a CompositionResult (the raw YAML node tree after all includes, merges, and instructions have been processed).
cr = loader.compose("config.yaml")
# inspect cr.root, cr.defined_vars, cr.trace, etc.
obj = loader.load_node(cr.root)
merge(cr1, cr2, merge_key)¶
Merge two CompositionResult objects using a merge key string.
dump(data, stream=None)¶
Serialize data back to YAML. Returns a string if stream is None, otherwise writes to the stream.
update_context(kwargs: dict)¶
Add entries to the loader's interpolation context.
copy()¶
Create a shallow copy of the loader with independent context. Useful for isolated operations.
stack(*sources, **ctx)¶
Create a CompositionStack from source strings/specs. Context kwargs are applied to the first layer.
Module-level Functions¶
These are convenience wrappers that create a DraconLoader internally.
dracon.load(config_paths, raw_dict=False, merge_key="<<{<+}[<~]", **kwargs)¶
Load one or more config files. Pass raw_dict=True to get plain Python dicts/lists instead of Dracontainers. Extra **kwargs go to the DraconLoader constructor.
dracon.loads(config_str, raw_dict=False, **kwargs)¶
Load from a YAML string.
dracon.load_file(config_path, raw_dict=True, **kwargs)¶
Load a single file. Adds file: prefix if no scheme is present. Defaults to raw_dict=True.
dracon.dump(data, stream=None, **kwargs)¶
Serialize to YAML.
dracon.compose(source, **kwargs)¶
Compose a DeferredNode with runtime context. Returns a CompositionResult.
dracon.construct(node_or_val, resolve=True, **kwargs)¶
Construct a DeferredNode, CompositionResult, or raw YAML Node into a Python object. When resolve=True, also resolves all lazy interpolables.
dracon.resolve_all_lazy(obj, permissive=False)¶
Walk a constructed object and resolve any remaining LazyInterpolable values. When permissive=True, unresolvable expressions are left as strings instead of raising.
dracon.make_callable(path_or_node, context=None, context_types=None, auto_context=False, **loader_kwargs)¶
Turn a YAML config file or DeferredNode into a callable function. See CLI API for details.
CompositionStack¶
Layered composition with per-layer context, merge strategy, and scope control.
LayerSpec¶
LayerSpec(
source: str | Node | CompositionResult,
context: dict[str, Any] = {},
merge_key: str = "<<{<+}[<~]",
scope: LayerScope = LayerScope.ISOLATED,
label: str | None = None,
metadata: dict[str, Any] = {},
)
metadata is opaque to Dracon. It is preserved across push, replace,
fork, snapshot, and restore, and surfaces on LayerInfo and on
trace entries via TraceEntry.layer.metadata. Downstream packages own
the schema (authorship, source URI, audit tags, ...).
LayerScope¶
| Value | Receives exports | Receives PREV |
|---|---|---|
LayerScope.ISOLATED |
No | No |
LayerScope.EXPORTS |
Yes | No |
LayerScope.EXPORTS_AND_PREV |
Yes | Yes |
Exports are !defined variables from earlier layers. PREV is a constructed snapshot of the previous layer's output, available as ${PREV}.
Methods¶
| Method | Description |
|---|---|
push(layer, **ctx) |
Append a layer. Returns the layer index. |
pop(index=-1) |
Remove and return a layer. Invalidates cache from that point. |
replace(index, layer, **ctx) |
Replace a layer in-place. Invalidates cache from that point. |
fork() |
Create an independent copy sharing cached prefix. |
composed |
Property. Returns the fully composed CompositionResult. |
construct(**kwargs) |
Compose, then construct. Extra kwargs update loader context. |
layers |
Property. The list of LayerSpec objects. |
snapshot() |
Return a CompositionStackSnapshot of the current layers and cache. |
restore(snapshot) |
Replace layers and cache with the snapshot. Layer/result objects are restored by reference. |
transaction() |
Return a StackTransaction context manager (rolls back unless commit()). |
layer_info(index_or_label) |
LayerInfo with prefix, contribution, composed, label, metadata. |
composed_at(index) |
The composed CompositionResult after layer index. |
Transactional mutation¶
from dracon import CompositionStack, LayerSpec
stack = CompositionStack(loader)
stack.push(LayerSpec(source=base_node, label="base", metadata={"author": "system"}))
with stack.transaction() as tx:
stack.push(LayerSpec(source=fragment_node, label="agent-panel",
metadata={"author": "agent:debugger"}))
spec = stack.construct()
validate(spec) # raises -> rollback
tx.commit() # otherwise rollback on context exit
Nested transactions each capture their own snapshot, so an inner rollback does not affect outer state. Exceptions propagate unchanged. For async validation, hold the transaction object explicitly:
tx = stack.transaction()
tx.__enter__()
try:
...
await validate(...)
tx.commit()
finally:
tx.__exit__(*sys.exc_info())
This API makes long-lived daemons, live configuration editors, and UI graph systems able to attempt layer mutations atomically and inspect the active stack without reaching into private cache state.
LayerInfo¶
LayerInfo(
index: int,
label: str | None,
metadata: dict[str, Any],
layer: LayerSpec,
prefix: CompositionResult | None, # None for index 0
contribution: CompositionResult, # this layer alone
composed: CompositionResult, # stack including this layer
)
prefix, contribution, and composed are composition-layer objects;
construction remains an explicit caller choice.
Composition Tracing¶
Opt-in provenance tracking. Records how each leaf value arrived at its final state.
Enable via trace=True in DraconLoader or DRACON_TRACE=1 environment variable.
TraceEntry¶
@dataclass
class TraceEntry:
value: Any # the value at this step
source: SourceContext # file/line/column
via: ViaKind # how the value arrived
detail: str = "" # human-readable context
replaced: TraceEntry = None # previous entry (linked list)
ViaKind¶
Literal union of: "definition", "file_layer", "include", "merge", "if_branch", "each_expansion", "cli_override", "set_default", "define", "context_variable".
CompositionTrace¶
| Method | Description |
|---|---|
record(path, entry) |
Record an entry. Auto-links replaced to previous. |
get(path) -> list[TraceEntry] |
Get history for a dotted path. |
all() -> dict |
All entries. |
format_path(path) -> str |
Plain-text trace for one path. |
format_all() -> str |
Plain-text full provenance tree. |
format_path_rich(path) |
Rich Panel for one path. |
format_all_rich() |
Rich Table of all entries. |
Built-in Context¶
Every interpolation gets these functions and variables by default.
Functions¶
| Name | Wraps |
|---|---|
getenv(name, default=None) |
os.getenv |
getcwd() |
os.getcwd |
listdir(path) |
os.listdir |
join(*parts) |
os.path.join |
basename(path) |
os.path.basename |
dirname(path) |
os.path.dirname |
expanduser(path) |
os.path.expanduser |
isfile(path) |
os.path.isfile |
isdir(path) |
os.path.isdir |
Path |
pathlib.Path |
now(fmt='%Y-%m-%d %H:%M:%S') |
datetime.now().strftime(fmt) |
construct(node, ...) |
dracon.construct (bound to current loader settings) |
File Context Variables¶
Set automatically when loading from a file:
| Variable | Example value |
|---|---|
DIR |
/home/user/project |
FILE |
/home/user/project/config.yaml |
FILE_PATH |
Same as FILE |
FILE_STEM |
config |
FILE_EXT |
.yaml |
FILE_LOAD_TIME |
2025-01-15 14:30:00 |
FILE_LOAD_TIME_UNIX |
1736952600 |
FILE_LOAD_TIME_UNIX_MS |
1736952600000 |
FILE_SIZE |
1234 (bytes) |