1 """GNUmed configuration handling.
2 """
3
4 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
5 __licence__ = "GPL"
6
7
8 import logging
9 import sys
10 import io
11 import re as regex
12 import shutil
13 import os
14 import tempfile
15
16
17 if __name__ == "__main__":
18 sys.path.insert(0, '../../')
19 from Gnumed.pycommon import gmBorg
20
21
22 _log = logging.getLogger('gm.cfg')
23
24
25
27
28 group_seen = False
29 option_seen = False
30 in_list = False
31
32 for line in src:
33
34
35 if option_seen:
36 sink.write(line)
37 continue
38
39
40 if regex.match('(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line) is not None:
41 in_list = True
42 sink.write(line)
43 continue
44
45
46 if regex.match('\$.+\$.*', line) is not None:
47 in_list = False
48 sink.write(line)
49 continue
50
51
52 if line.strip() == '[%s]' % group:
53 group_seen = True
54 sink.write(line)
55 continue
56
57
58 if regex.match('\[.+\].*', line) is not None:
59
60 if group_seen and not option_seen:
61 sink.write('%s = %s\n\n\n' % (option, value))
62 option_seen = True
63 sink.write(line)
64 continue
65
66
67 if regex.match('%s(\s|\t)*=' % option, line) is not None:
68 if group_seen:
69 sink.write('%s = %s\n' % (option, value))
70 option_seen = True
71 continue
72 sink.write(line)
73 continue
74
75
76 sink.write(line)
77
78
79 if option_seen:
80 return
81
82
83 if not group_seen:
84 sink.write('[%s]\n' % group)
85
86
87
88
89
90 sink.write('%s = %s\n' % (option, value))
91
92
94
95 our_group_seen = False
96 inside_our_group = False
97 our_list_seen = False
98 inside_our_list = False
99
100
101 for line in src:
102
103 if inside_our_list:
104
105
106 if regex.match('\$%s\$' % option, line.strip()) is not None:
107 inside_our_list = False
108 continue
109
110 continue
111
112 if inside_our_group:
113
114 if regex.match('%s(\s|\t)*=(\s|\t)*\$%s\$' % (option, option), line.strip()) is not None:
115 sink.write(line)
116 sink.write('\n'.join(value))
117 sink.write('\n')
118 sink.write('$%s$\n' % option)
119 our_list_seen = True
120 inside_our_list = True
121 continue
122
123
124 if regex.match('\[.+\]', line.strip()) is not None:
125
126 if not our_list_seen:
127
128 sink.write('%s = $%s$\n' % (option, option))
129 sink.write('\n'.join(value))
130 sink.write('\n')
131 sink.write('$%s$\n' % option)
132 our_list_seen = True
133 inside_our_list = False
134
135 sink.write(line)
136 inside_our_group = False
137 continue
138
139
140 sink.write(line)
141 continue
142
143
144 if line.strip() == '[%s]' % group:
145 our_group_seen = True
146 inside_our_group = True
147 sink.write(line)
148 continue
149
150 sink.write(line)
151
152
153 if not our_group_seen:
154 sink.write('[%s]\n' % group)
155
156 if not our_list_seen:
157
158
159
160
161 sink.write('%s = $%s$\n' % (option, option))
162 sink.write('\n'.join(value))
163 sink.write('\n')
164 sink.write('$%s$\n' % option)
165
166
168
169 our_group_seen = False
170 option_seen = False
171 in_list = False
172
173 for line in src:
174
175
176 if option_seen and in_list:
177
178 if regex.match('\$.+\$.*', line) is not None:
179 in_list = False
180 sink.write(line)
181 continue
182 continue
183
184
185 if option_seen and not in_list:
186 sink.write(line)
187 continue
188
189
190 match = regex.match('(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line)
191 if match is not None:
192 in_list = True
193
194 if our_group_seen and (match.group('list_name') == option):
195 option_seen = True
196 sink.write(line)
197 sink.write('\n'.join(value))
198 sink.write('\n')
199 continue
200 sink.write(line)
201 continue
202
203
204 if regex.match('\$.+\$.*', line) is not None:
205 in_list = False
206 sink.write(line)
207 continue
208
209
210 if line.strip() == '[%s]' % group:
211 sink.write(line)
212 our_group_seen = True
213 continue
214
215
216 if regex.match('\[%s\].*' % group, line) is not None:
217
218 if our_group_seen and not option_seen:
219 option_seen = True
220 sink.write('%s = $%s$\n' % (option, option))
221 sink.write('\n'.join(value))
222 sink.write('\n')
223 continue
224 sink.write(line)
225 continue
226
227
228 sink.write(line)
229
230
231 if option_seen:
232 return
233
234
235 if not our_group_seen:
236 sink.write('[%s]\n' % group)
237
238
239
240
241
242 sink.write('%s = $%s$\n' % (option, option))
243 sink.write('\n'.join(value))
244 sink.write('\n')
245 sink.write('$%s$\n' % option)
246
247
249
250 _log.debug('setting option "%s" to "%s" in group [%s]', option, value, group)
251 _log.debug('file: %s (%s)', filename, encoding)
252
253 sink = tempfile.NamedTemporaryFile(suffix = '.cfg', delete = True)
254 sink_name = sink.name
255 sink.close()
256 src = io.open(filename, mode = 'rt', encoding = encoding)
257 sink = io.open(sink_name, mode = 'wt', encoding = encoding)
258
259
260 if isinstance(value, type([])):
261 __set_list_in_INI_file(src, sink, group, option, value)
262 else:
263 __set_opt_in_INI_file(src, sink, group, option, value)
264
265 sink.close()
266 src.close()
267
268 shutil.copy2(sink_name, filename)
269
270
272 """Parse an iterable for INI-style data.
273
274 Returns a dict by sections containing a dict of values per section.
275 """
276 _log.debug('parsing INI-style data stream [%s] using [%s]', stream, encoding)
277
278 if encoding is None:
279 encoding = 'utf8'
280
281 data = {}
282 current_group = None
283 current_option = None
284 current_option_path = None
285 inside_list = False
286 line_idx = 0
287
288 for line in stream:
289 if type(line) is bytes:
290 line = line.decode(encoding)
291 line = line.replace('\015', '').replace('\012', '').strip()
292 line_idx += 1
293
294 if inside_list:
295 if line == '$%s$' % current_option:
296 inside_list = False
297 continue
298 data[current_option_path].append(line)
299 continue
300
301
302 if line == '' or line.startswith('#') or line.startswith(';'):
303 continue
304
305
306 if line.startswith('['):
307 if not line.endswith(']'):
308 _log.error('group line does not end in "]", aborting')
309 _log.error(line)
310 raise ValueError('INI-stream parsing error')
311 group = line.strip('[]').strip()
312 if group == '':
313 _log.error('group name is empty, aborting')
314 _log.error(line)
315 raise ValueError('INI-stream parsing error')
316 current_group = group
317 continue
318
319
320 if current_group is None:
321 _log.warning('option found before first group, ignoring')
322 _log.error(line)
323 continue
324
325 name, remainder = regex.split('\s*[=:]\s*', line, maxsplit = 1)
326 if name == '':
327 _log.error('option name empty, aborting')
328 _log.error(line)
329 raise ValueError('INI-stream parsing error')
330
331 if remainder.strip() == '':
332 if ('=' not in line) and (':' not in line):
333 _log.error('missing name/value separator (= or :), aborting')
334 _log.error(line)
335 raise ValueError('INI-stream parsing error')
336
337 current_option = name
338 current_option_path = '%s::%s' % (current_group, current_option)
339 if current_option_path in data:
340 _log.warning('duplicate option [%s]', current_option_path)
341
342 value = remainder.split('#', 1)[0].strip()
343
344
345 if value == '$%s$' % current_option:
346 inside_list = True
347 data[current_option_path] = []
348 continue
349
350 data[current_option_path] = value
351
352 if inside_list:
353 _log.critical('unclosed list $%s$ detected at end of config stream [%s]', current_option, stream)
354 raise SyntaxError('end of config stream but still in list')
355
356 return data
357
359
361 try:
362 self.__cfg_data
363 except AttributeError:
364 self.__cfg_data = {}
365 self.source_files = {}
366
367
368 - def get(self, group=None, option=None, source_order=None):
369 """Get the value of a configuration option in a config file.
370
371 <source_order> the order in which config files are searched
372 a list of tuples (source, policy)
373 policy:
374 return: return only this value immediately
375 append: append to list of potential values to return
376 extend: if the value per source happens to be a list
377 extend (rather than append to) the result list
378
379 returns NONE when there's no value for an option
380 """
381 if source_order is None:
382 source_order = [('internal', 'return')]
383 results = []
384 for source, policy in source_order:
385 if group is None:
386 group = source
387 option_path = '%s::%s' % (group, option)
388 try: source_data = self.__cfg_data[source]
389 except KeyError:
390 _log.error('invalid config source [%s]', source)
391 _log.debug('currently known sources: %s', self.__cfg_data.keys())
392
393 continue
394
395 try: value = source_data[option_path]
396 except KeyError:
397 _log.debug('option [%s] not in group [%s] in source [%s]', option, group, source)
398 continue
399 _log.debug('option [%s] found in source [%s]', option_path, source)
400
401 if policy == 'return':
402 return value
403
404 if policy == 'extend':
405 if isinstance(value, type([])):
406 results.extend(value)
407 else:
408 results.append(value)
409 else:
410 results.append(value)
411
412 if len(results) == 0:
413 return None
414
415 return results
416
417
418 - def set_option(self, option=None, value=None, group=None, source=None):
419 """Set a particular option to a particular value.
420
421 Note that this does NOT PERSIST the option anywhere !
422 """
423 if None in [option, value]:
424 raise ValueError('neither <option> nor <value> can be None')
425 if source is None:
426 source = 'internal'
427 try:
428 self.__cfg_data[source]
429 except KeyError:
430 self.__cfg_data[source] = {}
431 if group is None:
432 group = source
433 option_path = '%s::%s' % (group, option)
434 self.__cfg_data[source][option_path] = value
435
436
437
439 try:
440 data = parse_INI_stream(stream = stream, encoding = encoding)
441 except ValueError:
442 _log.exception('error parsing source <%s> from [%s]', source, stream)
443 raise
444
445 if source in self.__cfg_data:
446 _log.warning('overriding source <%s> with [%s]', source, stream)
447
448 self.__cfg_data[source] = data
449
451 """Add a source (a file) to the instance."""
452
453 _log.info('file source "%s": %s (%s)', source, file, encoding)
454
455 for existing_source, existing_file in self.source_files.items():
456 if existing_file == file:
457 if source != existing_source:
458 _log.warning('file [%s] already known as source [%s]', file, existing_source)
459 _log.warning('adding it as source [%s] may provoke trouble', source)
460
461 cfg_file = None
462 if file is not None:
463 try:
464 cfg_file = io.open(file, mode = 'rt', encoding = encoding)
465 except IOError:
466 _log.error('cannot open [%s], keeping as dummy source', file)
467
468 if cfg_file is None:
469 file = None
470 if source in self.__cfg_data:
471 _log.warning('overriding source <%s> with dummy', source)
472 self.__cfg_data[source] = {}
473 else:
474 self.add_stream_source(source = source, stream = cfg_file)
475 cfg_file.close()
476
477 self.source_files[source] = file
478
480 """Remove a source from the instance."""
481
482 _log.info('removing source <%s>', source)
483
484 try:
485 del self.__cfg_data[source]
486 except KeyError:
487 _log.warning("source <%s> doesn't exist", source)
488
489 try:
490 del self.source_files[source]
491 except KeyError:
492 pass
493
495 if file not in self.source_files.values():
496 return
497
498 for src, fname in self.source_files.items():
499 if fname == file:
500 self.add_file_source(source = src, file = fname, encoding = encoding)
501
502
503
504
505 - def add_cli(self, short_options='', long_options=None):
506 """Add command line parameters to config data.
507
508 short:
509 string containing one-letter options such as u'h?' for -h -?
510 long:
511 list of strings
512 'conf-file=' -> --conf-file=<...>
513 'debug' -> --debug
514 """
515 _log.info('adding command line arguments')
516 _log.debug('raw command line is:')
517 _log.debug('%s', sys.argv)
518
519 import getopt
520
521 if long_options is None:
522 long_options = []
523
524 opts, remainder = getopt.gnu_getopt (
525 sys.argv[1:],
526 short_options,
527 long_options
528 )
529
530 data = {}
531 for opt, val in opts:
532 if val == '':
533 data['%s::%s' % ('cli', opt)] = True
534 else:
535 data['%s::%s' % ('cli', opt)] = val
536
537 self.__cfg_data['cli'] = data
538
539
540
541 if __name__ == "__main__":
542
543 if len(sys.argv) < 2:
544 sys.exit()
545
546 if sys.argv[1] != 'test':
547 sys.exit()
548
549 logging.basicConfig(level = logging.DEBUG)
550
552 cfg = gmCfgData()
553 cfg.add_cli(short_options='h?', long_options=['help', 'conf-file='])
554 cfg.set_option('internal option', True)
555 print (cfg.get(option = '--help', source_order = [('cli', 'return')]))
556 print (cfg.get(option = '-?', source_order = [('cli', 'return')]))
557 fname = cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
558 if fname is not None:
559 cfg.add_file_source(source = 'explicit', file = fname)
560
562 src = [
563 '# a comment',
564 '',
565 '[empty group]',
566 '[second group]',
567 'some option = in second group',
568 '# another comment',
569 '[test group]',
570 '',
571 'test list = $test list$',
572 'old 1',
573 'old 2',
574 '$test list$',
575 '# another group:',
576 '[dummy group]'
577 ]
578
579 __set_list_in_INI_file (
580 src = src,
581 sink = sys.stdout,
582 group = 'test group',
583 option = 'test list',
584 value = list('123')
585 )
586
588 src = [
589 '# a comment',
590 '[empty group]',
591 '# another comment',
592 '',
593 '[second group]',
594 'some option = in second group',
595 '',
596 '[trap group]',
597 'trap list = $trap list$',
598 'dummy 1',
599 'test option = a trap',
600 'dummy 2',
601 '$trap list$',
602 '',
603 '[test group]',
604 'test option = for real (old)',
605 ''
606 ]
607
608 __set_opt_in_INI_file (
609 src = src,
610 sink = sys.stdout,
611 group = 'test group',
612 option = 'test option',
613 value = 'for real (new)'
614 )
615
616
617 test_set_list_opt()
618
619
620
621