DEV Community

Dakota Huang
Dakota Huang

Posted on

Pin Config Search Order and Winner Keys Before One Loader Split

Do not extract a settings helper on the first patch.
Pin discovery order and winner keys first.
A model-led split stays unsafe without that freeze.

Why messy loaders drift

God scripts often search too many config locations.
Home files still fight checked-in repo files.
Environment keys still fight nested JSON keys.

A clean extract often changes the silent winner.
Readers then debug production instead of tests.
The failure looks like a missing default.

The real defect is a reordered search path.
Silent file presence also changes later merge results.
You need order and winners stored on disk.

Freeze these six observables

Record all six before any file move.

  1. Ordered search paths after home expansion.
  2. Files that open() actually reached.
  3. Winner map from key to source label.
  4. Environment keys that overrode a file.
  5. Return value when a file is absent.
  6. Exception type when JSON is invalid.

Skip subjective "looks equivalent" reviews here.
The winner map is the public contract.
Search order explains every surprising winner.

Also pin value types after each override.
An env string can replace an int quietly.
That type change is a break, not cleanup.

Sample god loader

The next module is a labeled example.
It is not production code from this account.
It mixes defaults, files, and environment overrides.

# report_loader.py — example under test
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

DEFAULTS: dict[str, Any] = {
    "format": "text",
    "limit": 100,
    "verbose": False,
}


def _candidate_paths() -> list[Path]:
    env_path = os.environ.get("REPORT_CONFIG", "").strip()
    paths = [
        Path("/etc/acme/report.json"),
        Path.home() / ".acme" / "report.json",
        Path.cwd() / "report.json",
    ]
    if env_path:
        paths.append(Path(env_path))
    return paths


def load_report_config() -> tuple[dict[str, Any], dict[str, str], list[str]]:
    """Return (config, winners, opened_paths)."""
    cfg = dict(DEFAULTS)
    winners = {key: "defaults" for key in cfg}
    opened: list[str] = []
    for path in _candidate_paths():
        if not path.is_file():
            continue
        opened.append(str(path.resolve()))
        with path.open(encoding="utf-8") as handle:
            payload = json.load(handle)
        if not isinstance(payload, dict):
            raise ValueError(f"config must be an object: {path}")
        for key, value in payload.items():
            cfg[key] = value
            winners[key] = str(path.resolve())
    for key in list(cfg):
        env_key = f"REPORT_{key.upper()}"
        if env_key in os.environ:
            cfg[key] = os.environ[env_key]
            winners[key] = env_key
    return cfg, winners, opened
Enter fullscreen mode Exit fullscreen mode

Invalid JSON should surface as json.JSONDecodeError.
Missing files must not raise in this example.
Unknown keys from files are kept on purpose.

Environment values stay strings in this loader.
REPORT_LIMIT=99 does not become integer 99.
Callers may already branch on isinstance checks.

Winner matrix before any extract

Use this table as the first oracle.
Fill cells from the harness, not memory.
A later extract must reprint the same cells.

Setup Key Winner Value Type
no files format defaults text str
no files limit defaults 100 int
later cwd file only limit cwd report.json 30 int
etc then home then cwd format etc file json str
etc then home then cwd limit cwd file 30 int
etc then home then cwd verbose home file True bool
cwd file plus REPORT_LIMIT limit REPORT_LIMIT "99" str
extra file key color color that file red str
missing files opened list n/a [] list
invalid JSON in cwd n/a n/a JSONDecodeError exception

Later files win per key, not per file.
Environment labels beat every file label.
Absent files leave default labels untouched.

Characterization harness

Keep the first harness in-process.
Subprocess tests hide open() order details.
tmp_path plus monkeypatch give path isolation.

Patch _candidate_paths in the tests.
Do not fight real /etc or $HOME.
The production function stays messy on purpose.

# test_report_loader_characterization.py — example harness
from __future__ import annotations

import json
from pathlib import Path

import pytest

import report_loader


def _write(path: Path, payload: dict) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload), encoding="utf-8")
    return path.resolve()


@pytest.fixture
def roots(tmp_path, monkeypatch):
    etc = tmp_path / "etc.json"
    home = tmp_path / "home.json"
    cwd = tmp_path / "cwd.json"
    extra = tmp_path / "extra.json"
    monkeypatch.delenv("REPORT_CONFIG", raising=False)
    monkeypatch.delenv("REPORT_FORMAT", raising=False)
    monkeypatch.delenv("REPORT_LIMIT", raising=False)
    monkeypatch.delenv("REPORT_VERBOSE", raising=False)
    monkeypatch.delenv("REPORT_COLOR", raising=False)
    monkeypatch.setattr(
        report_loader,
        "_candidate_paths",
        lambda: [etc, home, cwd],
    )
    return {"etc": etc, "home": home, "cwd": cwd, "extra": extra}


def test_missing_files_keep_defaults(roots):
    cfg, winners, opened = report_loader.load_report_config()
    assert opened == []
    assert cfg == {"format": "text", "limit": 100, "verbose": False}
    assert winners == {
        "format": "defaults",
        "limit": "defaults",
        "verbose": "defaults",
    }


def test_later_file_wins_per_key(roots):
    etc = _write(roots["etc"], {"format": "json", "limit": 10})
    home = _write(roots["home"], {"limit": 20, "verbose": True})
    cwd = _write(roots["cwd"], {"limit": 30})
    cfg, winners, opened = report_loader.load_report_config()
    assert opened == [str(etc), str(home), str(cwd)]
    assert cfg["format"] == "json"
    assert cfg["limit"] == 30
    assert cfg["verbose"] is True
    assert winners["format"] == str(etc)
    assert winners["limit"] == str(cwd)
    assert winners["verbose"] == str(home)
    assert type(cfg["limit"]) is int


def test_env_override_keeps_string_type(roots, monkeypatch):
    cwd = _write(roots["cwd"], {"limit": 30, "format": "csv"})
    monkeypatch.setenv("REPORT_LIMIT", "99")
    cfg, winners, opened = report_loader.load_report_config()
    assert opened == [str(cwd)]
    assert cfg["limit"] == "99"
    assert type(cfg["limit"]) is str
    assert winners["limit"] == "REPORT_LIMIT"
    assert winners["format"] == str(cwd)


def test_unknown_file_key_is_kept(roots):
    cwd = _write(roots["cwd"], {"color": "red"})
    cfg, winners, _opened = report_loader.load_report_config()
    assert cfg["color"] == "red"
    assert winners["color"] == str(cwd)


def test_invalid_json_raises_decode_error(roots):
    roots["cwd"].write_text("{not json", encoding="utf-8")
    with pytest.raises(json.JSONDecodeError):
        report_loader.load_report_config()
Enter fullscreen mode Exit fullscreen mode

Run the harness before touching structure.

python -m pytest test_report_loader_characterization.py -q --tb=short
Enter fullscreen mode Exit fullscreen mode

Commit that pin as its own change.
Do not mix the later extract into it.
Diff review stays small and mechanical.

git add report_loader.py test_report_loader_characterization.py
git commit -m "test: pin config search order and winner keys"
Enter fullscreen mode Exit fullscreen mode

Seven steps for the smallest split

Follow this order on a messy repo.
Skip a step only with a written reason.
The harness remains the only merge gate.

  1. Inventory every path the loader may open.
    Read _candidate_paths and env names once.
    Write that list above the first test.

  2. Build a tmp_path fixture for those roots.
    Never open the real home directory.
    Never open the real /etc tree.

  3. Record opened paths, winners, and values.
    Assert types with type(x) is ... checks.
    Do not use loose equality for integers.

  4. Add absent-file and invalid JSON cases.
    Empty trees protect default labels.
    Broken JSON protects current exceptions.

  5. Add one extra-key case from a file.
    Models often drop unknown keys during cleanup.
    That drop is a behavior change.

  6. Extract one function and keep the triple.
    Move load_report_config into config.py.
    Re-export the same names from report_loader.

  7. Re-run the same file and stop on drift.
    Any winner label change fails the extract.
    Restore the tree and shrink the diff.

Allowed patch after the harness is green:

# report_loader.py — after the smallest extract
from config import load_report_config, DEFAULTS  # noqa: F401
Enter fullscreen mode Exit fullscreen mode
# config.py — moved body, same return triple
# paste load_report_config and helpers unchanged
Enter fullscreen mode Exit fullscreen mode

Then confirm the surface did not move.

git diff --stat
python -m pytest test_report_loader_characterization.py -q
python -c "from report_loader import load_report_config; print(load_report_config()[1])"
Enter fullscreen mode Exit fullscreen mode

--stat should list two files, not twelve.
New helpers are out of scope for this patch.
Schema validation waits for a later commit.

What a coding model may touch

Coding models now make file splits cheap.
Cheap splits still reorder search paths.
Cheap splits still coerce env strings to ints.

Feed the failing or green harness as input.
Ask for one function extract only.
Reject extra logging and extra defaults.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.
A free coding model can draft that extract.
A free server can run the harness when the laptop is noisy.
Neither replaces the winner matrix above.

Prompt the model with a tight contract.

Extract load_report_config into config.py.
Keep the return triple and key names.
Do not coerce env values.
Do not drop unknown keys.
Do not change candidate order.
Stop when test_report_loader_characterization.py is green.
Enter fullscreen mode Exit fullscreen mode

If the model adds int(os.environ[...]), fail it.
The matrix already pinned "99" as str.
Fixing that coercion is a second change.

Commands that catch silent drift

Print winners during local debugging.
Do not leave prints in the extract commit.

python - <<'PY'
from report_loader import load_report_config
cfg, winners, opened = load_report_config()
print("opened", opened)
print("winners", winners)
print("types", {k: type(v).__name__ for k, v in cfg.items()})
PY
Enter fullscreen mode Exit fullscreen mode

Compare that print to the committed table.
A new path in opened is a contract break.
A relabeled winner is also a contract break.

Limitations

This harness does not pin wall clocks.
This harness does not pin argv parsing.
This harness does not pin byte counts on reports.

In-process tests miss import-time file opens.
They also miss process-wide env leaks across tests.
Use monkeypatch and one loader import path.

JSON objects are the only file format here.
YAML merge keys need a separate matrix.
TOML type rules need a separate matrix.

Winner labels use resolved path strings.
Symlinks can change those strings across machines.
Keep fixtures inside tmp_path to avoid that.

The example stores no secrets in snapshots.
Do not pin API tokens as winner values.
Redact any accidental secret before commit.

Who should not use this approach

Skip this if settings already live in one type.
Skip this if precedence must change on purpose.
Skip this if files are encrypted at rest.

Do not combine a bugfix with the extract.
The string limit bug waits until later.
One behavior change per commit stays reviewable.

Teams without tests should still start here.
Start with these six observables, nothing else.
Then extract one function, not a new framework.

MonkeyCode provides free models that can run this workflow.

Top comments (0)