Coverage for src / tracekit / cli / shell.py: 99%

69 statements  

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

1"""Interactive REPL shell for TraceKit exploration. 

2 

3This module provides an interactive Python shell with TraceKit auto-imports, 

4tab completion, and persistent history for exploratory data analysis. 

5 

6 - Auto-imports TraceKit modules 

7 - Tab completion for methods and attributes 

8 - Persistent command history 

9 - Customized prompt with context info 

10 

11Example: 

12 $ tracekit shell 

13 TraceKit Shell v0.1.0 

14 Type 'help()' for TraceKit help, 'exit()' to quit. 

15 

16 In [1]: trace = load("capture.wfm") 

17 In [2]: rise_time(trace) 

18 Out[2]: 2.5e-9 

19 In [3]: freq, mag = fft(trace) 

20 

21References: 

22 - Python readline module 

23 - IPython-style interaction patterns 

24""" 

25 

26from __future__ import annotations 

27 

28import atexit 

29import code 

30import contextlib 

31import readline 

32import rlcompleter 

33import sys 

34from pathlib import Path 

35from typing import Any 

36 

37# History file location 

38HISTORY_FILE = Path.home() / ".tracekit_history" 

39HISTORY_LENGTH = 1000 

40 

41 

42def get_tracekit_namespace() -> dict[str, Any]: 

43 """Build namespace with TraceKit auto-imports. 

44 

45 Returns: 

46 Dictionary with all commonly-used TraceKit functions and classes. 

47 """ 

48 namespace: dict[str, Any] = {} 

49 

50 # Core imports 

51 try: 

52 import tracekit as tk 

53 

54 namespace["tk"] = tk 

55 

56 # Auto-import commonly used functions at top level 

57 from tracekit import ( 

58 DigitalTrace, 

59 ProtocolPacket, 

60 TraceMetadata, 

61 # Core types 

62 WaveformTrace, 

63 # Math 

64 add, 

65 amplitude, 

66 band_pass, 

67 band_stop, 

68 # Statistics 

69 basic_stats, 

70 detect_edges, 

71 differentiate, 

72 divide, 

73 duty_cycle, 

74 enob, 

75 fall_time, 

76 # Spectral 

77 fft, 

78 frequency, 

79 get_supported_formats, 

80 high_pass, 

81 histogram, 

82 integrate, 

83 # Loaders 

84 load, 

85 # Filtering 

86 low_pass, 

87 mean, 

88 measure, 

89 multiply, 

90 overshoot, 

91 percentiles, 

92 period, 

93 psd, 

94 pulse_width, 

95 # Measurements 

96 rise_time, 

97 rms, 

98 sfdr, 

99 sinad, 

100 snr, 

101 spectrogram, 

102 subtract, 

103 thd, 

104 # Digital 

105 to_digital, 

106 undershoot, 

107 ) 

108 

109 namespace.update( 

110 { 

111 "WaveformTrace": WaveformTrace, 

112 "DigitalTrace": DigitalTrace, 

113 "TraceMetadata": TraceMetadata, 

114 "ProtocolPacket": ProtocolPacket, 

115 "load": load, 

116 "get_supported_formats": get_supported_formats, 

117 "rise_time": rise_time, 

118 "fall_time": fall_time, 

119 "frequency": frequency, 

120 "period": period, 

121 "amplitude": amplitude, 

122 "rms": rms, 

123 "mean": mean, 

124 "overshoot": overshoot, 

125 "undershoot": undershoot, 

126 "duty_cycle": duty_cycle, 

127 "pulse_width": pulse_width, 

128 "measure": measure, 

129 "fft": fft, 

130 "psd": psd, 

131 "thd": thd, 

132 "snr": snr, 

133 "sinad": sinad, 

134 "enob": enob, 

135 "sfdr": sfdr, 

136 "spectrogram": spectrogram, 

137 "to_digital": to_digital, 

138 "detect_edges": detect_edges, 

139 "low_pass": low_pass, 

140 "high_pass": high_pass, 

141 "band_pass": band_pass, 

142 "band_stop": band_stop, 

143 "add": add, 

144 "subtract": subtract, 

145 "multiply": multiply, 

146 "divide": divide, 

147 "differentiate": differentiate, 

148 "integrate": integrate, 

149 "basic_stats": basic_stats, 

150 "histogram": histogram, 

151 "percentiles": percentiles, 

152 } 

153 ) 

154 

155 # Protocol decoders 

156 try: 

157 from tracekit.analyzers.protocols import ( 

158 decode_can, 

159 decode_i2c, 

160 decode_spi, 

161 decode_uart, 

162 ) 

163 

164 namespace.update( 

165 { 

166 "decode_uart": decode_uart, 

167 "decode_spi": decode_spi, 

168 "decode_i2c": decode_i2c, 

169 "decode_can": decode_can, 

170 } 

171 ) 

172 except ImportError: 

173 pass 

174 

175 # Discovery 

176 try: 

177 from tracekit.discovery import ( 

178 characterize_signal, 

179 decode_protocol, 

180 find_anomalies, 

181 ) 

182 

183 namespace.update( 

184 { 

185 "characterize_signal": characterize_signal, 

186 "find_anomalies": find_anomalies, 

187 "decode_protocol": decode_protocol, 

188 } 

189 ) 

190 except ImportError: 

191 pass 

192 

193 except ImportError as e: 

194 print(f"Warning: Could not import TraceKit: {e}") 

195 

196 # Common utilities 

197 try: 

198 import matplotlib.pyplot as plt 

199 

200 namespace["plt"] = plt 

201 except ImportError: 

202 pass 

203 

204 try: 

205 import numpy as np 

206 

207 namespace["np"] = np 

208 except ImportError: 

209 pass 

210 

211 return namespace 

212 

213 

214def setup_history() -> None: 

215 """Set up readline history with persistence.""" 

216 # Enable tab completion 

217 readline.parse_and_bind("tab: complete") 

218 

219 # Load history if exists 

220 if HISTORY_FILE.exists(): 

221 with contextlib.suppress(Exception): 

222 readline.read_history_file(HISTORY_FILE) 

223 

224 # Set history length 

225 readline.set_history_length(HISTORY_LENGTH) 

226 

227 # Save history on exit 

228 atexit.register(lambda: readline.write_history_file(HISTORY_FILE)) 

229 

230 

231def tracekit_help() -> None: 

232 """Display TraceKit help in the REPL.""" 

233 help_text = """ 

234TraceKit Interactive Shell - Quick Reference 

235============================================= 

236 

237Loading Data: 

238 trace = load("file.wfm") # Auto-detect format 

239 trace = load("file.csv") # CSV file 

240 formats = get_supported_formats() # List supported formats 

241 

242Waveform Measurements: 

243 rise_time(trace) # 10-90% rise time 

244 fall_time(trace) # 90-10% fall time 

245 frequency(trace) # Fundamental frequency 

246 amplitude(trace) # Peak-to-peak amplitude 

247 measure(trace) # All measurements 

248 

249Spectral Analysis: 

250 freq, mag = fft(trace) # FFT 

251 freq, pwr = psd(trace) # Power Spectral Density 

252 thd(trace) # Total Harmonic Distortion 

253 snr(trace) # Signal-to-Noise Ratio 

254 

255Digital Analysis: 

256 digital = to_digital(trace) # Extract digital signal 

257 edges = detect_edges(trace) # Find edges 

258 

259Filtering: 

260 filtered = low_pass(trace, 1e6) # Low-pass filter 

261 filtered = high_pass(trace, 1e3) # High-pass filter 

262 

263Protocol Decoding: 

264 packets = decode_uart(trace) # UART decode 

265 packets = decode_spi(clk, mosi) # SPI decode 

266 packets = decode_i2c(scl, sda) # I2C decode 

267 

268Discovery (Auto-Analysis): 

269 result = characterize_signal(trace) # Auto-characterize 

270 anomalies = find_anomalies(trace) # Find anomalies 

271 

272For detailed help on any function: 

273 help(function_name) 

274 

275Full documentation: https://github.com/lair-click-bats/tracekit 

276""" 

277 print(help_text) 

278 

279 

280class TraceKitConsole(code.InteractiveConsole): 

281 """Custom interactive console for TraceKit. 

282 

283 Provides IPython-style prompts and enhanced error handling. 

284 """ 

285 

286 def __init__(self, locals: dict[str, Any] | None = None) -> None: 

287 """Initialize the console with TraceKit namespace.""" 

288 super().__init__(locals=locals, filename="<tracekit>") 

289 self.prompt_counter = 1 

290 

291 def interact(self, banner: str | None = None, exitmsg: str | None = None) -> None: 

292 """Start the interactive session.""" 

293 if banner is None: 293 ↛ 304line 293 didn't jump to line 304 because the condition on line 293 was always true

294 import tracekit 

295 

296 banner = f""" 

297TraceKit Shell v{tracekit.__version__} 

298Python {sys.version.split()[0]} on {sys.platform} 

299Type 'tracekit_help()' for quick reference, 'exit()' to quit. 

300 

301Auto-imported: tk (tracekit), np (numpy), plt (matplotlib.pyplot) 

302Common functions: load, measure, fft, psd, thd, low_pass, high_pass 

303""" 

304 if exitmsg is None: 

305 exitmsg = "Goodbye!" 

306 

307 super().interact(banner=banner, exitmsg=exitmsg) 

308 

309 def raw_input(self, prompt: str = "") -> str: 

310 """Override prompt with counter.""" 

311 custom_prompt = f"In [{self.prompt_counter}]: " 

312 result = super().raw_input(custom_prompt) 

313 self.prompt_counter += 1 

314 return result 

315 

316 def showtraceback(self) -> None: 

317 """Show traceback with helpful hints.""" 

318 super().showtraceback() 

319 # Could add context-sensitive hints here 

320 

321 

322def start_shell() -> None: 

323 """Start the TraceKit interactive shell. 

324 

325 This is the main entry point for the REPL, providing: 

326 - Auto-imported TraceKit functions and modules 

327 - Tab completion 

328 - Persistent command history 

329 - Customized prompts 

330 """ 

331 # Set up history 

332 setup_history() 

333 

334 # Build namespace 

335 namespace = get_tracekit_namespace() 

336 

337 # Add help function 

338 namespace["tracekit_help"] = tracekit_help 

339 

340 # Set up completer 

341 completer = rlcompleter.Completer(namespace) 

342 readline.set_completer(completer.complete) 

343 

344 # Start console 

345 console = TraceKitConsole(locals=namespace) 

346 console.interact() 

347 

348 

349if __name__ == "__main__": 

350 start_shell()