logilab/common

view configuration.py @ 615:42c8cae57420

fix default values in help
author Emile Anclin <emile.anclin@logilab.fr>
date Mon, 25 Aug 2008 09:10:03 +0200
parents d1162ce2e196
children dbc6c61cd34c
line source
1 """Classes to handle advanced configuration in simple to complex applications.
3 Allows to load the configuration from a file or from command line
4 options, to generate a sample configuration file or to display
5 program's usage. Fills the gap between optik/optparse and ConfigParser
6 by adding data types (which are also available as a standalone optik
7 extension in the `optik_ext` module).
10 Quick start: simplest usage
11 ---------------------------
13 .. python ::
15 >>> import sys
16 >>> from logilab.common.configuration import Configuration
17 >>> options = [('dothis', {'type':'yn', 'default': True, 'metavar': '<y or n>'}),
18 ... ('value', {'type': 'string', 'metavar': '<string>'}),
19 ... ('multiple', {'type': 'csv', 'default': ('yop',),
20 ... 'metavar': '<comma separated values>',
21 ... 'help': 'you can also document the option'}),
22 ... ('number', {'type': 'int', 'default':2, 'metavar':'<int>'}),
23 ... ]
24 >>> config = Configuration(options=options, name='My config')
25 >>> print config['dothis']
26 True
27 >>> print config['value']
28 None
29 >>> print config['multiple']
30 ('yop',)
31 >>> print config['number']
32 2
33 >>> print config.help()
34 Usage: [options]
36 Options:
37 -h, --help show this help message and exit
38 --dothis=<y or n>
39 --value=<string>
40 --multiple=<comma separated values>
41 you can also document the option [current: none]
42 --number=<int>
44 >>> f = open('myconfig.ini', 'w')
45 >>> f.write('''[MY CONFIG]
46 ... number = 3
47 ... dothis = no
48 ... multiple = 1,2,3
49 ... ''')
50 >>> f.close()
51 >>> config.load_file_configuration('myconfig.ini')
52 >>> print config['dothis']
53 False
54 >>> print config['value']
55 None
56 >>> print config['multiple']
57 ['1', '2', '3']
58 >>> print config['number']
59 3
60 >>> sys.argv = ['mon prog', '--value', 'bacon', '--multiple', '4,5,6',
61 ... 'nonoptionargument']
62 >>> print config.load_command_line_configuration()
63 ['nonoptionargument']
64 >>> print config['value']
65 bacon
66 >>> config.generate_config()
67 # class for simple configurations which don't need the
68 # manager / providers model and prefer delegation to inheritance
69 #
70 # configuration values are accessible through a dict like interface
71 #
72 [MY CONFIG]
74 dothis=no
76 value=bacon
78 # you can also document the option
79 multiple=4,5,6
81 number=3
82 >>>
85 :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
86 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
87 :license: General Public License version 2 - http://www.gnu.org/licenses
88 """
89 from __future__ import generators
90 __docformat__ = "restructuredtext en"
92 __all__ = ('OptionsManagerMixIn', 'OptionsProviderMixIn',
93 'ConfigurationMixIn', 'Configuration',
94 'OptionsManager2ConfigurationAdapter')
96 import os
97 import sys
98 import re
99 from os.path import exists
100 from copy import copy
101 from ConfigParser import ConfigParser, NoOptionError, NoSectionError
103 from logilab.common.compat import set
104 from logilab.common.textutils import normalize_text, unquote
105 from logilab.common.optik_ext import OptionParser, OptionGroup, Values, \
106 OptionValueError, OptionError, HelpFormatter, generate_manpage, check_date, \
107 check_yn, check_csv, check_file, check_color, check_named, check_password,\
108 NO_DEFAULT, OPTPARSE_FORMAT_DEFAULT
110 REQUIRED = []
112 class UnsupportedAction(Exception):
113 """raised by set_option when it doesn't know what to do for an action"""
115 # validation functions ########################################################
117 def choice_validator(opt_dict, name, value):
118 """validate and return a converted value for option of type 'choice'
119 """
120 if not value in opt_dict['choices']:
121 msg = "option %s: invalid value: %r, should be in %s"
122 raise OptionValueError(msg % (name, value, opt_dict['choices']))
123 return value
125 def multiple_choice_validator(opt_dict, name, value):
126 """validate and return a converted value for option of type 'choice'
127 """
128 choices = opt_dict['choices']
129 values = check_csv(None, name, value)
130 for value in values:
131 if not value in choices:
132 msg = "option %s: invalid value: %r, should be in %s"
133 raise OptionValueError(msg % (name, value, choices))
134 return values
136 def csv_validator(opt_dict, name, value):
137 """validate and return a converted value for option of type 'csv'
138 """
139 return check_csv(None, name, value)
141 def yn_validator(opt_dict, name, value):
142 """validate and return a converted value for option of type 'yn'
143 """
144 return check_yn(None, name, value)
146 def named_validator(opt_dict, name, value):
147 """validate and return a converted value for option of type 'named'
148 """
149 return check_named(None, name, value)
151 def file_validator(opt_dict, name, value):
152 """validate and return a filepath for option of type 'file'"""
153 return check_file(None, name, value)
155 def color_validator(opt_dict, name, value):
156 """validate and return a valid color for option of type 'color'"""
157 return check_color(None, name, value)
159 def password_validator(opt_dict, name, value):
160 """validate and return a string for option of type 'password'"""
161 return check_password(None, name, value)
163 def date_validator(opt_dict, name, value):
164 """validate and return a mx DateTime object for option of type 'date'"""
165 return check_password(None, name, value)
168 VALIDATORS = {'string' : unquote,
169 'int' : int,
170 'float': float,
171 'file': file_validator,
172 'font': unquote,
173 'color': color_validator,
174 'regexp': re.compile,
175 'csv': csv_validator,
176 'yn': yn_validator,
177 'bool': yn_validator,
178 'named': named_validator,
179 'password': password_validator,
180 'date': date_validator,
181 'choice': choice_validator,
182 'multiple_choice': multiple_choice_validator,
183 }
185 def _call_validator(opttype, optdict, option, value):
186 if not VALIDATORS.has_key(opttype):
187 raise Exception('Unsupported type "%s"' % opttype)
188 try:
189 return VALIDATORS[opttype](optdict, option, value)
190 except TypeError:
191 try:
192 return VALIDATORS[opttype](value)
193 except OptionValueError:
194 raise
195 except:
196 raise OptionValueError('%s value (%r) should be of type %s' %
197 (option, value, opttype))
199 # user input functions ########################################################
201 def input_password(optdict, question='password:'):
202 from getpass import getpass
203 while True:
204 value = getpass(question)
205 value2 = getpass('confirm: ')
206 if value == value2:
207 return value
208 print 'password mismatch, try again'
210 def input_string(optdict, question):
211 value = raw_input(question).strip()
212 return value or None
214 def _make_input_function(opttype):
215 def input_validator(optdict, question):
216 while True:
217 value = raw_input(question)
218 if not value.strip():
219 return None
220 try:
221 return _call_validator(opttype, optdict, None, value)
222 except OptionValueError, ex:
223 msg = str(ex).split(':', 1)[-1].strip()
224 print 'bad value: %s' % msg
225 return input_validator
227 INPUT_FUNCTIONS = {
228 'string': input_string,
229 'password': input_password,
230 }
232 for opttype in VALIDATORS.keys():
233 INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype))
235 def expand_default(self, option):
236 """monkey patch OptionParser.expand_default since we have a particular
237 way to handle defaults to avoid overriding values in the configuration
238 file
239 """
240 if self.parser is None or not self.default_tag:
241 return option.help
242 optname = option._long_opts[0][2:]
243 try:
244 provider = self.parser.options_manager._all_options[optname]
245 except KeyError:
246 value = None
247 else:
248 optdict = provider.get_option_def(optname)
249 optname = provider.option_name(optname, optdict)
250 value = getattr(provider.config, optname, optdict)
251 value = format_option_value(optdict, value)
252 if value is NO_DEFAULT or not value:
253 value = self.NO_DEFAULT_VALUE
254 return option.help.replace(self.default_tag, str(value))
257 def convert(value, opt_dict, name=''):
258 """return a validated value for an option according to its type
260 optional argument name is only used for error message formatting
261 """
262 try:
263 _type = opt_dict['type']
264 except KeyError:
265 # FIXME
266 return value
267 return _call_validator(_type, opt_dict, name, value)
269 def comment(string):
270 """return string as a comment"""
271 lines = [line.strip() for line in string.splitlines()]
272 return '# ' + ('%s# ' % os.linesep).join(lines)
274 def format_option_value(optdict, value):
275 """return the user input's value from a 'compiled' value"""
276 if isinstance(value, (list, tuple)):
277 value = ','.join(value)
278 elif isinstance(value, dict):
279 value = ','.join(['%s:%s' % (k,v) for k,v in value.items()])
280 elif hasattr(value, 'match'): # optdict.get('type') == 'regexp'
281 # compiled regexp
282 value = value.pattern
283 elif optdict.get('type') == 'yn':
284 value = value and 'yes' or 'no'
285 elif isinstance(value, (str, unicode)) and value.isspace():
286 value = "'%s'" % value
287 return value
289 def ini_format_section(stream, section, options, doc=None):
290 """format an options section using the INI format"""
291 if doc:
292 print >> stream, comment(doc)
293 print >> stream, '[%s]' % section
294 for optname, optdict, value in options:
295 value = format_option_value(optdict, value)
296 help = optdict.get('help')
297 if help:
298 print >> stream
299 print >> stream, normalize_text(help, line_len=79, indent='# ')
300 else:
301 print >> stream
302 if value is None:
303 print >> stream, '#%s=' % optname
304 else:
305 print >> stream, '%s=%s' % (optname, str(value).strip())
307 format_section = ini_format_section
309 def rest_format_section(stream, section, options, doc=None):
310 """format an options section using the INI format"""
311 if section:
312 print >> stream, '%s\n%s' % (section, "'"*len(section))
313 if doc:
314 print >> stream, normalize_text(doc, line_len=79, indent='')
315 print >> stream
316 for optname, optdict, value in options:
317 help = optdict.get('help')
318 print >> stream, ':%s:' % optname
319 if help:
320 print >> stream, normalize_text(help, line_len=79, indent=' ')
321 if value:
322 print >> stream, ''
323 print >> stream, ' Default: ``%s``' % str(
324 format_option_value(optdict, value)).replace("`` ","```` ``")
327 class OptionsManagerMixIn(object):
328 """MixIn to handle a configuration from both a configuration file and
329 command line options
330 """
332 def __init__(self, usage, config_file=None, version=None, quiet=0):
333 self.config_file = config_file
334 self.reset_parsers(usage, version=version)
335 # list of registered options providers
336 self.options_providers = []
337 # dictionary assocating option name to checker
338 self._all_options = {}
339 self._short_options = {}
340 self._nocallback_options = {}
341 # verbosity
342 self.quiet = quiet
344 def reset_parsers(self, usage='', version=None):
345 # configuration file parser
346 self._config_parser = ConfigParser()
347 # command line parser
348 self._optik_parser = OptionParser(usage=usage, version=version)
349 self._optik_parser.options_manager = self
351 def register_options_provider(self, provider, own_group=True):
352 """register an options provider"""
353 assert provider.priority <= 0, "provider's priority can't be >= 0"
354 for i in range(len(self.options_providers)):
355 if provider.priority > self.options_providers[i].priority:
356 self.options_providers.insert(i, provider)
357 break
358 else:
359 self.options_providers.append(provider)
360 non_group_spec_options = [option for option in provider.options
361 if not option[1].has_key('group')]
362 groups = getattr(provider, 'option_groups', ())
363 if own_group:
364 self.add_option_group(provider.name.upper(), provider.__doc__,
365 non_group_spec_options, provider)
366 else:
367 for opt_name, opt_dict in non_group_spec_options:
368 args, opt_dict = self.optik_option(provider, opt_name, opt_dict)
369 self._optik_parser.add_option(*args, **opt_dict)
370 self._all_options[opt_name] = provider
371 for gname, gdoc in groups:
372 goptions = [option for option in provider.options
373 if option[1].get('group') == gname]
374 self.add_option_group(gname, gdoc, goptions, provider)
376 def add_option_group(self, group_name, doc, options, provider):
377 """add an option group including the listed options
378 """
379 # add section to the config file
380 self._config_parser.add_section(group_name)
381 # add option group to the command line parser
382 if options:
383 group = OptionGroup(self._optik_parser,
384 title=group_name.capitalize())
385 self._optik_parser.add_option_group(group)
386 # add provider's specific options
387 for opt_name, opt_dict in options:
388 args, opt_dict = self.optik_option(provider, opt_name, opt_dict)
389 group.add_option(*args, **opt_dict)
390 self._all_options[opt_name] = provider
392 def optik_option(self, provider, opt_name, opt_dict):
393 """get our personal option definition and return a suitable form for
394 use with optik/optparse
395 """
396 opt_dict = copy(opt_dict)
397 if opt_dict.has_key('action'):
398 self._nocallback_options[provider] = opt_name
399 else:
400 opt_dict['action'] = 'callback'
401 opt_dict['callback'] = self.cb_set_provider_option
402 for specific in ('default', 'group', 'inputlevel'):
403 if opt_dict.has_key(specific):
404 if (OPTPARSE_FORMAT_DEFAULT
405 and specific == 'default' and opt_dict.has_key('help')):
406 opt_dict['help'] += ' [current: %default]'
407 else:
408 del opt_dict[specific]
409 args = ['--' + opt_name]
410 if opt_dict.has_key('short'):
411 self._short_options[opt_dict['short']] = opt_name
412 args.append('-' + opt_dict['short'])
413 del opt_dict['short']
414 available_keys = set(self._optik_parser.option_class.ATTRS)
415 for key in opt_dict.keys():
416 if not key in available_keys:
417 opt_dict.pop(key)
418 return args, opt_dict
420 def cb_set_provider_option(self, option, opt_name, value, parser):
421 """optik callback for option setting"""
422 if opt_name.startswith('--'):
423 # remove -- on long option
424 opt_name = opt_name[2:]
425 else:
426 # short option, get its long equivalent
427 opt_name = self._short_options[opt_name[1:]]
428 # trick since we can't set action='store_true' on options
429 if value is None:
430 value = 1
431 self.global_set_option(opt_name, value)
433 def global_set_option(self, opt_name, value):
434 """set option on the correct option provider"""
435 self._all_options[opt_name].set_option(opt_name, value)
437 def generate_config(self, stream=None, skipsections=()):
438 """write a configuration file according to the current configuration
439 into the given stream or stdout
440 """
441 stream = stream or sys.stdout
442 printed = False
443 for provider in self.options_providers:
444 default_options = []
445 sections = {}
446 for section, options in provider.options_by_section():
447 if section in skipsections:
448 continue
449 options = [(n, d, v) for (n, d, v) in options
450 if d.get('type') is not None]
451 if not options:
452 continue
453 if section is None:
454 section = provider.name
455 doc = provider.__doc__
456 else:
457 doc = None
458 if printed:
459 print >> stream, '\n'
460 format_section(stream, section.upper(), options, doc)
461 printed = True
463 def generate_manpage(self, pkginfo, section=1, stream=None):
464 """write a man page for the current configuration into the given
465 stream or stdout
466 """
467 generate_manpage(self._optik_parser, pkginfo,
468 section, stream=stream or sys.stdout)
470 # initialization methods ##################################################
472 def load_provider_defaults(self):
473 """initialize configuration using default values"""
474 for provider in self.options_providers:
475 provider.load_defaults()
477 def load_file_configuration(self, config_file=None):
478 """load the configuration from file"""
479 self.read_config_file(config_file)
480 self.load_config_file()
482 def read_config_file(self, config_file=None):
483 """read the configuration file but do not load it (ie dispatching
484 values to each options provider)
485 """
486 if config_file is None:
487 config_file = self.config_file
488 if config_file and exists(config_file):
489 self._config_parser.read([config_file])
490 elif not self.quiet:
491 msg = 'No config file found, using default configuration'
492 print >> sys.stderr, msg
493 return
495 def input_config(self, onlysection=None, inputlevel=0, stream=None):
496 """interactivly get configuration values by asking to the user and generate
497 a configuration file
498 """
499 if onlysection is not None:
500 onlysection = onlysection.upper()
501 for provider in self.options_providers:
502 for section, option, optdict in provider.all_options():
503 if onlysection is not None and section != onlysection:
504 continue
505 provider.input_option(option, optdict, inputlevel)
506 # now we can generate the configuration file
507 if stream is not None:
508 self.generate_config(stream)
510 def load_config_file(self):
511 """dispatch values previously read from a configuration file to each
512 options provider)
513 """
514 parser = self._config_parser
515 for provider in self.options_providers:
516 for section, option, optdict in provider.all_options():
517 try:
518 value = parser.get(section, option)
519 provider.set_option(option, value, opt_dict=optdict)
520 except (NoSectionError, NoOptionError), ex:
521 continue
523 def load_configuration(self, **kwargs):
524 """override configuration according to given parameters
525 """
526 for opt_name, opt_value in kwargs.items():
527 opt_name = opt_name.replace('_', '-')
528 provider = self._all_options[opt_name]
529 provider.set_option(opt_name, opt_value)
531 def load_command_line_configuration(self, args=None):
532 """override configuration according to command line parameters
534 return additional arguments
535 """
536 # monkey patch optparse to deal with our default values
537 try:
538 expand_default_backup = HelpFormatter.expand_default
539 HelpFormatter.expand_default = expand_default
540 except AttributeError:
541 # python < 2.4: nothing to be done
542 pass
543 try:
544 if args is None:
545 args = sys.argv[1:]
546 else:
547 args = list(args)
548 (options, args) = self._optik_parser.parse_args(args=args)
549 for provider in self._nocallback_options.keys():
550 config = provider.config
551 for attr in config.__dict__.keys():
552 value = getattr(options, attr, None)
553 if value is None:
554 continue
555 setattr(config, attr, value)
556 return args
557 finally:
558 if hasattr(HelpFormatter, 'expand_default'):
559 # unpatch optparse to avoid side effects
560 HelpFormatter.expand_default = expand_default_backup
563 # help methods ############################################################
565 def add_help_section(self, title, description):
566 """add a dummy option section for help purpose """
567 group = OptionGroup(self._optik_parser,
568 title=title.capitalize(),
569 description=description)
570 self._optik_parser.add_option_group(group)
572 def help(self):
573 """return the usage string for available options """
574 return self._optik_parser.format_help()
577 class Method(object):
578 """used to ease late binding of default method (so you can define options
579 on the class using default methods on the configuration instance)
580 """
581 def __init__(self, methname):
582 self.method = methname
583 self._inst = None
585 def bind(self, instance):
586 """bind the method to its instance"""
587 if self._inst is None:
588 self._inst = instance
590 def __call__(self):
591 assert self._inst, 'unbound method'
592 return getattr(self._inst, self.method)()
595 class OptionsProviderMixIn(object):
596 """Mixin to provide options to an OptionsManager"""
598 # those attributes should be overridden
599 priority = -1
600 name = 'default'
601 options = ()
603 def __init__(self):
604 self.config = Values()
605 for option in self.options:
606 try:
607 option, optdict = option
608 except ValueError:
609 raise Exception('Bad option: %r' % option)
610 if isinstance(optdict.get('default'), Method):
611 optdict['default'].bind(self)
612 self.load_defaults()
614 def load_defaults(self):
615 """initialize the provider using default values"""
616 for opt_name, opt_dict in self.options:
617 action = opt_dict.get('action')
618 if action != 'callback':
619 # callback action have no default
620 default = self.option_default(opt_name, opt_dict)
621 if default is REQUIRED:
622 continue
623 self.set_option(opt_name, default, action, opt_dict)
625 def option_default(self, opt_name, opt_dict=None):
626 """return the default value for an option"""
627 if opt_dict is None:
628 opt_dict = self.get_option_def(opt_name)
629 default = opt_dict.get('default')
630 if callable(default):
631 default = default()
632 return default
634 def option_name(self, opt_name, opt_dict=None):
635 """get the config attribute corresponding to opt_name
636 """
637 if opt_dict is None:
638 opt_dict = self.get_option_def(opt_name)
639 return opt_dict.get('dest', opt_name.replace('-', '_'))
641 def option_value(self, opt_name):
642 """get the current value for the given option"""
643 return getattr(self.config, self.option_name(opt_name), None)
645 def set_option(self, opt_name, value, action=None, opt_dict=None):
646 """method called to set an option (registered in the options list)
647 """
648 # print "************ setting option", opt_name," to value", value
649 if opt_dict is None:
650 opt_dict = self.get_option_def(opt_name)
651 if value is not None:
652 value = convert(value, opt_dict, opt_name)
653 if action is None:
654 action = opt_dict.get('action', 'store')
655 if opt_dict.get('type') == 'named': # XXX need specific handling
656 optname = self.option_name(opt_name, opt_dict)
657 currentvalue = getattr(self.config, optname, None)
658 if currentvalue:
659 currentvalue.update(value)
660 value = currentvalue
661 if action == 'store':
662 setattr(self.config, self.option_name(opt_name, opt_dict), value)
663 elif action in ('store_true', 'count'):
664 setattr(self.config, self.option_name(opt_name, opt_dict), 0)
665 elif action == 'store_false':
666 setattr(self.config, self.option_name(opt_name, opt_dict), 1)
667 elif action == 'append':
668 opt_name = self.option_name(opt_name, opt_dict)
669 _list = getattr(self.config, opt_name, None)
670 if _list is None:
671 if type(value) in (type(()), type([])):
672 _list = value
673 elif value is not None:
674 _list = []
675 _list.append(value)
676 setattr(self.config, opt_name, _list)
677 elif type(_list) is type(()):
678 setattr(self.config, opt_name, _list + (value,))
679 else:
680 _list.append(value)
681 else:
682 raise UnsupportedAction(action)
684 def input_option(self, option, optdict, inputlevel=99):
685 default = self.option_default(option, optdict)
686 if default is REQUIRED:
687 defaultstr = '(required): '
688 elif optdict.get('inputlevel', 0) > inputlevel:
689 self.set_option(option, default, opt_dict=optdict)
690 return
691 elif optdict['type'] == 'password' or default is None:
692 defaultstr = ': '
693 else:
694 defaultstr = '(default: %s): ' % format_option_value(optdict, default)
695 print ':%s:' % option
696 print optdict.get('help') or option
697 inputfunc = INPUT_FUNCTIONS[optdict['type']]
698 value = inputfunc(optdict, defaultstr)
699 while default is REQUIRED and not value:
700 print 'please specify a value'
701 value = inputfunc(optdict, '%s: ' % option)
702 if value is None and default is not None:
703 value = default
704 self.set_option(option, value, opt_dict=optdict)
706 def get_option_def(self, opt_name):
707 """return the dictionary defining an option given it's name"""
708 assert self.options
709 for opt in self.options:
710 if opt[0] == opt_name:
711 return opt[1]
712 raise OptionError('no such option in section %r' % self.name, opt_name)
715 def all_options(self):
716 """return an iterator on available options for this provider
717 option are actually described by a 3-uple:
718 (section, option name, option dictionary)
719 """
720 for section, options in self.options_by_section():
721 if section is None:
722 section = self.name.upper()
723 for option, optiondict, value in options:
724 yield section, option, optiondict
726 def options_by_section(self):
727 """return an iterator on options grouped by section
729 (section, [list of (optname, optdict, optvalue)])
730 """
731 sections = {}
732 for optname, optdict in self.options:
733 sections.setdefault(optdict.get('group'), []).append(
734 (optname, optdict, self.option_value(optname)))
735 if None in sections:
736 yield None, sections.pop(None)
737 for section, options in sections.items():
738 yield section.upper(), options
741 class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn):
742 """basic mixin for simple configurations which don't need the
743 manager / providers model
744 """
745 def __init__(self, *args, **kwargs):
746 if not args:
747 kwargs.setdefault('usage', '')
748 kwargs.setdefault('quiet', 1)
749 OptionsManagerMixIn.__init__(self, *args, **kwargs)
750 OptionsProviderMixIn.__init__(self)
751 if not getattr(self, 'option_groups', None):
752 self.option_groups = []
753 for option, optdict in self.options:
754 try:
755 gdef = (optdict['group'], '')
756 except KeyError:
757 continue
758 if not gdef in self.option_groups:
759 self.option_groups.append(gdef)
760 self.register_options_provider(self, own_group=0)
762 def register_options(self, options):
763 """add some options to the configuration"""
764 options_by_group = {}
765 for optname, optdict in options:
766 options_by_group.setdefault(optdict.get('group', self.name.upper()), []).append((optname, optdict))
767 for group, options in options_by_group.items():
768 self.add_option_group(group, None, options, self)
769 self.options += tuple(options)
771 def load_defaults(self):
772 OptionsProviderMixIn.load_defaults(self)
774 def __getitem__(self, key):
775 try:
776 return getattr(self.config, self.option_name(key))
777 except (OptionValueError, AttributeError):
778 raise KeyError(key)
780 def __setitem__(self, key, value):
781 self.set_option(self.option_name(key), value)
783 def get(self, key, default=None):
784 try:
785 return getattr(self.config, self.option_name(key))
786 except (OptionError, AttributeError):
787 return default
790 class Configuration(ConfigurationMixIn):
791 """class for simple configurations which don't need the
792 manager / providers model and prefer delegation to inheritance
794 configuration values are accessible through a dict like interface
795 """
797 def __init__(self, config_file=None, options=None, name=None,
798 usage=None, doc=None, version=None):
799 if options is not None:
800 self.options = options
801 if name is not None:
802 self.name = name
803 if doc is not None:
804 self.__doc__ = doc
805 super(Configuration, self).__init__(config_file=config_file, usage=usage, version=version)
808 class OptionsManager2ConfigurationAdapter(object):
809 """Adapt an option manager to behave like a
810 `logilab.common.configuration.Configuration` instance
811 """
812 def __init__(self, provider):
813 self.config = provider
815 def __getattr__(self, key):
816 return getattr(self.config, key)
818 def __getitem__(self, key):
819 provider = self.config._all_options[key]
820 try:
821 return getattr(provider.config, provider.option_name(key))
822 except AttributeError:
823 raise KeyError(key)
825 def __setitem__(self, key, value):
826 self.config.global_set_option(self.config.option_name(key), value)
828 def get(self, key, default=None):
829 provider = self.config._all_options[key]
830 try:
831 return getattr(provider.config, provider.option_name(key))
832 except AttributeError:
833 return default
836 def read_old_config(newconfig, changes, configfile):
837 """initialize newconfig from a deprecated configuration file
839 possible changes:
840 * ('renamed', oldname, newname)
841 * ('moved', option, oldgroup, newgroup)
842 * ('typechanged', option, oldtype, newvalue)
843 """
844 # build an index of changes
845 changesindex = {}
846 for action in changes:
847 if action[0] == 'moved':
848 option, oldgroup, newgroup = action[1:]
849 changesindex.setdefault(option, []).append((action[0], oldgroup, newgroup))
850 continue
851 if action[0] == 'renamed':
852 oldname, newname = action[1:]
853 changesindex.setdefault(newname, []).append((action[0], oldname))
854 continue
855 if action[0] == 'typechanged':
856 option, oldtype, newvalue = action[1:]
857 changesindex.setdefault(option, []).append((action[0], oldtype, newvalue))
858 continue
859 if action[1] in ('added', 'removed'):
860 continue # nothing to do here
861 raise Exception('unknown change %s' % action[0])
862 # build a config object able to read the old config
863 options = []
864 for optname, optdef in newconfig.options:
865 for action in changesindex.pop(optname, ()):
866 if action[0] == 'moved':
867 oldgroup, newgroup = action[1:]
868 optdef = optdef.copy()
869 optdef['group'] = oldgroup
870 elif action[0] == 'renamed':
871 optname = action[1]
872 elif action[0] == 'typechanged':
873 oldtype = action[1]
874 optdef = optdef.copy()
875 optdef['type'] = oldtype
876 options.append((optname, optdef))
877 if changesindex:
878 raise Exception('unapplied changes: %s' % changesindex)
879 oldconfig = Configuration(options=options, name=newconfig.name)
880 # read the old config
881 oldconfig.load_file_configuration(configfile)
882 # apply values reverting changes
883 changes.reverse()
884 done = set()
885 for action in changes:
886 if action[0] == 'renamed':
887 oldname, newname = action[1:]
888 newconfig[newname] = oldconfig[oldname]
889 done.add(newname)
890 elif action[0] == 'typechanged':
891 optname, oldtype, newvalue = action[1:]
892 newconfig[optname] = newvalue
893 done.add(optname)
894 for optname, optdef in newconfig.options:
895 if not optname in done:
896 newconfig.set_option(optname, oldconfig[optname], opt_dict=optdef)
899 def merge_options(options):
900 """preprocess options to remove duplicate"""
901 alloptions = {}
902 options = list(options)
903 for i in range(len(options)-1, -1, -1):
904 optname, optdict = options[i]
905 if optname in alloptions:
906 options.pop(i)
907 alloptions[optname].update(optdict)
908 else:
909 alloptions[optname] = optdict
910 return tuple(options)