Concepts: Loading and Context¶
The DraconLoader
is the heart of Dracon's configuration processing system. It handles parsing YAML, processing Dracon's directives, managing context, and constructing the final Python objects.
The Loading Process¶
When you call dracon.load("file.yaml")
or loader.load("file.yaml")
, several steps occur:
- File Reading: The appropriate loader (
file:
,pkg:
, custom) reads the raw YAML content from the source. Caching may be used here. - YAML Parsing & Composition:
ruamel.yaml
parses the raw YAML into a basic node tree. Dracon'sDraconComposer
extends this to recognize Dracon-specific syntax like!include
,<<{...}
,!define
, etc., building an initial composition representation. - Instruction Processing: Instructions like
!define
,!if
,!each
are executed, modifying the node tree and context before includes or merges. - Include Resolution:
!include
directives are processed recursively. The content from included sources is loaded, composed, and inserted into the main tree. Context variables like$DIR
are injected into the included scope. - Merge Processing: Extended merge keys (
<<{...}[...]@...:
) are processed according to their specified strategies, combining different parts of the node tree. - Deferred Node Identification: Nodes tagged with
!deferred
or matchingdeferred_paths
are identified and wrapped. Their processing is paused. - Reference Preprocessing: Interpolation expressions (
${...}
) are scanned. References using&anchor
or&/path
(node references for templating) are prepared. - Final Construction: Dracon's
Draconstructor
traverses the final node tree.- It constructs basic Python types (dict, list, str, int...). By default, it uses
dracon.dracontainer.Mapping
andSequence
for automatic lazy interpolation handling. - When it encounters a tag (
!MyModel
), it resolves the corresponding type. - If the type is Pydantic, it passes the constructed data to Pydantic for validation and instance creation.
- If the type is custom, it attempts
YourClass(constructed_data)
. - Values containing
${...}
are wrapped inLazyInterpolable
objects (unlessenable_interpolation=False
).
- It constructs basic Python types (dict, list, str, int...). By default, it uses
- Return Value: The final constructed Python object (often a Pydantic model instance or a Dracon container) is returned.
The Role of Context¶
Context is a dictionary (dracon.utils.ShallowDict
internally) that holds variables and functions accessible during the loading process.
- Initial Context: Provided via
DraconLoader(context=...)
. This is the primary way to make Pydantic models, custom types, or helper functions available. - Default Context: Dracon automatically adds
getenv
,getcwd
, andconstruct
. - Instruction Context (
!define
,!set_default
): Instructions modify the context available to subsequent nodes within the same scope or child scopes during composition. - Include Context (
$DIR
, etc.): File/package loaders inject variables like$DIR
into the context of the included file's nodes. - Interpolation Context (
${...}
): Lazy interpolation expressions have access to the context captured at the time the LazyInterpolable object was created. This includes initial context, definitions, and include-specific variables. Context provided later via.resolve(context=...)
or.construct(context=...)
is merged with the captured context. - Deferred Node Context: A
DeferredNode
captures a snapshot of the context available when it was created. Context passed to.construct(context=...)
merges with this snapshot.!deferred::clear_ctx
controls which variables are excluded from the snapshot.
Context Precedence: Generally, more specific contexts override broader ones. Context provided at runtime (e.g., via .construct()
) typically overrides context captured during loading. Merge keys ({<+}
vs {>+}
) can influence merging behavior for context dictionaries passed down the tree.
Output Types (Dracontainer
vs. dict
/list
)¶
By default, mappings become dracon.dracontainer.Mapping
and sequences become dracon.dracontainer.Sequence
.
- Pros: These containers automatically resolve
${...}
interpolations when you access their elements (config.key
,config['key']
,config.list[0]
). - Cons: They are custom types, not standard
dict
orlist
.
You can use standard types:
loader = DraconLoader(base_dict_type=dict, base_list_type=list)
config = loader.load("config.yaml")
assert isinstance(config, dict)
# IMPORTANT: With standard types, accessing config['key'] will return
# the LazyInterpolable object itself if the value was '${...}'.
# You need to manually call resolve_all_lazy(config) or handle
# resolution yourself if needed before using the values.
Choose based on whether you prefer automatic lazy resolution or standard Python types. Dracontainer
is generally recommended unless you have specific reasons to use standard types.