Cross-Source References#
Sources can reference values from other sources using the ${@tag.key} syntax. This lets you pass a Vault token from an env variable, resolve a config file path from a CLI argument, or build connection strings that combine several sources — without imperative glue code.
Quick start#
"""Cross-source references — quick start."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ["APP_CONFIG_PATH"] = str(
Path(__file__).parent / "sources" / "app.json"
)
@dataclass
class AppConfig:
host: str = "localhost"
port: int = 8080
# Sources can be listed in any order — dature builds a dependency graph from
# ${@tag.key} patterns and loads them in topological order automatically.
# JsonSource is listed first but loaded second because it depends on EnvSource.
#
# The "env" in ${@env.config_path} is the source tag. By default the tag
# equals the source type's format_name ("env" for EnvSource, "json" for
# JsonSource). Set tag= explicitly when you have two sources of the same type.
cfg = dature.load(
dature.JsonSource(file="${@env.config_path}"),
dature.EnvSource(prefix="APP_"),
schema=AppConfig,
)
assert cfg.host == "db.internal"
assert cfg.port == 5432
The env in ${@env.config_path} is the source tag. By default the tag equals the source type's format_name — "env" for EnvSource, "json" for JsonSource, and so on. Set tag= explicitly when you have two sources of the same type.
Sources can be passed in any order: dature builds a dependency graph from ${@tag.key} patterns and loads them in topological order automatically. In the example above JsonSource is listed first but loaded second because it depends on EnvSource.
Syntax#
| Pattern | Result |
|---|---|
${@tag.key} | Value of key in the source tagged tag |
${@tag.section.key} | Nested key path (dot-separated) |
${@tag.key:-default} | Use default if key is absent |
$${@tag.key} | Literal ${@tag.key} (escape with $$) |
Escaping#
Prefix $$ to produce a literal $. This is useful when a config-file path should contain ${@...} literally rather than be treated as a cross-ref:
"""Cross-source references — $$ escaping in init-fields."""
from dataclasses import dataclass
from pathlib import Path
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
@dataclass
class Config:
value: str = ""
# SOURCES_DIR contains a file literally named "${@env.something}".
# $${@env.something} in the file= argument escapes to "${@env.something}",
# so dature opens the file by that exact name instead of treating it as a ref.
cfg = dature.load(
dature.JsonSource(file=str(SOURCES_DIR / "$${@env.something}")),
schema=Config,
)
assert cfg.value == "hello"
T-string syntax (Python 3.14+)#
Requires Python 3.14+
T-strings (PEP 750) are a Python 3.14 language feature. On earlier versions use the ${@tag.key} string syntax instead.
from dature import ref gives you a proxy object. ref.tag.key inside a t-string is exactly equivalent to "${@tag.key}" as a plain string:
"""Cross-source references — t-string syntax (Python 3.14+).
Equivalent to the ${@tag.key} string syntax but uses native template literals.
Requires Python 3.14+ (PEP 750).
"""
import os
import sys
from dataclasses import dataclass
from pathlib import Path
if sys.version_info < (3, 14):
raise SystemExit("t-string syntax requires Python 3.14+")
import dature
from dature import ref
SOURCES_DIR = Path(__file__).parent / "sources"
os.environ["APP_CONFIG_PATH"] = str(SOURCES_DIR / "app.json")
@dataclass
class AppConfig:
host: str = "localhost"
port: int = 8080
# t"{ref.env.config_path}" ≡ "${@env.config_path}"
# t"{ref.env.log_level:INFO}" ≡ "${@env.log_level:-INFO}"
cfg = dature.load(
dature.JsonSource(file=t"{ref.env.config_path}"),
dature.EnvSource(prefix="APP_"),
schema=AppConfig,
)
assert cfg.host == "db.internal"
assert cfg.port == 5432
The format spec becomes the default value — t"{ref.env.log_level:INFO}" is the same as "${@env.log_level:-INFO}".
Error messages#
Unknown tag#
"""Cross-source references — error: unknown tag."""
from dataclasses import dataclass
import dature
@dataclass
class Config:
host: str = ""
port: int = 8080
# 'vault' is referenced but no VaultSource (or any source tagged 'vault')
# is listed in the load() call — dature raises immediately.
dature.load(
dature.EnvSource(),
dature.JsonSource(file="${@vault.config_path}"),
schema=Config,
)
Cycle#
"""Cross-source references — error: cycle."""
from dataclasses import dataclass
import dature
@dataclass
class Config:
host: str = ""
# EnvSource depends on JsonSource (via prefix) and JsonSource depends on
# EnvSource (via file) — dature detects the cycle and raises immediately.
dature.load(
dature.EnvSource(prefix="${@json.prefix_key}"),
dature.JsonSource(file="${@env.config_path}"),
schema=Config,
)
dature.errors.exceptions.DatureError: Cross-source reference cycle detected:
EnvSource(tag='env') → references ${@json.prefix_key}
JsonSource(tag='json') → references ${@env.config_path}
closes back to EnvSource(tag='env')
Sources cannot reference each other's data in a cycle. Break the cycle by hardcoding one side or parsing one source imperatively.
Tag collision#
"""Cross-source references — error: tag collision."""
from dataclasses import dataclass
import dature
@dataclass
class Config:
host: str = ""
# Both EnvSources resolve to tag='env' (their default format_name).
# As long as 'env' is referenced by another source, dature raises.
dature.load(
dature.EnvSource(prefix="APP_"),
dature.EnvSource(prefix="DB_"),
dature.JsonSource(file="${@env.config_path}"),
schema=Config,
)
dature.errors.exceptions.DatureError: Tag collision: multiple sources share resolved_tag='env':
EnvSource(prefix='APP_', expand_env_vars='default', nested_resolve_strategy='flat')
EnvSource(prefix='DB_', expand_env_vars='default', nested_resolve_strategy='flat')
Set an explicit tag= on at least one of them.