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

190 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-20 21:54 +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'key "{key}" 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, mapping): 

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 Dictionary with its keys being the new names 

200 and its values being the parameter names of the configuration. 

201 

202 Returns 

203 ------- 

204 a: dict 

205 A dictionary with the keys of mapping 

206 and the corresponding values retrieved from the configuration 

207 using the values from mapping. 

208 """ 

209 a = {} 

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

211 if src in self: 

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

213 return a 

214 

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

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

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

218 

219 The description of a configuration parameter is printed out 

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

221 character ('#').  

222 

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

224 

225 Lines are folded if the character count of parameter 

226 descriptions or section title exceeds maxline. 

227  

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

229 formatted like the section titles. 

230 

231 Parameters 

232 ---------- 

233 fh: str or Path or file object 

234 Stream or file name for writing the configuration. 

235 header: str 

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

237 diff_only: bool 

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

239 maxline: int 

240 Maximum number of characters that fit into a line. 

241 comments: boolean 

242 Print out descriptions as comments if True. 

243 """ 

244 

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

246 # format comment: 

247 if len(comment) > 0: 

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

249 fh.write(cs + ' ') 

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

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

252 # line too long? 

253 if cc + len(w) > maxline: 

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

255 cc = len(cs) + 1 

256 fh.write(w + ' ') 

257 cc += len(w) + 1 

258 fh.write('\n') 

259 

260 # open file: 

261 own_file = False 

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

263 fh = open(fh, 'w') 

264 own_file = True 

265 # write header: 

266 First = True 

267 if comments and not header is None: 

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

269 First = False 

270 # get length of longest key: 

271 maxkey = 0 

272 for key in self.keys(): 

273 if maxkey < len(key): 

274 maxkey = len(key) 

275 # write out parameter: 

276 section = '' 

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

278 # possible section entry: 

279 if comments and key in self.sections: 

280 section = self.sections[key] 

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

282 val = None 

283 unit = '' 

284 comment = '' 

285 differs = False 

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

287 val = v[0] 

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

289 unit = ' ' + v[1] 

290 if len(v) > 2: 

291 comment = v[2] 

292 if len(v) > 3: 

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

294 else: 

295 val = v 

296 # only write parameter whose value differs: 

297 if diff_only and not differs: 

298 continue 

299 # write out section 

300 if len(section) > 0: 

301 if not First: 

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

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

304 section = '' 

305 First = False 

306 # write key-value pair: 

307 if comments : 

308 fh.write('\n') 

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

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

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

312 First = False 

313 # close file: 

314 if own_file: 

315 fh.close() 

316 

317 def load(self, filename): 

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

319 

320 Parameters 

321 ---------- 

322 filename: str or Path 

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

324 

325 """ 

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

327 for line in f: 

328 # do not process empty lines and comments: 

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

330 continue 

331 # parse key value pair: 

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

333 key = key.strip() 

334 # only read values of existing keys: 

335 if not key in self: 

336 continue 

337 cv = self[key] 

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

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

340 unit = '' 

341 if len(vals) > 1: 

342 unit = vals[1] 

343 if unit != cv[1]: 

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

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

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

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

348 else: 

349 try: 

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

351 except ValueError: 

352 cv[0] = vals[0] 

353 else: 

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

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

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

357 else: 

358 try: 

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

360 except ValueError: 

361 self[key] = vals[0] 

362 

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

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

365 

366 Parameters 

367 ---------- 

368 cfgfile: str or Path 

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

370 filepath: str or Path 

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

372 of the expanded path. 

373 maxlevel: int 

374 Read configuration files from up to maxlevel parent directories. 

375 verbose: int 

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

377 """ 

378 # load configuration from the current directory: 

379 cfgfile = Path(cfgfile) 

380 if cfgfile.is_file(): 

381 if verbose > 0: 

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

383 self.load(cfgfile) 

384 

385 # load configuration files from higher directories: 

386 filepath = Path(filepath) 

387 parents = filepath.resolve().parents 

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

389 path = parents[k] / cfgfile 

390 if path.is_file(): 

391 if verbose > 0: 

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

393 self.load(path) 

394 

395 

396def main(): 

397 cfg = ConfigFile() 

398 cfg.add_section('Power spectrum:') 

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

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

401 cfg.add_section('Peaks:') 

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

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

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

405 cfg.write() 

406 print() 

407 print('set values:') 

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

409 if s is not None: 

410 print('errors:') 

411 for es in s: 

412 print(' ', es) 

413 cfg.write() 

414 print() 

415 print() 

416 

417 print('delete nfft and windows:') 

418 del cfg['nfft'] 

419 del cfg['windows'] 

420 cfg.write() 

421 

422 

423if __name__ == "__main__": 

424 main()