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', list(self.__cfg_data))
392 continue
393
394 try: value = source_data[option_path]
395 except KeyError:
396 _log.debug('option [%s] not in group [%s] in source [%s]', option, group, source)
397 continue
398 _log.debug('option [%s] found in source [%s]', option_path, source)
399
400 if policy == 'return':
401 return value
402
403 if policy == 'extend':
404 if isinstance(value, type([])):
405 results.extend(value)
406 else:
407 results.append(value)
408 else:
409 results.append(value)
410
411 if len(results) == 0:
412 return None
413
414 return results
415
416
417 - def set_option(self, option=None, value=None, group=None, source=None):
418 """Set a particular option to a particular value.
419
420 Note that this does NOT PERSIST the option anywhere !
421 """
422 if None in [option, value]:
423 raise ValueError('neither <option> nor <value> can be None')
424 if source is None:
425 source = 'internal'
426 try:
427 self.__cfg_data[source]
428 except KeyError:
429 self.__cfg_data[source] = {}
430 if group is None:
431 group = source
432 option_path = '%s::%s' % (group, option)
433 self.__cfg_data[source][option_path] = value
434
435
436
438 data = parse_INI_stream(stream = stream, encoding = encoding)
439 if source in self.__cfg_data:
440 _log.warning('overriding source <%s> with [%s]', source, stream)
441
442 self.__cfg_data[source] = data
443
445 """Add a source (a file) to the instance."""
446
447 _log.info('file source "%s": %s (%s)', source, file, encoding)
448
449 for existing_source, existing_file in self.source_files.items():
450 if existing_file == file:
451 if source != existing_source:
452 _log.warning('file [%s] already known as source [%s]', file, existing_source)
453 _log.warning('adding it as source [%s] may provoke trouble', source)
454
455 cfg_file = None
456 if file is not None:
457 try:
458 cfg_file = io.open(file, mode = 'rt', encoding = encoding)
459 except IOError:
460 _log.error('cannot open [%s], keeping as dummy source', file)
461
462 if cfg_file is None:
463 file = None
464 if source in self.__cfg_data:
465 _log.warning('overriding source <%s> with dummy', source)
466 self.__cfg_data[source] = {}
467 else:
468 self.add_stream_source(source = source, stream = cfg_file)
469 cfg_file.close()
470
471 self.source_files[source] = file
472
474 """Remove a source from the instance."""
475
476 _log.info('removing source <%s>', source)
477
478 try:
479 del self.__cfg_data[source]
480 except KeyError:
481 _log.warning("source <%s> doesn't exist", source)
482
483 try:
484 del self.source_files[source]
485 except KeyError:
486 pass
487
489 if file not in self.source_files.values():
490 return
491
492 for src, fname in self.source_files.items():
493 if fname == file:
494 self.add_file_source(source = src, file = fname, encoding = encoding)
495
496
497
498
499 - def add_cli(self, short_options='', long_options=None):
500 """Add command line parameters to config data.
501
502 short:
503 string containing one-letter options such as u'h?' for -h -?
504 long:
505 list of strings
506 'conf-file=' -> --conf-file=<...>
507 'debug' -> --debug
508 """
509 _log.info('adding command line arguments')
510 _log.debug('raw command line is:')
511 _log.debug('%s', sys.argv)
512
513 import getopt
514
515 if long_options is None:
516 long_options = []
517
518 opts, remainder = getopt.gnu_getopt (
519 sys.argv[1:],
520 short_options,
521 long_options
522 )
523
524 data = {}
525 for opt, val in opts:
526 if val == '':
527 data['%s::%s' % ('cli', opt)] = True
528 else:
529 data['%s::%s' % ('cli', opt)] = val
530
531 self.__cfg_data['cli'] = data
532
533
534
535 if __name__ == "__main__":
536
537 if len(sys.argv) < 2:
538 sys.exit()
539
540 if sys.argv[1] != 'test':
541 sys.exit()
542
543 logging.basicConfig(level = logging.DEBUG)
544
546 cfg = gmCfgData()
547 cfg.add_cli(short_options='h?', long_options=['help', 'conf-file='])
548 cfg.set_option('internal option', True)
549 print (cfg.get(option = '--help', source_order = [('cli', 'return')]))
550 print (cfg.get(option = '-?', source_order = [('cli', 'return')]))
551 fname = cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
552 if fname is not None:
553 cfg.add_file_source(source = 'explicit', file = fname)
554
556 src = [
557 '# a comment',
558 '',
559 '[empty group]',
560 '[second group]',
561 'some option = in second group',
562 '# another comment',
563 '[test group]',
564 '',
565 'test list = $test list$',
566 'old 1',
567 'old 2',
568 '$test list$',
569 '# another group:',
570 '[dummy group]'
571 ]
572
573 __set_list_in_INI_file (
574 src = src,
575 sink = sys.stdout,
576 group = 'test group',
577 option = 'test list',
578 value = list('123')
579 )
580
582 src = [
583 '# a comment',
584 '[empty group]',
585 '# another comment',
586 '',
587 '[second group]',
588 'some option = in second group',
589 '',
590 '[trap group]',
591 'trap list = $trap list$',
592 'dummy 1',
593 'test option = a trap',
594 'dummy 2',
595 '$trap list$',
596 '',
597 '[test group]',
598 'test option = for real (old)',
599 ''
600 ]
601
602 __set_opt_in_INI_file (
603 src = src,
604 sink = sys.stdout,
605 group = 'test group',
606 option = 'test option',
607 value = 'for real (new)'
608 )
609
610
611 test_set_list_opt()
612
613
614
615