Debug & Reports#
Pass debug=True to collect a LoadReport:
"""Debug report — get_load_report() shows which source set each field."""
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,
debug=True,
)
report = dature.get_load_report(config)
assert report is not None
origins = report.field_origins
assert len(origins) == 3
assert origins[0].key == "host"
assert origins[0].value == "production.example.com"
assert origins[0].source_index == 1
assert origins[0].source_file == str(SHARED_DIR / "common_overrides.yaml")
assert origins[1].key == "port"
assert origins[1].value == 8080
assert origins[1].source_index == 1
assert origins[1].source_file == str(SHARED_DIR / "common_overrides.yaml")
assert origins[2].key == "tags"
assert origins[2].value == ["web", "api"]
assert origins[2].source_index == 1
assert origins[2].source_file == str(SHARED_DIR / "common_overrides.yaml")
Report Structure#
@dataclass(frozen=True, slots=True, kw_only=True)
class SourceEntry:
index: int
file_path: str | None
loader_type: str
raw_data: JSONValue
@dataclass(frozen=True, slots=True, kw_only=True)
class FieldOrigin:
key: str
value: JSONValue
source_index: int
source_file: str | None
source_loader_type: str
@dataclass(frozen=True, slots=True, kw_only=True)
class LoadReport:
dataclass_name: str
strategy: SourceMergeStrategy | None
sources: tuple[SourceEntry, ...]
field_origins: tuple[FieldOrigin, ...]
merged_data: JSONValue
Debug Logging#
All loading steps are logged at DEBUG level under the "dature" logger regardless of the debug flag. Secret values are automatically masked:
"""Debug logging — loading steps are logged at DEBUG under "dature"."""
import difflib
import io
import logging
from dataclasses import dataclass
from pathlib import Path
import dature
log_stream = io.StringIO()
handler = logging.StreamHandler(log_stream)
handler.setLevel(logging.DEBUG)
logging.getLogger("dature").addHandler(handler)
logging.getLogger("dature").setLevel(logging.DEBUG)
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,
)
log_lines = [
line for line in log_stream.getvalue().splitlines() if "[Config]" in line
]
defaults = str(SHARED_DIR / "common_defaults.yaml")
overrides = str(SHARED_DIR / "common_overrides.yaml")
keys = "['host', 'port', 'tags']"
defaults_data = "{'host': 'localhost', 'port': 3000, 'tags': ['default']}"
overrides_data = (
"{'host': 'production.example.com', 'port': 8080, 'tags': ['web', 'api']}"
)
expected_log_lines = [
f"[Config] Source 0 loaded: loader=yaml1.2, file={defaults}, keys={keys}",
f"[Config] Source 0 raw data: {defaults_data}",
(
"[Config] Merge step 0 (strategy=last_wins): "
"added=['host', 'port', 'tags'], overwritten=[]"
),
f"[Config] State after step 0: {defaults_data}",
f"[Config] Source 1 loaded: loader=yaml1.2, file={overrides}, keys={keys}",
f"[Config] Source 1 raw data: {overrides_data}",
(
"[Config] Merge step 1 (strategy=last_wins): "
"added=[], overwritten=['host', 'port', 'tags']"
),
f"[Config] State after step 1: {overrides_data}",
(
"[Config] Merged result (strategy=last_wins, 2 sources): "
f"{overrides_data}"
),
f"[Config] Field 'host' = 'production.example.com'"
f" <-- source 1 ({overrides})",
f"[Config] Field 'port' = 8080 <-- source 1 ({overrides})",
f"[Config] Field 'tags' = ['web', 'api'] <-- source 1 ({overrides})",
]
diff = difflib.ndiff(expected_log_lines, log_lines)
assert log_lines == expected_log_lines, f"Difference:\n{'\n'.join(diff)}"
Report on Error#
If loading fails with DatureConfigError and debug=True was passed, the report is attached to the dataclass type:
"""Report on error — get_load_report() from the type after a failed load."""
from dataclasses import dataclass
from pathlib import Path
import dature
from dature.errors import DatureConfigError
from dature.strategies.source import SourceLastWins
SOURCES_DIR = Path(__file__).parent / "sources"
SHARED_DIR = Path(__file__).parents[2] / "shared"
@dataclass
class Config:
host: str
port: int
tags: list[str]
try:
config = dature.load(
dature.Yaml12Source(file=SHARED_DIR / "common_overrides.yaml"),
dature.Yaml12Source(
file=SOURCES_DIR / "advanced_debug_error_defaults.yaml",
),
schema=Config,
debug=True,
)
except DatureConfigError:
report = dature.get_load_report(Config)
assert report is not None
assert report.dataclass_name == "Config"
assert isinstance(report.strategy, SourceLastWins)
assert report.merged_data == {
"host": "localhost",
"port": "not_a_number",
"tags": ["default"],
}
assert len(report.sources) == 2
assert report.sources[0].index == 0
assert report.sources[0].loader_type == "yaml1.2"
assert "overrides" in str(report.sources[0].file_path)
assert report.sources[0].raw_data == {
"host": "production.example.com",
"port": 8080,
"tags": ["web", "api"],
}
assert report.sources[1].index == 1
assert report.sources[1].loader_type == "yaml1.2"
assert "advanced_debug_error_defaults" in str(report.sources[1].file_path)
assert report.sources[1].raw_data == {
"host": "localhost",
"port": "not_a_number",
"tags": ["default"],
}
assert len(report.field_origins) == 3
for origin in report.field_origins:
assert origin.source_index == 1
assert "advanced_debug_error_defaults" in str(origin.source_file)
Without debug=True, get_load_report() returns None and emits a warning.