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