Skip to content

Commit 5de81a3

Browse files
fix: revert load_file to dict return, cache literal-dotted keys per path
- load_file() returns dict again (check/scan callers expect dict not tuple) - New module-level _literal_dotted_cache keyed by resolved path - get_literal_dotted_keys(path) retrieves cached set for fix command - Restore baseline_path = Path(files[0]) before load in fix command - Eliminates NameError on baseline_path and AttributeError on tuple.keys()
1 parent 2a58aaf commit 5de81a3

2 files changed

Lines changed: 41 additions & 20 deletions

File tree

src/configdrift/cli.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def _json_null_handler(obj: Any) -> Any:
4545
Severity,
4646
diff_environments,
4747
)
48-
from configdrift.loader import load_file
48+
from configdrift.loader import get_literal_dotted_keys, load_file
4949

5050
app = typer.Typer(
5151
name="configdrift",
@@ -322,14 +322,15 @@ def fix(
322322
False, "--dry-run", "-n", help="Show what would change without modifying files."
323323
),
324324
) -> None:
325+
baseline_path = Path(files[0])
326+
if not baseline_path.exists():
327+
console.print(f"[red]ERROR: Baseline file not found: {baseline_path}[/red]")
328+
raise typer.Exit(code=1)
325329
try:
326-
baseline_data, baseline_literal_dotted = load_file(str(baseline_path))
330+
baseline_data = load_file(str(baseline_path))
327331
except Exception as e:
328332
console.print(f"[red]Error loading baseline config: {e}[/red]")
329333
raise typer.Exit(code=1) from e
330-
if not baseline_path.exists():
331-
console.print(f"[red]ERROR: Baseline file not found: {baseline_path}[/red]")
332-
raise typer.Exit(code=1)
333334

334335

335336
# Track targets that could not be fixed so the command returns a
@@ -346,7 +347,7 @@ def fix(
346347
continue
347348

348349
try:
349-
target_data, target_literal_dotted = load_file(str(target_path))
350+
target_data = load_file(str(target_path))
350351
except Exception as e:
351352
console.print(f"[red]Error loading target config {target_path}: {e}[/red]")
352353
failed_targets.append(str(target_path))
@@ -488,7 +489,7 @@ def fix(
488489
# source document are kept as single mapping keys rather
489490
# than being re-split into nested levels.
490491
# Merge literal dotted keys from both baseline and target
491-
all_literal_dotted = baseline_literal_dotted | target_literal_dotted
492+
all_literal_dotted = get_literal_dotted_keys(str(baseline_path)) | get_literal_dotted_keys(str(target_path))
492493
nested: dict[str, Any] = {}
493494
for k, v in target_data.items():
494495
if "." not in k or k in all_literal_dotted:
@@ -504,7 +505,7 @@ def fix(
504505
atomic_write_text(target_path, _json.dumps(nested, indent=2, default=_json_null_handler) + "\n")
505506
elif ext in (".yaml", ".yml"):
506507
# Reconstruct nested structure from flat keys for YAML output
507-
all_literal_dotted = baseline_literal_dotted | target_literal_dotted
508+
all_literal_dotted = get_literal_dotted_keys(str(baseline_path)) | get_literal_dotted_keys(str(target_path))
508509
nested: dict[str, Any] = {}
509510
for k, v in target_data.items():
510511
if "." not in k or k in all_literal_dotted:
@@ -542,7 +543,7 @@ def _has_null(val: Any) -> bool:
542543
)
543544
failed_targets.append(str(target_path))
544545
continue
545-
all_literal_dotted = baseline_literal_dotted | target_literal_dotted
546+
all_literal_dotted = get_literal_dotted_keys(str(baseline_path)) | get_literal_dotted_keys(str(target_path))
546547
nested_toml: dict[str, Any] = {}
547548
for k, v in target_data.items():
548549
if "." not in k or k in all_literal_dotted:

src/configdrift/loader.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,56 @@
66
from pathlib import Path
77
from typing import Any
88

9+
_literal_dotted_cache: dict[str, set[str]] = {}
10+
11+
12+
def get_literal_dotted_keys(path: str) -> set[str]:
13+
"""Retrieve literal dotted keys stored by the most recent load_file(path) call."""
14+
return _literal_dotted_cache.get(str(Path(path).resolve()), set())
15+
16+
917
_toml = importlib.import_module("tomllib" if __import__("sys").version_info >= (3, 11) else "tomli")
1018

1119

12-
def load_file(path: str) -> tuple[dict[str, Any], set[str]]:
20+
def load_file(path: str) -> dict[str, Any]:
1321
"""Load a config file based on its extension.
1422
15-
Returns a tuple of (flat_data, literal_dotted_keys) where
16-
literal_dotted_keys is the set of top-level keys that already
17-
contained dots in the original document.
23+
Returns flat data with dot-separated keys for nested values.
24+
Literal dotted keys (top-level keys already containing dots in the
25+
source document) are stored internally and can be retrieved via
26+
``get_literal_dotted_keys(path)`` — used by the ``fix`` command to
27+
avoid re-splitting them during reconstruction.
1828
"""
1929
p = Path(path)
2030
ext = p.suffix.lower()
31+
resolved = str(p.resolve())
2132
if ext in (".yaml", ".yml"):
22-
return _load_yaml(p)
33+
result, literal = _load_yaml(p)
34+
_literal_dotted_cache[resolved] = literal
35+
return result
2336
elif ext == ".json":
24-
return _load_json(p)
37+
result, literal = _load_json(p)
38+
_literal_dotted_cache[resolved] = literal
39+
return result
2540
elif ext == ".toml":
26-
return _load_toml(p)
41+
result, literal = _load_toml(p)
42+
_literal_dotted_cache[resolved] = literal
43+
return result
2744
elif ext == ".env":
28-
return _load_dotenv(p), set()
45+
_literal_dotted_cache[resolved] = set()
46+
return _load_dotenv(p)
2947
else:
30-
# Try known parsers in order
3148
for loader in [_load_yaml, _load_json, _load_toml]:
3249
try:
33-
return loader(p)
50+
result, literal = loader(p)
51+
_literal_dotted_cache[resolved] = literal
52+
return result
3453
except Exception:
3554
continue
3655
# Last resort: try dotenv
3756
try:
38-
return _load_dotenv(p), set()
57+
_literal_dotted_cache[resolved] = set()
58+
return _load_dotenv(p)
3959
except Exception:
4060
pass
4161
raise ValueError(f"Unsupported file format: {ext}") from None

0 commit comments

Comments
 (0)