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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-20 21:54 +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'key "{key}" 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, mapping):
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 Dictionary with its keys being the new names
200 and its values being the parameter names of the configuration.
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
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.
219 The description of a configuration parameter is printed out
220 right before its key-value pair with an initial comment
221 character ('#').
223 Section titles get two comment characters prependend ('##').
225 Lines are folded if the character count of parameter
226 descriptions or section title exceeds maxline.
228 A header can be printed initially. This is a simple string that is
229 formatted like the section titles.
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 """
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')
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()
317 def load(self, filename):
318 """Set values of configuration to values from key-value pairs read in from file.
320 Parameters
321 ----------
322 filename: str or Path
323 Name of the file from which to read the configuration.
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]
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.
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)
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)
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()
417 print('delete nfft and windows:')
418 del cfg['nfft']
419 del cfg['windows']
420 cfg.write()
423if __name__ == "__main__":
424 main()