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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Default configuration values and injection.
3This module provides default configuration values and utilities
4for injecting defaults into user configurations.
7Example:
8 >>> from tracekit.config.defaults import inject_defaults
9 >>> config = {"name": "test"}
10 >>> full_config = inject_defaults(config, "protocol")
11"""
13from __future__ import annotations
15import copy
16from typing import Any
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}
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}
103def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
104 """Recursively merge two dictionaries.
106 Values from override take precedence. Nested dictionaries are
107 merged recursively.
109 Args:
110 base: Base dictionary.
111 override: Dictionary with values to override.
113 Returns:
114 Merged dictionary (new instance).
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)
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)
130 return result
133def inject_defaults(
134 config: dict[str, Any],
135 schema_name: str,
136) -> dict[str, Any]:
137 """Inject default values into configuration.
139 Adds default values for missing fields based on schema type.
141 Args:
142 config: User configuration dictionary.
143 schema_name: Schema name to determine defaults.
145 Returns:
146 Configuration with defaults injected.
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, {})
156 if not defaults:
157 return copy.deepcopy(config)
159 # Merge defaults with config (config takes precedence)
160 return deep_merge(defaults, config)
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.
169 Combines base defaults, schema-specific defaults, and user configuration.
171 Args:
172 user_config: User-provided configuration.
173 schema_name: Schema to apply defaults for.
175 Returns:
176 Complete configuration with all defaults.
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)
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)
191 # Apply user configuration
192 if user_config:
193 result = deep_merge(result, user_config)
195 return result
198def get_default(
199 key_path: str,
200 schema_name: str | None = None,
201) -> Any:
202 """Get default value for a configuration key.
204 Args:
205 key_path: Dot-separated path (e.g., "defaults.sample_rate").
206 schema_name: Optional schema for schema-specific defaults.
208 Returns:
209 Default value or None if not found.
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
221 # Fall back to base defaults
222 return _get_nested(DEFAULT_CONFIG, key_path)
225def _get_nested(config: dict[str, Any], key_path: str) -> Any:
226 """Get nested value by dot-separated path.
228 Args:
229 config: Configuration dictionary.
230 key_path: Dot-separated path.
232 Returns:
233 Value or None if not found.
234 """
235 keys = key_path.split(".")
236 value = config
238 for key in keys:
239 if isinstance(value, dict) and key in value:
240 value = value[key]
241 else:
242 return None
244 return value
247__all__ = [
248 "DEFAULT_CONFIG",
249 "SCHEMA_DEFAULTS",
250 "deep_merge",
251 "get_default",
252 "get_effective_config",
253 "inject_defaults",
254]