How-To: Use Deferred Execution¶
Dracon allows you to delay parts of the configuration processing using DeferredNode
and Resolvable
.
Delaying Node Construction (DeferredNode
)¶
Use !deferred
or DraconLoader(deferred_paths=...)
when you need to postpone the entire construction of a configuration branch, usually because required context (variables, functions) is only available later at runtime.
Scenario: Constructing a database connection string that requires a password fetched after initial config loading.
1. Mark the Node:
# config.yaml
database_config: !deferred:DatabaseConfig # Defer construction of DatabaseConfig
host: db.example.com
port: 5432
username: app_user
# Password needs runtime context
connection_string: "postgresql://${username}:${DB_PASSWORD}@${host}:${port}/mydb"
# Define the Pydantic model (in Python)
# from pydantic import BaseModel
# class DatabaseConfig(BaseModel): ... host, port, username, connection_string ...
2. Load Configuration:
# main.py
import dracon as dr
from models import DatabaseConfig # Your Pydantic model
loader = dr.DraconLoader(context={'DatabaseConfig': DatabaseConfig})
config = loader.load("config.yaml")
# config.database_config is a DeferredNode instance here
print(type(config.database_config)) # <class 'dracon.deferred.DeferredNode'>
3. Provide Context and Construct:
# ... later in your code ...
# Fetch the password (e.g., from a secrets manager)
db_password = get_secret("database_password")
# Provide the missing context and call .construct()
runtime_context = {'DB_PASSWORD': db_password}
final_db_config = config.database_config.construct(context=runtime_context)
# Now final_db_config is the fully constructed DatabaseConfig instance
assert isinstance(final_db_config, DatabaseConfig)
print(final_db_config.connection_string)
# Output: postgresql://app_user:runtime_secret@db.example.com:5432/mydb
# Optionally, replace the deferred node in the main config
config.database_config = final_db_config
Key Points for DeferredNode
:
- Pauses construction of the entire tagged node and its children.
- Captures the YAML node structure and the context available at load time.
- Requires calling
.construct()
manually, providing any missing context. - Useful for late-binding, resource initialization, or conditional construction.
- Can be targeted implicitly using
DraconLoader(deferred_paths=['/path/to/defer'])
. - Use
!deferred::clear_ctx=VAR
or!deferred::clear_ctx
to control context inheritance.
Delaying Value Resolution (Resolvable
)¶
Use Resolvable[T]
(typically via type hints and Arg(resolvable=True)
) when the configuration is mostly loaded, but you need a final step to process or validate a single specific value, often after CLI parsing.
Scenario: A CLI argument for an output file path needs formatting based on another input argument.
1. Define Model with Resolvable
and Arg
:
# models.py
from pydantic import BaseModel
from typing import Annotated
from dracon import Arg, Resolvable
class ProcessingConfig(BaseModel):
input_file: Annotated[str, Arg(positional=True)]
# Output path pattern needs final processing
output_file_pattern: Annotated[Resolvable[str], Arg(
resolvable=True, # Crucial: Tells CLI to wrap in Resolvable
short='o',
help="Output file pattern, e.g., '{input}.out'"
)] = "{input}.processed" # Default pattern
2. Parse CLI Arguments:
# main.py
import dracon as dr
from models import ProcessingConfig
program = dr.make_program(ProcessingConfig, context={'ProcessingConfig': ProcessingConfig})
config, _ = program.parse_args() # e.g., running with: python main.py data.csv -o {input}.result
# config.output_file_pattern is a Resolvable object here
print(type(config.output_file_pattern)) # <class 'dracon.resolvable.Resolvable'>
3. Resolve the Value:
# ... application logic ...
# Call .resolve() on the Resolvable object, providing context if needed
# The context here allows the pattern string itself to use '{input}'
final_output_path = config.output_file_pattern.resolve(
context={'input': config.input_file}
)
# Now final_output_path is the resolved string
print(f"Input: {config.input_file}") # Output: data.csv
print(f"Output Path: {final_output_path}") # Output: data.csv.result
Key Points for Resolvable
:
- Delays the final processing/validation of a single field's value.
- The main configuration object is already constructed.
- Requires calling
.resolve()
manually, providing context if the value's final form depends on it. - Often used with
Arg(resolvable=True)
for CLI arguments needing post-parsing logic. - The
Resolvable
object holds the original YAML node and the expected inner typeT
.
Choose DeferredNode
to delay building a whole component, and Resolvable
to delay finalizing a specific value within an already built structure. See Deferred vs Resolvable Concepts.