Merging#
Load configuration from multiple sources and merge them into one dataclass.
Basic Merging#
Pass multiple Source objects to dature.load():
"""Basic merging — Merge with two YAML sources."""
from dataclasses import dataclass
from pathlib import Path
import dature
SHARED_DIR = Path(__file__).parents[2] / "shared"
@dataclass
class Config:
host: str
port: int
tags: list[str]
config = dature.load(
dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
schema=Config,
strategy="last_wins",
)
assert config.host == "production.example.com"
assert config.port == 8080
assert config.tags == ["web", "api"]
Multiple Sources#
Multiple sources use "last_wins" by default:
"""Multiple sources — implicit LAST_WINS merge."""
from dataclasses import dataclass
from pathlib import Path
import dature
SHARED_DIR = Path(__file__).parents[2] / "shared"
@dataclass
class Config:
host: str
port: int
tags: list[str]
config = dature.load(
dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
schema=Config,
)
assert config.host == "production.example.com"
assert config.port == 8080
assert config.tags == ["web", "api"]
Works as a decorator too:
"""Multiple sources as a decorator — implicit LAST_WINS merge."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
SHARED_DIR = Path(__file__).parents[2] / "shared"
os.environ["APP_HOST"] = "env_localhost"
@dature.load(
dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
dature.EnvSource(prefix="APP_"),
)
@dataclass
class Config:
host: str
port: int
debug: bool = False
config = Config()
assert config.host == "env_localhost"
assert config.port == 3000
assert config.debug is False
Merge Strategies#
| Strategy | Behavior |
|---|---|
"last_wins" | Last source overrides (default) |
"first_wins" | First source wins |
"first_found" | Uses the first source that loads successfully, skips broken sources automatically |
"raise_on_conflict" | Raises MergeConflictError if the same key appears in multiple sources with different values |
Nested dicts are merged recursively. Lists and scalars are replaced entirely according to the strategy.
Last source overrides earlier ones. This is the default strategy.
"""LAST_WINS — last source overrides earlier ones."""
from dataclasses import dataclass
from pathlib import Path
import dature
SHARED_DIR = Path(__file__).parents[2] / "shared"
@dataclass
class Config:
host: str
port: int
tags: list[str]
config = dature.load(
dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
schema=Config,
strategy="last_wins",
)
assert config.host == "production.example.com"
assert config.port == 8080
assert config.tags == ["web", "api"]
First source wins on conflict. Later sources only fill in missing keys.
"""FIRST_WINS — first source wins on conflict."""
from dataclasses import dataclass
from pathlib import Path
import dature
SHARED_DIR = Path(__file__).parents[2] / "shared"
@dataclass
class Config:
host: str
port: int
tags: list[str]
config = dature.load(
dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
schema=Config,
strategy="first_wins",
)
assert config.host == "localhost"
assert config.port == 3000
assert config.tags == ["default"]
Uses the first source that loads successfully and ignores the rest. Broken sources (missing file, parse error) are skipped automatically — no skip_if_broken needed. Type errors (wrong type, missing field) are not skipped.
"""FIRST_FOUND — use the first source that loads successfully."""
from dataclasses import dataclass
from pathlib import Path
import dature
SHARED_DIR = Path(__file__).parents[2] / "shared"
@dataclass
class Config:
host: str
port: int
tags: list[str]
config = dature.load(
dature.Yaml12Source(file=SHARED_DIR / "nonexistent.yaml"),
dature.Yaml12Source(file=SHARED_DIR / "common_defaults.yaml"),
dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
schema=Config,
strategy="first_found",
)
# nonexistent.yaml is skipped, common_defaults.yaml is used entirely
assert config.host == "localhost"
assert config.port == 3000
assert config.tags == ["default"]
Raises MergeConflictError if the same key appears in multiple sources with different values. Works best when sources have disjoint keys.
"""RAISE_ON_CONFLICT — raises if the same key has different values."""
from dataclasses import dataclass
from pathlib import Path
import dature
SHARED_DIR = Path(__file__).parents[2] / "shared"
@dataclass
class Config:
host: str
port: int
debug: bool
config = dature.load(
dature.Yaml12Source(file=SHARED_DIR / "common_raise_on_conflict_a.yaml"),
dature.Yaml12Source(file=SHARED_DIR / "common_raise_on_conflict_b.yaml"),
schema=Config,
strategy="raise_on_conflict",
)
# Disjoint keys — no conflict
assert config.host == "localhost"
assert config.port == 3000
assert config.debug is True
strategy is not limited to the names above — any object implementing the SourceMergeStrategy Protocol is accepted, so you can plug in your own merge logic (e.g. let env sources override files unconditionally) while still composing the built-in strategies. See Custom Source Strategy.
For per-field strategy overrides, see Per-Field Merge Strategies. To enforce that related fields are always overridden together, see Field Groups.
Merge Parameters#
All merge-related parameters are passed directly to dature.load() as keyword arguments:
| Parameter | Description |
|---|---|
strategy | Global merge strategy. Default: "last_wins". See Merge Strategies |
field_merges | Per-field merge strategy overrides. See Per-Field Merge Strategies |
field_groups | Enforce related fields are overridden together. See Field Groups |
skip_broken_sources | Skip sources that fail to load. See Skipping Broken Sources |
skip_invalid_fields | Drop fields with invalid values. See Skipping Invalid Fields |
expand_env_vars | ENV variable expansion mode. See ENV Expansion |
secret_field_names | Extra secret name patterns for masking. See Masking |
mask_secrets | Enable/disable secret masking for all sources. See Masking |
nested_resolve_strategy | Default priority when both JSON and flat keys exist: "flat" (default) or "json". Applies to all sources. See Nested Resolve |
nested_resolve | Default per-field strategy overrides for all sources. See Nested Resolve |