Coverage for savcfg / core.py: 54%
52 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-10 15:30 +0100
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-10 15:30 +0100
1from types import NoneType
2from typing import Any
4Primitive = bool | float | int | str | NoneType
7class Group(dict):
8 """
9 A dictionary that enforces:
10 - All keys are strings
11 - All values are JSON-serializable (primitives, lists, dicts, or nested JsonableDict)
12 """
14 PRIMITIVES = (str, int, float, bool, NoneType)
16 def __init__(self, *args, **kwargs):
17 # Create a temporary dict from input
18 temp = dict(*args, **kwargs)
19 # Validate all entries before accepting
20 for key, value in temp.items():
22 self._validate_key(key)
23 self._validate_value(value)
24 # Use dict's __init__ after validation
25 super().__init__(temp)
27 def _validate_key(self, key: Any) -> None:
28 if not isinstance(key, str):
29 raise TypeError(
30 f"JsonableDict keys must be strings, got {type(key).__name__}: {key!r}"
31 )
33 def _validate_value(self, value: Any) -> None:
34 if value is None or isinstance(value, (str, int, float, bool)):
35 return
37 if isinstance(value, (list, tuple)):
38 for item in value:
39 self._validate_value(item)
40 return
42 if isinstance(value, dict):
43 # Allow regular dicts (will be recursively validated on assignment)
44 for k, v in value.items():
45 self._validate_key(k)
46 self._validate_value(v)
47 return
49 if isinstance(value, Group):
50 return # Already validated internally
52 raise TypeError(
53 f"Value is not JSON serializable: {value!r} (type: {type(value).__name__})"
54 )
56 # Override mutation methods to enforce rules
58 def __setitem__(self, key: str, value: Primitive) -> None:
59 self._validate_key(key)
60 self._validate_value(value)
61 super().__setitem__(key, value)
63 def update(self, other=(), /, **kwds) -> None:
64 print(other)
65 if hasattr(other, "keys"):
66 for k in other.keys():
67 if type(other[k]) is Group and type(self.get(k)) is Group:
68 self[k].update(other[k])
69 else:
70 self[k] = other[k]
71 else:
72 for k, v in other:
73 if type(self.get(k)) is Group and type(v) is Group:
74 self[k].update(v)
75 else:
76 self[k] = v
77 for k, v in kwds.items():
78 if type(self.get(k)) is Group and type(v) is Group:
79 self[k].update(v)
80 else:
81 self[k] = v
83 def setdefault(self, key: str, default: Primitive = None) -> Primitive:
84 if key not in self:
85 self[key] = default
86 return self[key]