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

164 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-09 16:02 +0000

1"""Handling of configuration parameter. 

2""" 

3 

4import os 

5import sys 

6 

7 

8class ConfigFile(object): 

9 """Handling of configuration parameter. 

10 

11 Configuration parameter have a name (key), a value, a unit and a 

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

13 

14 Configuration parameter can be further structured in the 

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

16 

17 The triple value, unit, description can be retrieved via the name 

18 of a parameter using the [] operator. 

19 

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

21 and set by set(). 

22 

23 Values of several configuration parameter can be mapped to 

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

25 passed as key-word arguments to a function. 

26 

27 The configuration parameter can be written to a configuration file 

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

29 """ 

30 

31 

32 def __init__(self, orig=None): 

33 self.cfg = {} 

34 self.sections = dict() 

35 self.new_section = None 

36 if not orig is None: 

37 for k, v in orig.cfg.items(): 

38 self.cfg[k] = list(v) 

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

40 self.sections[k] = v 

41 self.new_section = None 

42 

43 

44 def __eq__(self, other): 

45 """Check whether the parameter and their values are the same. 

46 """ 

47 return self.cfg == other.cfg 

48 

49 

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

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

52 

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

54 characters are intepreted as new paragraphs. 

55 

56 Parameters 

57 ---------- 

58 key: string 

59 Key of the parameter. 

60 value: any type 

61 Value of the parameter. 

62 unit: string 

63 Unit of the parameter value. 

64 description: string 

65 Textual description of the parameter. 

66 """ 

67 # add a pending section: 

68 if self.new_section is not None: 

69 self.sections[key] = self.new_section 

70 self.new_section = None 

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

72 self.cfg[key] = [value, unit, description, value] 

73 

74 

75 def add_section(self, description): 

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

77 

78 Parameters 

79 ---------- 

80 description: string 

81 Textual description of the section 

82 """ 

83 self.new_section = description 

84 

85 

86 def __contains__(self, key): 

87 """Check for existence of a configuration parameter. 

88 

89 Parameters 

90 ---------- 

91 key: string 

92 The name of the configuration parameter to be checked for. 

93 

94 Returns 

95 ------- 

96 contains: bool 

97 True if `key` specifies an existing configuration parameter. 

98 """ 

99 return key in self.cfg 

100 

101 

102 def __getitem__(self, key): 

103 """Returns the list [value, unit, description, default] 

104 of the configuration parameter key. 

105 

106 Parameters 

107 ---------- 

108 key: string 

109 Key of the configuration parameter. 

110 

111 Returns 

112 ------- 

113 value: any type 

114 Value of the configuraion parameter. 

115 unit: string 

116 Unit of the configuraion parameter. 

117 description: string 

118 Description of the configuraion parameter. 

119 default: any type 

120 Default value of the configuraion parameter. 

121 """ 

122 return self.cfg[key] 

123 

124 

125 def value(self, key): 

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

127 

128 Parameters 

129 ---------- 

130 key: string 

131 Key of the configuration parameter. 

132 

133 Returns 

134 ------- 

135 value: any type 

136 Value of the configuraion parameter. 

137 """ 

138 return self.cfg[key][0] 

139 

140 

141 def set(self, key, value): 

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

143 

144 Parameters 

145 ---------- 

146 key: string 

147 Key of the configuration parameter. 

148 value: any type 

149 The new value. 

150 

151 Raises 

152 ------ 

153 IndexError: 

154 If key does not exist. 

155 """ 

156 if not key in self.cfg: 

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

158 self.cfg[key][0] = value 

159 

160 

161 def __delitem__(self, key): 

162 """Remove an entry from the configuration. 

163 

164 Parameters 

165 ---------- 

166 key: string 

167 Key of the configuration parameter to be removed. 

168 """ 

169 if key in self.sections: 

170 sec = self.sections.pop(key) 

171 keys = list(self.cfg.keys()) 

172 inx = keys.index(key)+1 

173 if inx < len(keys): 

174 next_key = keys[inx] 

175 if not next_key in self.sections: 

176 self.sections[next_key] = sec 

177 del self.cfg[key] 

178 

179 def map(self, mapping): 

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

181 

182 Use this function to generate key-word arguments 

183 that can be passed on to functions. 

184 

185 Parameters 

186 ---------- 

187 mapping: dict 

188 Dictionary with its keys being the new names 

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

190 

191 Returns 

192 ------- 

193 a: dict 

194 A dictionary with the keys of mapping 

195 and the corresponding values retrieved from the configuration 

196 using the values from mapping. 

197 """ 

198 a = {} 

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

200 if src in self.cfg: 

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

202 return a 

203 

204 

205 def write(self, stream, header=None, diff_only=False, maxline=60, comments=True): 

206 """Pretty print configuration into stream. 

207 

208 The description of a configuration parameter is printed out 

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

210 character ('#').  

211 

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

213 

214 Lines are folded if the character count of parameter 

215 descriptions or section title exceeds maxline. 

216  

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

218 formatted like the section titles. 

219 

220 Parameters 

221 ---------- 

222 stream: 

223 Stream for writing the configuration. 

224 header: string 

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

226 diff_only: bool 

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

228 maxline: int 

229 Maximum number of characters that fit into a line. 

230 comments: boolean 

231 Print out descriptions as comments if True. 

232 """ 

233 

234 def write_comment(stream, comment, maxline, cs): 

235 # format comment: 

236 if len(comment) > 0: 

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

238 stream.write(cs + ' ') 

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

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

241 # line too long? 

242 if cc + len(w) > maxline: 

243 stream.write('\n' + cs + ' ') 

244 cc = len(cs) + 1 

245 stream.write(w + ' ') 

246 cc += len(w) + 1 

247 stream.write('\n') 

248 

249 # write header: 

250 First = True 

251 if comments and not header is None: 

252 write_comment(stream, header, maxline, '##') 

253 First = False 

254 # get length of longest key: 

255 maxkey = 0 

256 for key in self.cfg.keys(): 

257 if maxkey < len(key): 

258 maxkey = len(key) 

259 # write out parameter: 

260 section = '' 

261 for key, v in self.cfg.items(): 

262 # possible section entry: 

263 if comments and key in self.sections: 

264 section = self.sections[key] 

265 

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

267 val = None 

268 unit = '' 

269 comment = '' 

270 differs = False 

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

272 val = v[0] 

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

274 unit = ' ' + v[1] 

275 if len(v) > 2: 

276 comment = v[2] 

277 if len(v) > 3: 

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

279 else: 

280 val = v 

281 

282 # only write parameter whose value differs: 

283 if diff_only and not differs: 

284 continue 

285 

286 # write out section 

287 if len(section) > 0: 

288 if not First: 

289 stream.write('\n\n') 

290 write_comment(stream, section, maxline, '##') 

291 section = '' 

292 First = False 

293 

294 # write key-value pair: 

295 if comments : 

296 stream.write('\n') 

297 write_comment(stream, comment, maxline, '#') 

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

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

300 First = False 

301 

302 

303 def dump(self, filename, header=None, diff_only=False, maxline=60, comments=True): 

304 """Pretty print configuration into file. 

305 

306 See write() for more details. 

307 

308 Parameters 

309 ---------- 

310 filename: string 

311 Name of the file for writing the configuration. 

312 """ 

313 with open(filename, 'w') as f: 

314 self.write(f, header, diff_only, maxline, comments) 

315 

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: string 

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.cfg: 

336 continue 

337 cv = self.cfg[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.cfg[key] = (vals[0].lower() == 'true' 

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

357 else: 

358 try: 

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

360 except ValueError: 

361 self.cfg[key] = vals[0] 

362 

363 

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

365 """Load configuration from current working directory 

366 as well as from several levels of a file path. 

367 

368 Parameters 

369 ---------- 

370 cfgfile: string 

371 Name of the configuration file. 

372 filepath: string 

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

374 of the expanded path. 

375 maxlevel: int 

376 Read configuration files from up to maxlevel parent directories. 

377 verbose: int 

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

379 """ 

380 

381 # load configuration from the current directory: 

382 if os.path.isfile(cfgfile): 

383 if verbose > 0: 

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

385 self.load(cfgfile) 

386 

387 # load configuration files from higher directories: 

388 absfilepath = os.path.abspath(filepath) 

389 dirs = os.path.dirname(absfilepath).split(os.sep) 

390 dirs[0] = os.sep 

391 dirs.append('') 

392 ml = len(dirs) - 1 

393 if ml > maxlevel: 

394 ml = maxlevel 

395 for k in range(ml, 0, -1): 

396 path = os.path.join(*(dirs[:-k] + [cfgfile])) 

397 if os.path.isfile(path): 

398 if verbose > 0: 

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

400 self.load(path) 

401 

402 

403def main(): 

404 cfg = ConfigFile() 

405 cfg.add_section('Power spectrum:') 

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

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

408 cfg.add_section('Peaks:') 

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

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

411 cfg.write(sys.stdout) 

412 print('') 

413 

414 del cfg['nfft'] 

415 del cfg['windows'] 

416 cfg.write(sys.stdout) 

417 

418 

419if __name__ == "__main__": 

420 main()