Coverage for src / tracekit / config / defaults.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""Default configuration values and injection. 

2 

3This module provides default configuration values and utilities 

4for injecting defaults into user configurations. 

5 

6 

7Example: 

8 >>> from tracekit.config.defaults import inject_defaults 

9 >>> config = {"name": "test"} 

10 >>> full_config = inject_defaults(config, "protocol") 

11""" 

12 

13from __future__ import annotations 

14 

15import copy 

16from typing import Any 

17 

18# Default configuration values 

19DEFAULT_CONFIG: dict[str, Any] = { 

20 "version": "1.0", 

21 "defaults": { 

22 "sample_rate": 1e6, # 1 MHz default 

23 "window_function": "hann", 

24 "fft_size": 1024, 

25 }, 

26 "loaders": { 

27 "auto_detect": True, 

28 "formats": ["wfm", "csv", "npz", "hdf5", "tdms", "vcd", "sr", "wav", "pcap"], 

29 "tektronix": {"byte_order": "little"}, 

30 "csv": {"delimiter": ",", "skip_header": 0}, 

31 }, 

32 "measurements": { 

33 "rise_time": {"ref_levels": [0.1, 0.9]}, 

34 "fall_time": {"ref_levels": [0.9, 0.1]}, 

35 "frequency": {"min_periods": 3}, 

36 }, 

37 "spectral": { 

38 "default_window": "hann", 

39 "overlap": 0.5, 

40 "nfft": None, # Auto-determine from signal length 

41 }, 

42 "visualization": { 

43 "default_style": "seaborn", 

44 "figure_size": [10, 6], 

45 "dpi": 100, 

46 "colormap": "viridis", 

47 }, 

48 "export": { 

49 "csv": {"precision": 6}, 

50 "hdf5": {"compression": "gzip", "compression_opts": 4}, 

51 }, 

52 "logging": { 

53 "level": "INFO", 

54 "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", 

55 }, 

56} 

57 

58 

59# Schema-specific default values 

60SCHEMA_DEFAULTS: dict[str, dict[str, Any]] = { 

61 "protocol": { 

62 "version": "1.0.0", 

63 "timing": { 

64 "data_bits": [8], 

65 "stop_bits": [1], 

66 "parity": ["none"], 

67 }, 

68 }, 

69 "pipeline": { 

70 "version": "1.0.0", 

71 "parallel_groups": [], 

72 }, 

73 "logic_family": { 

74 "temperature_range": { 

75 "min": 0, 

76 "max": 70, 

77 }, 

78 }, 

79 "threshold_profile": { 

80 "tolerance": 0, 

81 "overrides": {}, 

82 }, 

83 "preferences": { 

84 "defaults": { 

85 "sample_rate": 1e6, 

86 "window_function": "hann", 

87 }, 

88 "visualization": { 

89 "style": "seaborn", 

90 "dpi": 100, 

91 }, 

92 "export": { 

93 "default_format": "csv", 

94 "precision": 6, 

95 }, 

96 "logging": { 

97 "level": "INFO", 

98 }, 

99 }, 

100} 

101 

102 

103def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: 

104 """Recursively merge two dictionaries. 

105 

106 Values from override take precedence. Nested dictionaries are 

107 merged recursively. 

108 

109 Args: 

110 base: Base dictionary. 

111 override: Dictionary with values to override. 

112 

113 Returns: 

114 Merged dictionary (new instance). 

115 

116 Example: 

117 >>> base = {"a": 1, "b": {"c": 2}} 

118 >>> override = {"b": {"d": 3}} 

119 >>> deep_merge(base, override) 

120 {'a': 1, 'b': {'c': 2, 'd': 3}} 

121 """ 

122 result = copy.deepcopy(base) 

123 

124 for key, value in override.items(): 

125 if key in result and isinstance(result[key], dict) and isinstance(value, dict): 

126 result[key] = deep_merge(result[key], value) 

127 else: 

128 result[key] = copy.deepcopy(value) 

129 

130 return result 

131 

132 

133def inject_defaults( 

134 config: dict[str, Any], 

135 schema_name: str, 

136) -> dict[str, Any]: 

137 """Inject default values into configuration. 

138 

139 Adds default values for missing fields based on schema type. 

140 

141 Args: 

142 config: User configuration dictionary. 

143 schema_name: Schema name to determine defaults. 

144 

145 Returns: 

146 Configuration with defaults injected. 

147 

148 Example: 

149 >>> config = {"name": "uart", "timing": {"baud_rates": [9600]}} 

150 >>> full = inject_defaults(config, "protocol") 

151 >>> print(full["timing"]["data_bits"]) 

152 [8] 

153 """ 

154 defaults = SCHEMA_DEFAULTS.get(schema_name, {}) 

155 

156 if not defaults: 

157 return copy.deepcopy(config) 

158 

159 # Merge defaults with config (config takes precedence) 

160 return deep_merge(defaults, config) 

161 

162 

163def get_effective_config( 

164 user_config: dict[str, Any] | None = None, 

165 schema_name: str | None = None, 

166) -> dict[str, Any]: 

167 """Get effective configuration with all defaults applied. 

168 

169 Combines base defaults, schema-specific defaults, and user configuration. 

170 

171 Args: 

172 user_config: User-provided configuration. 

173 schema_name: Schema to apply defaults for. 

174 

175 Returns: 

176 Complete configuration with all defaults. 

177 

178 Example: 

179 >>> config = get_effective_config({"defaults": {"sample_rate": 2e6}}) 

180 >>> print(config["defaults"]["sample_rate"]) 

181 2000000.0 

182 """ 

183 # Start with base defaults 

184 result = copy.deepcopy(DEFAULT_CONFIG) 

185 

186 # Add schema-specific defaults 

187 if schema_name and schema_name in SCHEMA_DEFAULTS: 

188 schema_defaults = SCHEMA_DEFAULTS[schema_name] 

189 result = deep_merge(result, schema_defaults) 

190 

191 # Apply user configuration 

192 if user_config: 

193 result = deep_merge(result, user_config) 

194 

195 return result 

196 

197 

198def get_default( 

199 key_path: str, 

200 schema_name: str | None = None, 

201) -> Any: 

202 """Get default value for a configuration key. 

203 

204 Args: 

205 key_path: Dot-separated path (e.g., "defaults.sample_rate"). 

206 schema_name: Optional schema for schema-specific defaults. 

207 

208 Returns: 

209 Default value or None if not found. 

210 

211 Example: 

212 >>> get_default("defaults.sample_rate") 

213 1000000.0 

214 """ 

215 # Check schema-specific defaults first 

216 if schema_name and schema_name in SCHEMA_DEFAULTS: 

217 value = _get_nested(SCHEMA_DEFAULTS[schema_name], key_path) 

218 if value is not None: 

219 return value 

220 

221 # Fall back to base defaults 

222 return _get_nested(DEFAULT_CONFIG, key_path) 

223 

224 

225def _get_nested(config: dict[str, Any], key_path: str) -> Any: 

226 """Get nested value by dot-separated path. 

227 

228 Args: 

229 config: Configuration dictionary. 

230 key_path: Dot-separated path. 

231 

232 Returns: 

233 Value or None if not found. 

234 """ 

235 keys = key_path.split(".") 

236 value = config 

237 

238 for key in keys: 

239 if isinstance(value, dict) and key in value: 

240 value = value[key] 

241 else: 

242 return None 

243 

244 return value 

245 

246 

247__all__ = [ 

248 "DEFAULT_CONFIG", 

249 "SCHEMA_DEFAULTS", 

250 "deep_merge", 

251 "get_default", 

252 "get_effective_config", 

253 "inject_defaults", 

254]