Package logilab-common-0 ::
Package 39 ::
Package 0 ::
Module configuration
|
|
1 """Classes to handle advanced configuration in simple to complex applications.
2
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).
8
9
10 Quick start: simplest usage
11 ---------------------------
12
13 .. python ::
14
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]
35
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>
43
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]
73
74 dothis=no
75
76 value=bacon
77
78 # you can also document the option
79 multiple=4,5,6
80
81 number=3
82 >>>
83
84
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"
91
92 __all__ = ('OptionsManagerMixIn', 'OptionsProviderMixIn',
93 'ConfigurationMixIn', 'Configuration',
94 'OptionsManager2ConfigurationAdapter')
95
96 import os
97 import sys
98 import re
99 from os.path import exists, expanduser
100 from copy import copy
101 from ConfigParser import ConfigParser, NoOptionError, NoSectionError
102
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
109
110 REQUIRED = []
111
113 """raised by set_option when it doesn't know what to do for an action"""
114
115
117 encoding = encoding or getattr(stream, 'encoding', None)
118 if not encoding:
119 import locale
120 encoding = locale.getpreferredencoding()
121 return encoding
122
124 if isinstance(string, unicode):
125 return string.encode(encoding)
126 return str(string)
127
128
129
130
132 """validate and return a converted value for option of type 'choice'
133 """
134 if not value in opt_dict['choices']:
135 msg = "option %s: invalid value: %r, should be in %s"
136 raise OptionValueError(msg % (name, value, opt_dict['choices']))
137 return value
138
140 """validate and return a converted value for option of type 'choice'
141 """
142 choices = opt_dict['choices']
143 values = check_csv(None, name, value)
144 for value in values:
145 if not value in choices:
146 msg = "option %s: invalid value: %r, should be in %s"
147 raise OptionValueError(msg % (name, value, choices))
148 return values
149
151 """validate and return a converted value for option of type 'csv'
152 """
153 return check_csv(None, name, value)
154
156 """validate and return a converted value for option of type 'yn'
157 """
158 return check_yn(None, name, value)
159
161 """validate and return a converted value for option of type 'named'
162 """
163 return check_named(None, name, value)
164
166 """validate and return a filepath for option of type 'file'"""
167 return check_file(None, name, value)
168
170 """validate and return a valid color for option of type 'color'"""
171 return check_color(None, name, value)
172
174 """validate and return a string for option of type 'password'"""
175 return check_password(None, name, value)
176
178 """validate and return a mx DateTime object for option of type 'date'"""
179 return check_date(None, name, value)
180
181
182 VALIDATORS = {'string' : unquote,
183 'int' : int,
184 'float': float,
185 'file': file_validator,
186 'font': unquote,
187 'color': color_validator,
188 'regexp': re.compile,
189 'csv': csv_validator,
190 'yn': yn_validator,
191 'bool': yn_validator,
192 'named': named_validator,
193 'password': password_validator,
194 'date': date_validator,
195 'choice': choice_validator,
196 'multiple_choice': multiple_choice_validator,
197 }
198
200 if opttype not in VALIDATORS:
201 raise Exception('Unsupported type "%s"' % opttype)
202 try:
203 return VALIDATORS[opttype](optdict, option, value)
204 except TypeError:
205 try:
206 return VALIDATORS[opttype](value)
207 except OptionValueError:
208 raise
209 except:
210 raise OptionValueError('%s value (%r) should be of type %s' %
211 (option, value, opttype))
212
213
214
223
227
239 return input_validator
240
241 INPUT_FUNCTIONS = {
242 'string': input_string,
243 'password': input_password,
244 }
245
246 for opttype in VALIDATORS.keys():
247 INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype))
248
250 """monkey patch OptionParser.expand_default since we have a particular
251 way to handle defaults to avoid overriding values in the configuration
252 file
253 """
254 if self.parser is None or not self.default_tag:
255 return option.help
256 optname = option._long_opts[0][2:]
257 try:
258 provider = self.parser.options_manager._all_options[optname]
259 except KeyError:
260 value = None
261 else:
262 optdict = provider.get_option_def(optname)
263 optname = provider.option_name(optname, optdict)
264 value = getattr(provider.config, optname, optdict)
265 value = format_option_value(optdict, value)
266 if value is NO_DEFAULT or not value:
267 value = self.NO_DEFAULT_VALUE
268 return option.help.replace(self.default_tag, str(value))
269
270
271 -def convert(value, opt_dict, name=''):
272 """return a validated value for an option according to its type
273
274 optional argument name is only used for error message formatting
275 """
276 try:
277 _type = opt_dict['type']
278 except KeyError:
279
280 return value
281 return _call_validator(_type, opt_dict, name, value)
282
287
302
323
324 format_section = ini_format_section
325
345
346
348 """MixIn to handle a configuration from both a configuration file and
349 command line options
350 """
351
352 - def __init__(self, usage, config_file=None, version=None, quiet=0):
353 self.config_file = config_file
354 self.reset_parsers(usage, version=version)
355
356 self.options_providers = []
357
358 self._all_options = {}
359 self._short_options = {}
360 self._nocallback_options = {}
361
362 self.quiet = quiet
363
365
366 self._config_parser = ConfigParser()
367
368 self._optik_parser = OptionParser(usage=usage, version=version)
369 self._optik_parser.options_manager = self
370
372 """register an options provider"""
373 assert provider.priority <= 0, "provider's priority can't be >= 0"
374 for i in range(len(self.options_providers)):
375 if provider.priority > self.options_providers[i].priority:
376 self.options_providers.insert(i, provider)
377 break
378 else:
379 self.options_providers.append(provider)
380 non_group_spec_options = [option for option in provider.options
381 if 'group' not in option[1]]
382 groups = getattr(provider, 'option_groups', ())
383 if own_group:
384 self.add_option_group(provider.name.upper(), provider.__doc__,
385 non_group_spec_options, provider)
386 else:
387 for opt_name, opt_dict in non_group_spec_options:
388 args, opt_dict = self.optik_option(provider, opt_name, opt_dict)
389 self._optik_parser.add_option(*args, **opt_dict)
390
391 self._all_options[opt_name] = provider
392 for gname, gdoc in groups:
393 gname = gname.upper()
394 goptions = [option for option in provider.options
395 if option[1].get('group', '').upper() == gname]
396 self.add_option_group(gname, gdoc, goptions, provider)
397
399 """add an option group including the listed options
400 """
401
402 if group_name != "DEFAULT":
403 self._config_parser.add_section(group_name)
404
405 if options:
406 group = OptionGroup(self._optik_parser,
407 title=group_name.capitalize())
408 self._optik_parser.add_option_group(group)
409
410 for opt_name, opt_dict in options:
411 args, opt_dict = self.optik_option(provider, opt_name, opt_dict)
412 group.add_option(*args, **opt_dict)
413 self._all_options[opt_name] = provider
414
416 """get our personal option definition and return a suitable form for
417 use with optik/optparse
418 """
419 opt_dict = copy(opt_dict)
420 if 'action' in opt_dict:
421 self._nocallback_options[provider] = opt_name
422 else:
423 opt_dict['action'] = 'callback'
424 opt_dict['callback'] = self.cb_set_provider_option
425
426
427 if 'default' in opt_dict:
428 if OPTPARSE_FORMAT_DEFAULT and 'help' in opt_dict:
429 opt_dict['help'] += ' [current: %default]'
430 del opt_dict['default']
431 args = ['--' + opt_name]
432 if 'short' in opt_dict:
433 self._short_options[opt_dict['short']] = opt_name
434 args.append('-' + opt_dict['short'])
435 del opt_dict['short']
436 available_keys = set(self._optik_parser.option_class.ATTRS)
437
438 for key in opt_dict.keys():
439 if not key in available_keys:
440 opt_dict.pop(key)
441 return args, opt_dict
442
444 """optik callback for option setting"""
445 if opt_name.startswith('--'):
446
447 opt_name = opt_name[2:]
448 else:
449
450 opt_name = self._short_options[opt_name[1:]]
451
452 if value is None:
453 value = 1
454 self.global_set_option(opt_name, value)
455
457 """set option on the correct option provider"""
458 self._all_options[opt_name].set_option(opt_name, value)
459
461 """write a configuration file according to the current configuration
462 into the given stream or stdout
463 """
464 stream = stream or sys.stdout
465 encoding = _get_encoding(encoding, stream)
466 printed = False
467 for provider in self.options_providers:
468 default_options = []
469 sections = {}
470 for section, options in provider.options_by_section():
471 if section in skipsections:
472 continue
473 options = [(n, d, v) for (n, d, v) in options
474 if d.get('type') is not None]
475 if not options:
476 continue
477 if section is None:
478 section = provider.name
479 doc = provider.__doc__
480 else:
481 doc = None
482 if printed:
483 print >> stream, '\n'
484 format_section(stream, section.upper(), options, encoding, doc)
485 printed = True
486
487 - def generate_manpage(self, pkginfo, section=1, stream=None):
488 """write a man page for the current configuration into the given
489 stream or stdout
490 """
491 self._monkeypatch_expand_default()
492 try:
493 generate_manpage(self._optik_parser, pkginfo,
494 section, stream=stream or sys.stdout)
495 finally:
496 self._unmonkeypatch_expand_default()
497
498
499
501 """initialize configuration using default values"""
502 for provider in self.options_providers:
503 provider.load_defaults()
504
509
511 """read the configuration file but do not load it (ie dispatching
512 values to each options provider)
513 """
514 if config_file is None:
515 config_file = self.config_file
516 if config_file is not None:
517 config_file = expanduser(config_file)
518 if config_file and exists(config_file):
519 parser = self._config_parser
520 parser.read([config_file])
521
522 for sect, values in parser._sections.items():
523 if not sect.isupper() and values:
524 parser._sections[sect.upper()] = values
525 elif not self.quiet:
526 msg = 'No config file found, using default configuration'
527 print >> sys.stderr, msg
528 return
529
544
546 """dispatch values previously read from a configuration file to each
547 options provider)
548 """
549 parser = self._config_parser
550 for provider in self.options_providers:
551 for section, option, optdict in provider.all_options():
552 try:
553 value = parser.get(section, option)
554 provider.set_option(option, value, opt_dict=optdict)
555 except (NoSectionError, NoOptionError), ex:
556 continue
557
559 """override configuration according to given parameters
560 """
561 for opt_name, opt_value in kwargs.items():
562 opt_name = opt_name.replace('_', '-')
563 provider = self._all_options[opt_name]
564 provider.set_option(opt_name, opt_value)
565
567 """override configuration according to command line parameters
568
569 return additional arguments
570 """
571 self._monkeypatch_expand_default()
572 try:
573 if args is None:
574 args = sys.argv[1:]
575 else:
576 args = list(args)
577 (options, args) = self._optik_parser.parse_args(args=args)
578 for provider in self._nocallback_options.keys():
579 config = provider.config
580 for attr in config.__dict__.keys():
581 value = getattr(options, attr, None)
582 if value is None:
583 continue
584 setattr(config, attr, value)
585 return args
586 finally:
587 self._unmonkeypatch_expand_default()
588
589
590
591
593 """add a dummy option section for help purpose """
594 group = OptionGroup(self._optik_parser,
595 title=title.capitalize(),
596 description=description)
597 self._optik_parser.add_option_group(group)
598
600
601 try:
602 self.__expand_default_backup = HelpFormatter.expand_default
603 HelpFormatter.expand_default = expand_default
604 except AttributeError:
605
606 pass
608
609 if hasattr(HelpFormatter, 'expand_default'):
610
611 HelpFormatter.expand_default = self.__expand_default_backup
612
614 """return the usage string for available options """
615 self._monkeypatch_expand_default()
616 try:
617 return self._optik_parser.format_help()
618 finally:
619 self._unmonkeypatch_expand_default()
620
621
623 """used to ease late binding of default method (so you can define options
624 on the class using default methods on the configuration instance)
625 """
627 self.method = methname
628 self._inst = None
629
630 - def bind(self, instance):
631 """bind the method to its instance"""
632 if self._inst is None:
633 self._inst = instance
634
636 assert self._inst, 'unbound method'
637 return getattr(self._inst, self.method)()
638
639
641 """Mixin to provide options to an OptionsManager"""
642
643
644 priority = -1
645 name = 'default'
646 options = ()
647
649 self.config = Values()
650 for option in self.options:
651 try:
652 option, optdict = option
653 except ValueError:
654 raise Exception('Bad option: %r' % option)
655 if isinstance(optdict.get('default'), Method):
656 optdict['default'].bind(self)
657 self.load_defaults()
658
660 """initialize the provider using default values"""
661 for opt_name, opt_dict in self.options:
662 action = opt_dict.get('action')
663 if action != 'callback':
664
665 default = self.option_default(opt_name, opt_dict)
666 if default is REQUIRED:
667 continue
668 self.set_option(opt_name, default, action, opt_dict)
669
671 """return the default value for an option"""
672 if opt_dict is None:
673 opt_dict = self.get_option_def(opt_name)
674 default = opt_dict.get('default')
675 if callable(default):
676 default = default()
677 return default
678
680 """get the config attribute corresponding to opt_name
681 """
682 if opt_dict is None:
683 opt_dict = self.get_option_def(opt_name)
684 return opt_dict.get('dest', opt_name.replace('-', '_'))
685
687 """get the current value for the given option"""
688 return getattr(self.config, self.option_name(opt_name), None)
689
690 - def set_option(self, opt_name, value, action=None, opt_dict=None):
691 """method called to set an option (registered in the options list)
692 """
693
694 if opt_dict is None:
695 opt_dict = self.get_option_def(opt_name)
696 if value is not None:
697 value = convert(value, opt_dict, opt_name)
698 if action is None:
699 action = opt_dict.get('action', 'store')
700 if opt_dict.get('type') == 'named':
701 optname = self.option_name(opt_name, opt_dict)
702 currentvalue = getattr(self.config, optname, None)
703 if currentvalue:
704 currentvalue.update(value)
705 value = currentvalue
706 if action == 'store':
707 setattr(self.config, self.option_name(opt_name, opt_dict), value)
708 elif action in ('store_true', 'count'):
709 setattr(self.config, self.option_name(opt_name, opt_dict), 0)
710 elif action == 'store_false':
711 setattr(self.config, self.option_name(opt_name, opt_dict), 1)
712 elif action == 'append':
713 opt_name = self.option_name(opt_name, opt_dict)
714 _list = getattr(self.config, opt_name, None)
715 if _list is None:
716 if type(value) in (type(()), type([])):
717 _list = value
718 elif value is not None:
719 _list = []
720 _list.append(value)
721 setattr(self.config, opt_name, _list)
722 elif type(_list) is type(()):
723 setattr(self.config, opt_name, _list + (value,))
724 else:
725 _list.append(value)
726 elif action == 'callback':
727 opt_dict['callback'](None, opt_name, value, None)
728 else:
729 raise UnsupportedAction(action)
730
752
754 """return the dictionary defining an option given it's name"""
755 assert self.options
756 for opt in self.options:
757 if opt[0] == opt_name:
758 return opt[1]
759 raise OptionError('no such option in section %r' % self.name, opt_name)
760
761
763 """return an iterator on available options for this provider
764 option are actually described by a 3-uple:
765 (section, option name, option dictionary)
766 """
767 for section, options in self.options_by_section():
768 if section is None:
769 section = self.name.upper()
770 for option, optiondict, value in options:
771 yield section, option, optiondict
772
774 """return an iterator on options grouped by section
775
776 (section, [list of (optname, optdict, optvalue)])
777 """
778 sections = {}
779 for optname, optdict in self.options:
780 sections.setdefault(optdict.get('group'), []).append(
781 (optname, optdict, self.option_value(optname)))
782 if None in sections:
783 yield None, sections.pop(None)
784 for section, options in sections.items():
785 yield section.upper(), options
786
787
789 """basic mixin for simple configurations which don't need the
790 manager / providers model
791 """
808
817
820
822 return iter(self.config.__dict__.iteritems())
823
825 try:
826 return getattr(self.config, self.option_name(key))
827 except (OptionValueError, AttributeError):
828 raise KeyError(key)
829
832
833 - def get(self, key, default=None):
834 try:
835 return getattr(self.config, self.option_name(key))
836 except (OptionError, AttributeError):
837 return default
838
839
841 """class for simple configurations which don't need the
842 manager / providers model and prefer delegation to inheritance
843
844 configuration values are accessible through a dict like interface
845 """
846
847 - def __init__(self, config_file=None, options=None, name=None,
848 usage=None, doc=None, version=None):
856
857
859 """Adapt an option manager to behave like a
860 `logilab.common.configuration.Configuration` instance
861 """
863 self.config = provider
864
866 return getattr(self.config, key)
867
869 provider = self.config._all_options[key]
870 try:
871 return getattr(provider.config, provider.option_name(key))
872 except AttributeError:
873 raise KeyError(key)
874
877
878 - def get(self, key, default=None):
879 provider = self.config._all_options[key]
880 try:
881 return getattr(provider.config, provider.option_name(key))
882 except AttributeError:
883 return default
884
885
887 """initialize newconfig from a deprecated configuration file
888
889 possible changes:
890 * ('renamed', oldname, newname)
891 * ('moved', option, oldgroup, newgroup)
892 * ('typechanged', option, oldtype, newvalue)
893 """
894
895 changesindex = {}
896 for action in changes:
897 if action[0] == 'moved':
898 option, oldgroup, newgroup = action[1:]
899 changesindex.setdefault(option, []).append((action[0], oldgroup, newgroup))
900 continue
901 if action[0] == 'renamed':
902 oldname, newname = action[1:]
903 changesindex.setdefault(newname, []).append((action[0], oldname))
904 continue
905 if action[0] == 'typechanged':
906 option, oldtype, newvalue = action[1:]
907 changesindex.setdefault(option, []).append((action[0], oldtype, newvalue))
908 continue
909 if action[1] in ('added', 'removed'):
910 continue
911 raise Exception('unknown change %s' % action[0])
912
913 options = []
914 for optname, optdef in newconfig.options:
915 for action in changesindex.pop(optname, ()):
916 if action[0] == 'moved':
917 oldgroup, newgroup = action[1:]
918 optdef = optdef.copy()
919 optdef['group'] = oldgroup
920 elif action[0] == 'renamed':
921 optname = action[1]
922 elif action[0] == 'typechanged':
923 oldtype = action[1]
924 optdef = optdef.copy()
925 optdef['type'] = oldtype
926 options.append((optname, optdef))
927 if changesindex:
928 raise Exception('unapplied changes: %s' % changesindex)
929 oldconfig = Configuration(options=options, name=newconfig.name)
930
931 oldconfig.load_file_configuration(configfile)
932
933 changes.reverse()
934 done = set()
935 for action in changes:
936 if action[0] == 'renamed':
937 oldname, newname = action[1:]
938 newconfig[newname] = oldconfig[oldname]
939 done.add(newname)
940 elif action[0] == 'typechanged':
941 optname, oldtype, newvalue = action[1:]
942 newconfig[optname] = newvalue
943 done.add(optname)
944 for optname, optdef in newconfig.options:
945 if not optname in done:
946 newconfig.set_option(optname, oldconfig[optname], opt_dict=optdef)
947
948
950 """preprocess options to remove duplicate"""
951 alloptions = {}
952 options = list(options)
953 for i in range(len(options)-1, -1, -1):
954 optname, optdict = options[i]
955 if optname in alloptions:
956 options.pop(i)
957 alloptions[optname].update(optdict)
958 else:
959 alloptions[optname] = optdict
960 return tuple(options)
961