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.
- Ordered search paths after home expansion.
- Files that
open()actually reached. - Winner map from key to source label.
- Environment keys that overrode a file.
- Return value when a file is absent.
- 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
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()
Run the harness before touching structure.
python -m pytest test_report_loader_characterization.py -q --tb=short
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"
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.
Inventory every path the loader may open.
Read_candidate_pathsand env names once.
Write that list above the first test.Build a
tmp_pathfixture for those roots.
Never open the real home directory.
Never open the real/etctree.Record opened paths, winners, and values.
Assert types withtype(x) is ...checks.
Do not use loose equality for integers.Add absent-file and invalid JSON cases.
Empty trees protect default labels.
Broken JSON protects current exceptions.Add one extra-key case from a file.
Models often drop unknown keys during cleanup.
That drop is a behavior change.Extract one function and keep the triple.
Moveload_report_configintoconfig.py.
Re-export the same names fromreport_loader.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
# config.py — moved body, same return triple
# paste load_report_config and helpers unchanged
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])"
--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.
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
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)