Conditional Sources#
Use when= to include a source only when a condition is met. A source that doesn't match is skipped entirely — it never touches the filesystem, the network, or the dependency graph.
Quick start#
Set when= to a mapping of template-string keys to expected values. The source is enabled if every key expands to the expected value.
"""Conditional sources — dev environment."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ["APP_ENV"] = "dev"
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when={"${APP_ENV}": ("dev", "local")},
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Keys support the same ${VAR} and ${@tag.key} expansion syntax as source init-fields. when=None or when={} (the default) means always enabled.
Allowing multiple values#
Pass a tuple to accept any of several values:
"""Conditional sources — multiple allowed values (tuple)."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ["APP_ENV"] = "local"
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when={"${APP_ENV}": ("dev", "local")}, # enabled for either value
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
APP_ENV=local matches ("dev", "local"), so the source is enabled.
Combining conditions (AND)#
List multiple keys to require all of them to match simultaneously:
"""Conditional sources — multiple keys (AND semantics)."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ["APP_ENV"] = "prod"
os.environ["REGION"] = "eu"
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when={
"${APP_ENV}": "prod",
"${REGION}": ("eu", "us"), # both keys must match
},
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
The source is enabled only when both APP_ENV=prod and REGION is eu or us. If either key doesn't match, the source is skipped.
Defaults for unset variables#
Use ${VAR:-default} when the variable may not be set. Both when= keys must share the same default so the conditions stay mutually exclusive:
"""Conditional sources — APP_ENV not set, using ${VAR:-default}."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ.pop("APP_ENV", None)
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(tag="secrets", when={"${APP_ENV:-dev}": "prod"}),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when={"${APP_ENV:-dev}": ("dev", "local")},
),
schema=SecretsConfig,
)
# APP_ENV unset → default "dev" matches → token from file
assert cfg.vault_token == "dev-token-from-file"
Both keys expand to "dev" when APP_ENV is unset — exactly one source is enabled, no collision.
Error: all sources filtered out#
Without a :-default, an unset variable expands to "", which matches nothing. If every source is conditional and none matches, dature raises immediately:
"""Conditional sources — error: all sources filtered out.
APP_ENV is not set, so ${APP_ENV} expands to "" which matches neither "prod"
nor ("dev", "local"). dature raises DatureError at construction time.
"""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ.pop("APP_ENV", None)
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class SecretsConfig:
vault_token: str = ""
dature.load(
dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
dature.EnvFileSource(
tag="secrets", file=dev_env_path, when={"${APP_ENV}": ("dev", "local")}
),
schema=SecretsConfig,
)
Switching environments#
The same pattern scales to prod. In prod the token is injected into the process environment by the deployment platform; in dev it comes from a local file. The dature.load() call is identical in both environments:
"""Conditional sources — prod environment."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ["APP_ENV"] = "prod"
os.environ["VAULT_TOKEN"] = (
"prod-token-from-env" # injected by the platform in real deployments
)
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when={"${APP_ENV}": ("dev", "local")},
),
schema=SecretsConfig,
)
assert cfg.vault_token == "prod-token-from-env"
"""Conditional sources — dev environment."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ["APP_ENV"] = "dev"
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when={"${APP_ENV}": ("dev", "local")},
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Because when= conditions are mutually exclusive, only one source is ever active and both sources can safely share the same tag="secrets".
Toggle from another source#
Use ${@tag.key} as a when= key when the toggle value lives in a file or another source rather than in an OS environment variable:
"""Conditional sources — toggle from another source.
The toggle value lives in config.json, not in an OS env var.
JsonSource loads first; its "env" key drives the when= of EnvFileSource.
"""
from dataclasses import dataclass
from pathlib import Path
import dature
cfg_path = Path(__file__).parent / "sources" / "config.json"
vault_dev_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class AppConfig:
vault_token: str = ""
cfg = dature.load(
dature.JsonSource(tag="cfg", file=str(cfg_path)),
dature.EnvFileSource(
tag="secrets",
file=str(vault_dev_path),
when={"${@cfg.env}": ("dev", "local")},
),
schema=AppConfig,
)
# cfg.env == "dev" → EnvFileSource enabled → token from file
assert cfg.vault_token == "dev-token-from-file"
JsonSource loads first; its env key drives the when= of EnvFileSource. Unlike ${VAR} (resolved when load() is called), ${@tag.key} is resolved after the referenced source loads.
Referencing a disabled source#
A source disabled by a ${@tag.key}-based when= still occupies its tag slot in the dependency graph. Its data is empty, so a cross-ref to it without a default raises. Use :- to provide a fallback:
"""Conditional sources — referencing a disabled source with a fallback.
config.json contains {"env": "dev"}. The "secrets" source is disabled lazily
(its when= depends on ${@cfg.env}) because env != "prod".
Disabled sources still occupy their tag slot in the dependency graph, so
${@secrets.remote_config} is a valid reference — it just resolves to absent.
The :- default kicks in and points to the local config.json instead.
"""
from dataclasses import dataclass
from pathlib import Path
import dature
config_path = Path(__file__).parent / "sources" / "config.json"
@dataclass
class AppConfig:
env: str = ""
cfg = dature.load(
dature.JsonSource(tag="cfg", file=str(config_path)), # {"env": "dev"}
dature.EnvSource(
tag="secrets", when={"${@cfg.env}": "prod"}
), # disabled: "dev" != "prod"
dature.JsonSource(
file=f"${{@secrets.remote_config:-{config_path}}}"
), # fallback fires
schema=AppConfig,
)
# secrets disabled → remote_config absent → fallback to config.json → env="dev"
assert cfg.env == "dev"
secrets is disabled (cfg.env is "dev", not "prod"), so ${@secrets.remote_config} is absent — the :- default fires instead.
Same tag, different conditions#
when= enables or disables a Source instance as a whole. Multiple sources can share the same tag= as long as their conditions are mutually exclusive — at most one is active at a time. Use separate instances with different prefix= or field_mapping= to load different subsets of keys conditionally:
"""Conditional sources — same tag, different conditions.
when= enables or disables a Source instance as a whole. To load some keys
unconditionally and others only in a specific environment, use separate Source
instances with different prefixes or field_mapping= targeting different subsets.
Here base.env (DB_HOST, PORT) is always loaded, while the vault token comes
from the OS environment in prod and from a local file in dev.
"""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
os.environ["APP_ENV"] = "dev"
base_env_path = Path(__file__).parent / "sources" / "base.env"
vault_dev_path = Path(__file__).parent / "sources" / "vault_dev.env"
@dataclass
class AppConfig:
db_host: str = ""
port: int = 8080
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(file=str(base_env_path)), # always — DB_HOST, PORT
dature.EnvSource(
tag="secrets", when={"${APP_ENV}": "prod"}
), # prod — VAULT_TOKEN from env
dature.EnvFileSource( # dev — VAULT_TOKEN from file
tag="secrets",
file=str(vault_dev_path),
when={"${APP_ENV}": ("dev", "local")},
),
schema=AppConfig,
)
assert cfg.db_host == "db.internal"
assert cfg.port == 5432
assert cfg.vault_token == "dev-token-from-file"
base.env is always loaded (no when=); only the secrets source switches.
Error: tag collision#
If conditions overlap, two sources with the same explicit tag= are both enabled — dature raises DatureError at construction time:
"""Conditional sources — error: tag collision (explicit tag=).
APP_ENV is not set. Both when= conditions fire simultaneously because they
use different defaults, leaving two sources enabled under the same explicit
tag="secrets".
Unlike a tag collision caused by ${@tag.key} references, dature detects
this at construction time whenever tag= is set explicitly — no consumer
source needed.
Fix: use the same default in both keys — see the no APP_ENV example.
"""
import os
from dataclasses import dataclass
import dature
os.environ.pop("APP_ENV", None)
@dataclass
class SecretsConfig:
vault_token: str = ""
dature.load(
dature.EnvSource(tag="secrets", when={"${APP_ENV:-prod}": "prod"}),
dature.EnvFileSource(
tag="secrets",
file="sources/vault_dev.env",
when={"${APP_ENV:-dev}": ("dev", "local")},
),
schema=SecretsConfig,
)
The same collision can appear with auto-tags (no explicit tag=): two sources of the same type share the same auto-tag, and the collision is detected only when a downstream source references that tag:
"""Conditional sources — error: tag collision (auto-tag, consumer reference).
Two EnvSources share the same auto-tag "env" (no tag= set explicitly).
Without a consumer, dature does not notice — but when VaultSource references
${@env.VAULT_TOKEN}, the ambiguous tag is detected and DatureError is raised.
Fix: assign an explicit tag= to at least one EnvSource.
"""
from dataclasses import dataclass
import dature
@dataclass
class AppConfig:
vault_token: str = ""
dature.load(
dature.EnvSource(), # auto-tag "env"
dature.EnvSource(prefix="BACKUP_"), # auto-tag "env" — collision!
dature.VaultSource(path="secret/app", token="${@env.VAULT_TOKEN}"), # noqa: S106
schema=AppConfig,
)
Fix: use the same :-default in both when= keys so exactly one condition matches when the variable is unset.
Interaction with skip_if_broken#
when= filtering runs before any I/O: a source that doesn't match its when= condition is never opened, never loaded, and never considered broken. skip_if_broken=True (or the skip_broken_sources=True load-level flag) only applies to sources that pass the when= gate and then raise during loading (e.g. file not found). In other words, when=False always takes priority over skip_if_broken.
Syntax reference#
| Key form | Section |
|---|---|
"${APP_ENV}": "prod" | Quick start |
"${APP_ENV}": ("dev", "local") | Multiple values |
"${APP_ENV:-dev}": "prod", two keys with same default | Defaults for unset variables |
"${A}": …, "${B}": … — multiple keys | Combining conditions |
"${@tag.key}": "prod" | Toggle from another source |