Coverage for src / lilbee / settings.py: 100%

43 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-04-29 19:16 +0000

1"""Persistent settings stored in config.toml alongside the data directory.""" 

2 

3import sys 

4import threading 

5import tomllib 

6from pathlib import Path 

7 

8_settings_lock = threading.Lock() 

9 

10 

11def _config_path(data_root: Path) -> Path: 

12 return data_root / "config.toml" 

13 

14 

15def _escape_toml_string(s: str) -> str: 

16 """Escape a string for embedding in a TOML double-quoted value.""" 

17 return ( 

18 s.replace("\\", "\\\\") 

19 .replace('"', '\\"') 

20 .replace("\n", "\\n") 

21 .replace("\r", "\\r") 

22 .replace("\t", "\\t") 

23 .replace("\b", "\\b") 

24 .replace("\f", "\\f") 

25 ) 

26 

27 

28def load(data_root: Path) -> dict[str, str]: 

29 """Read all settings from config.toml. Returns {} if file is missing.""" 

30 path = _config_path(data_root) 

31 if not path.exists(): 

32 return {} 

33 with path.open("rb") as f: 

34 return {k: str(v) for k, v in tomllib.load(f).items()} 

35 

36 

37def save(data_root: Path, settings: dict[str, str]) -> None: 

38 """Write settings dict as simple TOML key-value pairs.""" 

39 path = _config_path(data_root) 

40 path.parent.mkdir(parents=True, exist_ok=True) 

41 lines = [f'{k} = "{_escape_toml_string(v)}"\n' for k, v in sorted(settings.items())] 

42 path.write_text("".join(lines)) 

43 if sys.platform != "win32": 

44 path.chmod(0o600) 

45 

46 

47def get(data_root: Path, key: str) -> str | None: 

48 """Look up a single key from config.toml.""" 

49 return load(data_root).get(key) 

50 

51 

52def set_value(data_root: Path, key: str, value: str) -> None: 

53 """Read-modify-write a single key in config.toml (thread-safe).""" 

54 with _settings_lock: 

55 current = load(data_root) 

56 current[key] = value 

57 save(data_root, current) 

58 

59 

60def delete_value(data_root: Path, key: str) -> None: 

61 """Remove a key from config.toml. No-op if key doesn't exist.""" 

62 with _settings_lock: 

63 current = load(data_root) 

64 current.pop(key, None) 

65 save(data_root, current) 

66 

67 

68def update_values(data_root: Path, updates: dict[str, str]) -> None: 

69 """Batch update multiple keys in config.toml (single write).""" 

70 with _settings_lock: 

71 current = load(data_root) 

72 current.update(updates) 

73 save(data_root, current) 

74 

75 

76def delete_values(data_root: Path, keys: list[str]) -> None: 

77 """Batch delete multiple keys from config.toml (single write).""" 

78 with _settings_lock: 

79 current = load(data_root) 

80 for key in keys: 

81 current.pop(key, None) 

82 save(data_root, current)