Coverage for src / thunderlab / configfile.py: 92%
194 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-10 21:08 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-10 21:08 +0000
1"""
2class `ConfigFile`: handling of configuration parameter.
3"""
5import sys
7from pathlib import Path
10class ConfigFile(dict):
11 """Handling of configuration parameter.
13 Configuration parameter have a name (key), a value, a unit, a
14 description and a default value. ConfigFile is a dictionary with
15 the parameter names as keys and the tuple (value, unit,
16 description, default) as value.
18 New parameter can be added with the add() function.
20 Configuration parameter can be further structured in the
21 configuration file by inserting section titles via add_section().
23 The tuple (value, unit, description, default) can be retrieved via
24 the name of a parameter using the [] operator.
26 The value of a configuration parameter is retrieved by value()
27 and set by set().
29 Values of several configuration parameter can be mapped to
30 new names with the map() function. The resulting dictionary can be
31 passed as key-word arguments to a function.
33 The configuration parameter can be written to a configuration file
34 with write() and loaded from a file with load() and load_files().
36 Methods
37 -------
39 - `add()`: add a new parameter to the configuration.
40 - `add_section()`: add a new section to the configuration.
41 - `value()`: returns the value of the configuration parameter defined by key.
42 - `set(): set the value of the configuration parameter defined by key.
43 - `set_values(): set values of configuration parameters from list of str.
44 - `map()`: map the values of the configuration onto new names.
45 - `write()`: pretty print configuration into file or stream.
46 - `load()`: set values of configuration to values from key-value pairs read in from file.
47 - `load_files()`: load configuration from current working directory as well as from several levels of a file path.
49 """
51 def __init__(self, orig=None):
52 super().__init__(dict())
53 self.sections = dict()
54 self.new_section = None
55 if not orig is None:
56 for k, v in orig.items():
57 self[k] = list(v)
58 for k, v in orig.sections.items():
59 self.sections[k] = v
60 self.new_section = None
62 def add(self, key, value, unit, description):
63 """Add a new parameter to the configuration.
65 The description of the parameter is a single string. Newline
66 characters are intepreted as new paragraphs.
68 Parameters
69 ----------
70 key: str
71 Key (name) of the parameter.
72 value: any type
73 Value of the parameter.
74 unit: str
75 Unit of the parameter value.
76 description: str
77 Textual description of the parameter.
78 """
79 # add a pending section:
80 if self.new_section is not None:
81 self.sections[key] = self.new_section
82 self.new_section = None
83 # add configuration parameter (4th element is default value):
84 self[key] = [value, unit, description, value]
86 def add_section(self, description):
87 """Add a new section to the configuration.
89 Parameters
90 ----------
91 description: str
92 Textual description of the section
93 """
94 self.new_section = description
96 def value(self, key):
97 """Returns the value of the configuration parameter defined by key.
99 Parameters
100 ----------
101 key: str
102 Key of the configuration parameter.
104 Returns
105 -------
106 value: any type
107 Value of the configuraion parameter.
108 """
109 return self[key][0]
111 def set(self, key, value):
112 """Set the value of the configuration parameter defined by key.
114 Parameters
115 ----------
116 key: str
117 Key of the configuration parameter.
118 value: any type
119 The new value.
121 Raises
122 ------
123 IndexError:
124 If key does not exist.
125 """
126 if not key in self:
127 raise IndexError(f'Key {key} does not exist')
128 self[key][0] = value
130 def set_values(self, values):
131 """Set values of configuration parameters from list of str.
133 Parameters
134 ----------
135 values: list of str
136 Each string can be one or several key-value pairs
137 separated by commas.
138 A key-value pair is separated by colons.
140 Returns
141 -------
142 errors: list of str
143 Error messages.
145 """
146 errors = []
147 for kvs in values:
148 for kv in kvs.strip().split(','):
149 if ':' in kv:
150 ss = kv.split(':')
151 key = ss[0].strip()
152 val = ':'.join(ss[1:]).strip()
153 if key in self:
154 vt = type(self[key][0])
155 if vt is bool:
156 if val.lower() in ['true', 'on', 'yes', 't', 'y']:
157 self[key][0] = True
158 elif val.lower() in ['false', 'off', 'no', 'f', 'n']:
159 self[key][0] = False
160 else:
161 errors.append(f'configuration parameter "{key}": cannot convert "{val}" to bool')
162 else:
163 try:
164 self[key][0] = vt(val)
165 except ValueError:
166 errors.append(f'configuration parameter "{key}": cannot convert "{val}" to {vt}')
167 else:
168 errors.append(f'configuration parameter "{key}:{val}" not found in configuration parameters')
169 return errors if len(errors) > 0 else None
171 def __delitem__(self, key):
172 """Remove an entry from the configuration.
174 Parameters
175 ----------
176 key: str
177 Key of the configuration parameter to be removed.
178 """
179 if key in self.sections:
180 sec = self.sections.pop(key)
181 keys = list(self.keys())
182 inx = keys.index(key)+1
183 if inx < len(keys):
184 next_key = keys[inx]
185 if not next_key in self.sections:
186 self.sections[next_key] = sec
187 super().__delitem__(key)
189 def map(self, *args, **kwargs):
190 """Map the values of the configuration onto new names.
192 Use this function to generate key-word arguments
193 that can be passed on to functions.
195 Parameters
196 ----------
197 mapping: dict
198 If provided as the first argument, then
199 dictionary with keys being the new names
200 and corresponding values being the parameter names
201 of the configuration.
202 kwargs: dict
203 Further key-word arguments with keys being the new names
204 and corresponding values being the parameter names
205 of the configuration
207 Returns
208 -------
209 a: dict
210 A dictionary with the keys of mapping
211 and the corresponding values retrieved from the configuration
212 using the values from mapping.
213 """
214 mapping = {}
215 for a in args:
216 mapping.update(**a)
217 mapping.update(**kwargs)
218 a = {}
219 for dest, src in mapping.items():
220 if src in self:
221 a[dest] = self.value(src)
222 return a
224 def write(self, fh=sys.stdout, header=None,
225 diff_only=False, maxline=60, comments=True):
226 """Pretty print configuration into file or stream.
228 The description of a configuration parameter is printed out
229 right before its key-value pair with an initial comment
230 character ('#').
232 Section titles get two comment characters prependend ('##').
234 Lines are folded if the character count of parameter
235 descriptions or section title exceeds maxline.
237 A header can be printed initially. This is a simple string that is
238 formatted like the section titles.
240 Parameters
241 ----------
242 fh: str or Path or file object
243 Stream or file name for writing the configuration.
244 header: str
245 A string that is written as an introductory comment into the file.
246 diff_only: bool
247 If true write out only those parameters whose value differs from their default.
248 maxline: int
249 Maximum number of characters that fit into a line.
250 comments: boolean
251 Print out descriptions as comments if True.
252 """
254 def write_comment(fh, comment, maxline, cs):
255 # format comment:
256 if len(comment) > 0:
257 for line in comment.split('\n'):
258 fh.write(cs + ' ')
259 cc = len(cs) + 1 # character count
260 for w in line.strip().split(' '):
261 # line too long?
262 if cc + len(w) > maxline:
263 fh.write('\n' + cs + ' ')
264 cc = len(cs) + 1
265 fh.write(w + ' ')
266 cc += len(w) + 1
267 fh.write('\n')
269 # open file:
270 own_file = False
271 if not hasattr(fh, 'write'):
272 fh = open(fh, 'w')
273 own_file = True
274 # write header:
275 First = True
276 if comments and not header is None:
277 write_comment(fh, header, maxline, '##')
278 First = False
279 # get length of longest key:
280 maxkey = 0
281 for key in self.keys():
282 if maxkey < len(key):
283 maxkey = len(key)
284 # write out parameter:
285 section = ''
286 for key, v in self.items():
287 # possible section entry:
288 if comments and key in self.sections:
289 section = self.sections[key]
290 # get value, unit, and comment from v:
291 val = None
292 unit = ''
293 comment = ''
294 differs = False
295 if hasattr(v, '__len__') and (not isinstance(v, str)):
296 val = v[0]
297 if len(v) > 1 and len(v[1]) > 0:
298 unit = ' ' + v[1]
299 if len(v) > 2:
300 comment = v[2]
301 if len(v) > 3:
302 differs = (val != v[3])
303 else:
304 val = v
305 # only write parameter whose value differs:
306 if diff_only and not differs:
307 continue
308 # write out section
309 if len(section) > 0:
310 if not First:
311 fh.write('\n\n')
312 write_comment(fh, section, maxline, '##')
313 section = ''
314 First = False
315 # write key-value pair:
316 if comments :
317 fh.write('\n')
318 write_comment(fh, comment, maxline, '#')
319 fh.write('{key:<{width}s}: {val}{unit:s}\n'.format(
320 key=key, width=maxkey, val=val, unit=unit))
321 First = False
322 # close file:
323 if own_file:
324 fh.close()
326 def load(self, filename):
327 """Set values of configuration to values from key-value pairs read in from file.
329 Parameters
330 ----------
331 filename: str or Path
332 Name of the file from which to read the configuration.
334 """
335 with open(filename, 'r') as f:
336 for line in f:
337 # do not process empty lines and comments:
338 if len(line.strip()) == 0 or line[0] == '#' or not ':' in line:
339 continue
340 # parse key value pair:
341 key, val = line.split(':', 1)
342 key = key.strip()
343 # only read values of existing keys:
344 if not key in self:
345 continue
346 cv = self[key]
347 vals = val.strip().split(' ')
348 if hasattr(cv, '__len__') and (not isinstance(cv, str)):
349 unit = ''
350 if len(vals) > 1:
351 unit = vals[1]
352 if unit != cv[1]:
353 print(f'unit for {key} is {unit} but should be {cv[1]}')
354 if type(cv[0]) == bool:
355 cv[0] = (vals[0].lower() == 'true'
356 or vals[0].lower() == 'yes')
357 else:
358 try:
359 cv[0] = type(cv[0])(vals[0])
360 except ValueError:
361 cv[0] = vals[0]
362 else:
363 if type(cv[0]) == bool:
364 self[key] = (vals[0].lower() == 'true'
365 or vals[0].lower() == 'yes')
366 else:
367 try:
368 self[key] = type(cv)(vals[0])
369 except ValueError:
370 self[key] = vals[0]
372 def load_files(self, cfgfile, filepath, maxlevel=3, verbose=0):
373 """Load configuration from current working directory as well as from several levels of a file path.
375 Parameters
376 ----------
377 cfgfile: str or Path
378 Name of the configuration file (without any path).
379 filepath: str or Path
380 Path of a file. Configuration files are read in from different levels
381 of the expanded path.
382 maxlevel: int
383 Read configuration files from up to maxlevel parent directories.
384 verbose: int
385 If greater than zero, print out from which files configuration has been loaded.
386 """
387 # load configuration from the current directory:
388 cfgfile = Path(cfgfile)
389 if cfgfile.is_file():
390 if verbose > 0:
391 print(f'load configuration {cfgfile}')
392 self.load(cfgfile)
394 # load configuration files from higher directories:
395 filepath = Path(filepath)
396 parents = filepath.resolve().parents
397 for k in reversed(range(min(maxlevel, len(parents)))):
398 path = parents[k] / cfgfile
399 if path.is_file():
400 if verbose > 0:
401 print(f'load configuration {path}')
402 self.load(path)
405def main():
406 cfg = ConfigFile()
407 cfg.add_section('Power spectrum:')
408 cfg.add('nfft', 256, '', 'Number of data poinst for fourier transform.')
409 cfg.add('windows', 4, '', 'Number of windows on which power spectra are computed.')
410 cfg.add_section('Peaks:')
411 cfg.add('threshold', 20.0, 'dB', 'Threshold for peak detection.')
412 cfg.add('deltaf', 10.0, 'Hz', 'Minimum distance between peaks.')
413 cfg.add('flip', True, '', 'Flip peaks.')
414 cfg.write()
415 print()
416 print('set values:')
417 s = cfg.set_values(['nfft: 1024, windows: 2', 'threshold: donkey', 'flip: false', 'something: blue'])
418 if s is not None:
419 print('errors:')
420 for es in s:
421 print(' ', es)
422 cfg.write()
423 print()
424 print()
426 print('delete nfft and windows:')
427 del cfg['nfft']
428 del cfg['windows']
429 cfg.write()
432if __name__ == "__main__":
433 main()