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