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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-03 13:48 +0000
1"""configfile
3class `ConfigFile`: handling of configuration parameter.
4"""
6import sys
8from pathlib import Path
11class ConfigFile(dict):
12 """Handling of configuration parameter.
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.
19 New parameter can be added with the add() function.
21 Configuration parameter can be further structured in the
22 configuration file by inserting section titles via add_section().
24 The tuple (value, unit, description, default) can be retrieved via
25 the name of a parameter using the [] operator.
27 The value of a configuration parameter is retrieved by value()
28 and set by set().
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.
34 The configuration parameter can be written to a configuration file
35 with dump() and loaded from a file with load() and load_files().
37 Methods
38 -------
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.
50 """
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
63 def add(self, key, value, unit, description):
64 """Add a new parameter to the configuration.
66 The description of the parameter is a single string. Newline
67 characters are intepreted as new paragraphs.
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]
87 def add_section(self, description):
88 """Add a new section to the configuration.
90 Parameters
91 ----------
92 description: str
93 Textual description of the section
94 """
95 self.new_section = description
97 def value(self, key):
98 """Returns the value of the configuration parameter defined by key.
100 Parameters
101 ----------
102 key: str
103 Key of the configuration parameter.
105 Returns
106 -------
107 value: any type
108 Value of the configuraion parameter.
109 """
110 return self[key][0]
112 def set(self, key, value):
113 """Set the value of the configuration parameter defined by key.
115 Parameters
116 ----------
117 key: str
118 Key of the configuration parameter.
119 value: any type
120 The new value.
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
131 def set_values(self, values):
132 """Set values of configuration parameters from list of str.
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.
141 Returns
142 -------
143 errors: list of str
144 Error messages.
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
172 def __delitem__(self, key):
173 """Remove an entry from the configuration.
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)
190 def map(self, *args, **kwargs):
191 """Map the values of the configuration onto new names.
193 Use this function to generate key-word arguments
194 that can be passed on to functions.
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
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
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.
229 The description of a configuration parameter is printed out
230 right before its key-value pair with an initial comment
231 character ('#').
233 Section titles get two comment characters prependend ('##').
235 Lines are folded if the character count of parameter
236 descriptions or section title exceeds maxline.
238 A header can be printed initially. This is a simple string that is
239 formatted like the section titles.
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 """
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')
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()
327 def load(self, filename):
328 """Set values of configuration to values from key-value pairs read in from file.
330 Parameters
331 ----------
332 filename: str or Path
333 Name of the file from which to read the configuration.
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]
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.
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)
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)
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()
427 print('delete nfft and windows:')
428 del cfg['nfft']
429 del cfg['windows']
430 cfg.write()
433if __name__ == "__main__":
434 main()