Skip to content

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
{
  "host": "db.internal",
  "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"
{
  "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
{
  "host": "db.internal",
  "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,
)
dature.errors.exceptions.DatureError: JsonSource(tag='json') references unknown tag 'vault'. Known tags: 'env', 'json'. Ensure a source with that tag is listed in the same load() call.

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.