Skip to content

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")
host: "localhost"
port: 3000
tags:
  - "default"
host: "production.example.com"
port: 8080
tags:
  - "web"
  - "api"

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)}"
host: "localhost"
port: 3000
tags:
  - "default"
host: "production.example.com"
port: 8080
tags:
  - "web"
  - "api"

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)
host: "production.example.com"
port: 8080
tags:
  - "web"
  - "api"
host: "localhost"
port: "not_a_number"
tags:
  - "default"

Without debug=True, get_load_report() returns None and emits a warning.