Coverage for src / thunderlab / configfile.py: 92%

194 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-03 13:48 +0000

1"""configfile 

2 

3class `ConfigFile`: handling of configuration parameter. 

4""" 

5 

6import sys 

7 

8from pathlib import Path 

9 

10 

11class ConfigFile(dict): 

12 """Handling of configuration parameter. 

13 

14 Configuration parameter have a name (key), a value, a unit, a 

15 description and a default value. ConfigFile is a dictionary with 

16 the parameter names as keys and the tuple (value, unit, 

17 description, default) as value. 

18  

19 New parameter can be added with the add() function. 

20 

21 Configuration parameter can be further structured in the 

22 configuration file by inserting section titles via add_section(). 

23 

24 The tuple (value, unit, description, default) can be retrieved via 

25 the name of a parameter using the [] operator. 

26 

27 The value of a configuration parameter is retrieved by value() 

28 and set by set(). 

29 

30 Values of several configuration parameter can be mapped to 

31 new names with the map() function. The resulting dictionary can be 

32 passed as key-word arguments to a function. 

33 

34 The configuration parameter can be written to a configuration file 

35 with dump() and loaded from a file with load() and load_files(). 

36  

37 Methods 

38 ------- 

39 

40 - `add()`: add a new parameter to the configuration. 

41 - `add_section()`: add a new section to the configuration. 

42 - `value()`: returns the value of the configuration parameter defined by key. 

43 - `set(): set the value of the configuration parameter defined by key. 

44 - `set_values(): set values of configuration parameters from list of str. 

45 - `map()`: map the values of the configuration onto new names. 

46 - `write()`: pretty print configuration into file or stream. 

47 - `load()`: set values of configuration to values from key-value pairs read in from file. 

48 - `load_files()`: load configuration from current working directory as well as from several levels of a file path. 

49 

50 """ 

51 

52 def __init__(self, orig=None): 

53 super().__init__(dict()) 

54 self.sections = dict() 

55 self.new_section = None 

56 if not orig is None: 

57 for k, v in orig.items(): 

58 self[k] = list(v) 

59 for k, v in orig.sections.items(): 

60 self.sections[k] = v 

61 self.new_section = None 

62 

63 def add(self, key, value, unit, description): 

64 """Add a new parameter to the configuration. 

65 

66 The description of the parameter is a single string. Newline 

67 characters are intepreted as new paragraphs. 

68 

69 Parameters 

70 ---------- 

71 key: str 

72 Key (name) of the parameter. 

73 value: any type 

74 Value of the parameter. 

75 unit: str 

76 Unit of the parameter value. 

77 description: str 

78 Textual description of the parameter. 

79 """ 

80 # add a pending section: 

81 if self.new_section is not None: 

82 self.sections[key] = self.new_section 

83 self.new_section = None 

84 # add configuration parameter (4th element is default value): 

85 self[key] = [value, unit, description, value] 

86 

87 def add_section(self, description): 

88 """Add a new section to the configuration. 

89 

90 Parameters 

91 ---------- 

92 description: str 

93 Textual description of the section 

94 """ 

95 self.new_section = description 

96 

97 def value(self, key): 

98 """Returns the value of the configuration parameter defined by key. 

99 

100 Parameters 

101 ---------- 

102 key: str 

103 Key of the configuration parameter. 

104 

105 Returns 

106 ------- 

107 value: any type 

108 Value of the configuraion parameter. 

109 """ 

110 return self[key][0] 

111 

112 def set(self, key, value): 

113 """Set the value of the configuration parameter defined by key. 

114 

115 Parameters 

116 ---------- 

117 key: str 

118 Key of the configuration parameter. 

119 value: any type 

120 The new value. 

121 

122 Raises 

123 ------ 

124 IndexError: 

125 If key does not exist. 

126 """ 

127 if not key in self: 

128 raise IndexError(f'Key {key} does not exist') 

129 self[key][0] = value 

130 

131 def set_values(self, values): 

132 """Set values of configuration parameters from list of str. 

133  

134 Parameters 

135 ---------- 

136 values: list of str 

137 Each string can be one or several key-value pairs 

138 separated by commas. 

139 A key-value pair is separated by colons. 

140 

141 Returns 

142 ------- 

143 errors: list of str 

144 Error messages. 

145 

146 """ 

147 errors = [] 

148 for kvs in values: 

149 for kv in kvs.strip().split(','): 

150 if ':' in kv: 

151 ss = kv.split(':') 

152 key = ss[0].strip() 

153 val = ':'.join(ss[1:]).strip() 

154 if key in self: 

155 vt = type(self[key][0]) 

156 if vt is bool: 

157 if val.lower() in ['true', 'on', 'yes', 't', 'y']: 

158 self[key][0] = True 

159 elif val.lower() in ['false', 'off', 'no', 'f', 'n']: 

160 self[key][0] = False 

161 else: 

162 errors.append(f'configuration parameter "{key}": cannot convert "{val}" to bool') 

163 else: 

164 try: 

165 self[key][0] = vt(val) 

166 except ValueError: 

167 errors.append(f'configuration parameter "{key}": cannot convert "{val}" to {vt}') 

168 else: 

169 errors.append(f'configuration parameter "{key}:{val}" not found in configuration parameters') 

170 return errors if len(errors) > 0 else None 

171 

172 def __delitem__(self, key): 

173 """Remove an entry from the configuration. 

174 

175 Parameters 

176 ---------- 

177 key: str 

178 Key of the configuration parameter to be removed. 

179 """ 

180 if key in self.sections: 

181 sec = self.sections.pop(key) 

182 keys = list(self.keys()) 

183 inx = keys.index(key)+1 

184 if inx < len(keys): 

185 next_key = keys[inx] 

186 if not next_key in self.sections: 

187 self.sections[next_key] = sec 

188 super().__delitem__(key) 

189 

190 def map(self, *args, **kwargs): 

191 """Map the values of the configuration onto new names. 

192 

193 Use this function to generate key-word arguments 

194 that can be passed on to functions. 

195 

196 Parameters 

197 ---------- 

198 mapping: dict 

199 If provided as the first argument, then 

200 dictionary with keys being the new names 

201 and corresponding values being the parameter names 

202 of the configuration. 

203 kwargs: dict 

204 Further key-word arguments with keys being the new names 

205 and corresponding values being the parameter names 

206 of the configuration 

207 

208 Returns 

209 ------- 

210 a: dict 

211 A dictionary with the keys of mapping 

212 and the corresponding values retrieved from the configuration 

213 using the values from mapping. 

214 """ 

215 mapping = {} 

216 for a in args: 

217 mapping.update(**a) 

218 mapping.update(**kwargs) 

219 a = {} 

220 for dest, src in mapping.items(): 

221 if src in self: 

222 a[dest] = self.value(src) 

223 return a 

224 

225 def write(self, fh=sys.stdout, header=None, 

226 diff_only=False, maxline=60, comments=True): 

227 """Pretty print configuration into file or stream. 

228 

229 The description of a configuration parameter is printed out 

230 right before its key-value pair with an initial comment 

231 character ('#').  

232 

233 Section titles get two comment characters prependend ('##'). 

234 

235 Lines are folded if the character count of parameter 

236 descriptions or section title exceeds maxline. 

237  

238 A header can be printed initially. This is a simple string that is 

239 formatted like the section titles. 

240 

241 Parameters 

242 ---------- 

243 fh: str or Path or file object 

244 Stream or file name for writing the configuration. 

245 header: str 

246 A string that is written as an introductory comment into the file. 

247 diff_only: bool 

248 If true write out only those parameters whose value differs from their default. 

249 maxline: int 

250 Maximum number of characters that fit into a line. 

251 comments: boolean 

252 Print out descriptions as comments if True. 

253 """ 

254 

255 def write_comment(fh, comment, maxline, cs): 

256 # format comment: 

257 if len(comment) > 0: 

258 for line in comment.split('\n'): 

259 fh.write(cs + ' ') 

260 cc = len(cs) + 1 # character count 

261 for w in line.strip().split(' '): 

262 # line too long? 

263 if cc + len(w) > maxline: 

264 fh.write('\n' + cs + ' ') 

265 cc = len(cs) + 1 

266 fh.write(w + ' ') 

267 cc += len(w) + 1 

268 fh.write('\n') 

269 

270 # open file: 

271 own_file = False 

272 if not hasattr(fh, 'write'): 

273 fh = open(fh, 'w') 

274 own_file = True 

275 # write header: 

276 First = True 

277 if comments and not header is None: 

278 write_comment(fh, header, maxline, '##') 

279 First = False 

280 # get length of longest key: 

281 maxkey = 0 

282 for key in self.keys(): 

283 if maxkey < len(key): 

284 maxkey = len(key) 

285 # write out parameter: 

286 section = '' 

287 for key, v in self.items(): 

288 # possible section entry: 

289 if comments and key in self.sections: 

290 section = self.sections[key] 

291 # get value, unit, and comment from v: 

292 val = None 

293 unit = '' 

294 comment = '' 

295 differs = False 

296 if hasattr(v, '__len__') and (not isinstance(v, str)): 

297 val = v[0] 

298 if len(v) > 1 and len(v[1]) > 0: 

299 unit = ' ' + v[1] 

300 if len(v) > 2: 

301 comment = v[2] 

302 if len(v) > 3: 

303 differs = (val != v[3]) 

304 else: 

305 val = v 

306 # only write parameter whose value differs: 

307 if diff_only and not differs: 

308 continue 

309 # write out section 

310 if len(section) > 0: 

311 if not First: 

312 fh.write('\n\n') 

313 write_comment(fh, section, maxline, '##') 

314 section = '' 

315 First = False 

316 # write key-value pair: 

317 if comments : 

318 fh.write('\n') 

319 write_comment(fh, comment, maxline, '#') 

320 fh.write('{key:<{width}s}: {val}{unit:s}\n'.format( 

321 key=key, width=maxkey, val=val, unit=unit)) 

322 First = False 

323 # close file: 

324 if own_file: 

325 fh.close() 

326 

327 def load(self, filename): 

328 """Set values of configuration to values from key-value pairs read in from file. 

329 

330 Parameters 

331 ---------- 

332 filename: str or Path 

333 Name of the file from which to read the configuration. 

334 

335 """ 

336 with open(filename, 'r') as f: 

337 for line in f: 

338 # do not process empty lines and comments: 

339 if len(line.strip()) == 0 or line[0] == '#' or not ':' in line: 

340 continue 

341 # parse key value pair: 

342 key, val = line.split(':', 1) 

343 key = key.strip() 

344 # only read values of existing keys: 

345 if not key in self: 

346 continue 

347 cv = self[key] 

348 vals = val.strip().split(' ') 

349 if hasattr(cv, '__len__') and (not isinstance(cv, str)): 

350 unit = '' 

351 if len(vals) > 1: 

352 unit = vals[1] 

353 if unit != cv[1]: 

354 print(f'unit for {key} is {unit} but should be {cv[1]}') 

355 if type(cv[0]) == bool: 

356 cv[0] = (vals[0].lower() == 'true' 

357 or vals[0].lower() == 'yes') 

358 else: 

359 try: 

360 cv[0] = type(cv[0])(vals[0]) 

361 except ValueError: 

362 cv[0] = vals[0] 

363 else: 

364 if type(cv[0]) == bool: 

365 self[key] = (vals[0].lower() == 'true' 

366 or vals[0].lower() == 'yes') 

367 else: 

368 try: 

369 self[key] = type(cv)(vals[0]) 

370 except ValueError: 

371 self[key] = vals[0] 

372 

373 def load_files(self, cfgfile, filepath, maxlevel=3, verbose=0): 

374 """Load configuration from current working directory as well as from several levels of a file path. 

375 

376 Parameters 

377 ---------- 

378 cfgfile: str or Path 

379 Name of the configuration file (without any path). 

380 filepath: str or Path 

381 Path of a file. Configuration files are read in from different levels 

382 of the expanded path. 

383 maxlevel: int 

384 Read configuration files from up to maxlevel parent directories. 

385 verbose: int 

386 If greater than zero, print out from which files configuration has been loaded. 

387 """ 

388 # load configuration from the current directory: 

389 cfgfile = Path(cfgfile) 

390 if cfgfile.is_file(): 

391 if verbose > 0: 

392 print(f'load configuration {cfgfile}') 

393 self.load(cfgfile) 

394 

395 # load configuration files from higher directories: 

396 filepath = Path(filepath) 

397 parents = filepath.resolve().parents 

398 for k in reversed(range(min(maxlevel, len(parents)))): 

399 path = parents[k] / cfgfile 

400 if path.is_file(): 

401 if verbose > 0: 

402 print(f'load configuration {path}') 

403 self.load(path) 

404 

405 

406def main(): 

407 cfg = ConfigFile() 

408 cfg.add_section('Power spectrum:') 

409 cfg.add('nfft', 256, '', 'Number of data poinst for fourier transform.') 

410 cfg.add('windows', 4, '', 'Number of windows on which power spectra are computed.') 

411 cfg.add_section('Peaks:') 

412 cfg.add('threshold', 20.0, 'dB', 'Threshold for peak detection.') 

413 cfg.add('deltaf', 10.0, 'Hz', 'Minimum distance between peaks.') 

414 cfg.add('flip', True, '', 'Flip peaks.') 

415 cfg.write() 

416 print() 

417 print('set values:') 

418 s = cfg.set_values(['nfft: 1024, windows: 2', 'threshold: donkey', 'flip: false', 'something: blue']) 

419 if s is not None: 

420 print('errors:') 

421 for es in s: 

422 print(' ', es) 

423 cfg.write() 

424 print() 

425 print() 

426 

427 print('delete nfft and windows:') 

428 del cfg['nfft'] 

429 del cfg['windows'] 

430 cfg.write() 

431 

432 

433if __name__ == "__main__": 

434 main()