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
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-09 16:02 +0000
1"""Handling of configuration parameter.
2"""
4import os
5import sys
8class ConfigFile(object):
9 """Handling of configuration parameter.
11 Configuration parameter have a name (key), a value, a unit and a
12 description. New parameter can be added with the add() function.
14 Configuration parameter can be further structured in the
15 configuration file by inserting section titles via add_section().
17 The triple value, unit, description can be retrieved via the name
18 of a parameter using the [] operator.
20 The value of a configuration parameter is retrieved by value()
21 and set by set().
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.
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 """
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
44 def __eq__(self, other):
45 """Check whether the parameter and their values are the same.
46 """
47 return self.cfg == other.cfg
50 def add(self, key, value, unit, description):
51 """Add a new parameter to the configuration.
53 The description of the parameter is a single string. Newline
54 characters are intepreted as new paragraphs.
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]
75 def add_section(self, description):
76 """Add a new section to the configuration.
78 Parameters
79 ----------
80 description: string
81 Textual description of the section
82 """
83 self.new_section = description
86 def __contains__(self, key):
87 """Check for existence of a configuration parameter.
89 Parameters
90 ----------
91 key: string
92 The name of the configuration parameter to be checked for.
94 Returns
95 -------
96 contains: bool
97 True if `key` specifies an existing configuration parameter.
98 """
99 return key in self.cfg
102 def __getitem__(self, key):
103 """Returns the list [value, unit, description, default]
104 of the configuration parameter key.
106 Parameters
107 ----------
108 key: string
109 Key of the configuration parameter.
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]
125 def value(self, key):
126 """Returns the value of the configuration parameter defined by key.
128 Parameters
129 ----------
130 key: string
131 Key of the configuration parameter.
133 Returns
134 -------
135 value: any type
136 Value of the configuraion parameter.
137 """
138 return self.cfg[key][0]
141 def set(self, key, value):
142 """Set the value of the configuration parameter defined by key.
144 Parameters
145 ----------
146 key: string
147 Key of the configuration parameter.
148 value: any type
149 The new value.
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
161 def __delitem__(self, key):
162 """Remove an entry from the configuration.
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]
179 def map(self, mapping):
180 """Map the values of the configuration onto new names.
182 Use this function to generate key-word arguments
183 that can be passed on to functions.
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.
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
205 def write(self, stream, header=None, diff_only=False, maxline=60, comments=True):
206 """Pretty print configuration into stream.
208 The description of a configuration parameter is printed out
209 right before its key-value pair with an initial comment
210 character ('#').
212 Section titles get two comment characters prependend ('##').
214 Lines are folded if the character count of parameter
215 descriptions or section title exceeds maxline.
217 A header can be printed initially. This is a simple string that is
218 formatted like the section titles.
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 """
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')
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]
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
282 # only write parameter whose value differs:
283 if diff_only and not differs:
284 continue
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
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
303 def dump(self, filename, header=None, diff_only=False, maxline=60, comments=True):
304 """Pretty print configuration into file.
306 See write() for more details.
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)
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: string
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.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]
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.
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 """
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)
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)
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('')
414 del cfg['nfft']
415 del cfg['windows']
416 cfg.write(sys.stdout)
419if __name__ == "__main__":
420 main()