Package logilab-common-0 ::
Package 36 ::
Package 1 ::
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
116
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
124
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
135
137 """validate and return a converted value for option of type 'csv'
138 """
139 return check_csv(None, name, value)
140
142 """validate and return a converted value for option of type 'yn'
143 """
144 return check_yn(None, name, value)
145
147 """validate and return a converted value for option of type 'named'
148 """
149 return check_named(None, name, value)
150
152 """validate and return a filepath for option of type 'file'"""
153 return check_file(None, name, value)
154
156 """validate and return a valid color for option of type 'color'"""
157 return check_color(None, name, value)
158
160 """validate and return a string for option of type 'password'"""
161 return check_password(None, name, value)
162
164 """validate and return a mx DateTime object for option of type 'date'"""
165 return check_date(None, name, value)
166
167
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 }
184
186 if opttype not in VALIDATORS:
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))
198
199
200
209
213
225 return input_validator
226
227 INPUT_FUNCTIONS = {
228 'string': input_string,
229 'password': input_password,
230 }
231
232 for opttype in VALIDATORS.keys():
233 INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype))
234
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))
255
256
257 -def convert(value, opt_dict, name=''):
258 """return a validated value for an option according to its type
259
260 optional argument name is only used for error message formatting
261 """
262 try:
263 _type = opt_dict['type']
264 except KeyError:
265
266 return value
267 return _call_validator(_type, opt_dict, name, value)
268
273
288
306
307 format_section = ini_format_section
308
325
326
328 """MixIn to handle a configuration from both a configuration file and
329 command line options
330 """
331
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
336 self.options_providers = []
337
338 self._all_options = {}
339 self._short_options = {}
340 self._nocallback_options = {}
341
342 self.quiet = quiet
343
345
346 self._config_parser = ConfigParser()
347
348 self._optik_parser = OptionParser(usage=usage, version=version)
349 self._optik_parser.options_manager = self
350
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 'group' not in option[1]]
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
371 self._all_options[opt_name] = provider
372 for gname, gdoc in groups:
373 goptions = [option for option in provider.options
374 if option[1].get('group') == gname]
375 self.add_option_group(gname, gdoc, goptions, provider)
376
378 """add an option group including the listed options
379 """
380
381 if group_name != "DEFAULT":
382 self._config_parser.add_section(group_name)
383
384 if options:
385 group = OptionGroup(self._optik_parser,
386 title=group_name.capitalize())
387 self._optik_parser.add_option_group(group)
388
389 for opt_name, opt_dict in options:
390 args, opt_dict = self.optik_option(provider, opt_name, opt_dict)
391 group.add_option(*args, **opt_dict)
392 self._all_options[opt_name] = provider
393
395 """get our personal option definition and return a suitable form for
396 use with optik/optparse
397 """
398 opt_dict = copy(opt_dict)
399 if 'action' in opt_dict:
400 self._nocallback_options[provider] = opt_name
401 else:
402 opt_dict['action'] = 'callback'
403 opt_dict['callback'] = self.cb_set_provider_option
404
405
406 if 'default' in opt_dict:
407 if OPTPARSE_FORMAT_DEFAULT and 'help' in opt_dict:
408 opt_dict['help'] += ' [current: %default]'
409 del opt_dict['default']
410 args = ['--' + opt_name]
411 if 'short' in opt_dict:
412 self._short_options[opt_dict['short']] = opt_name
413 args.append('-' + opt_dict['short'])
414 del opt_dict['short']
415 available_keys = set(self._optik_parser.option_class.ATTRS)
416
417 for key in opt_dict.keys():
418 if not key in available_keys:
419 opt_dict.pop(key)
420 return args, opt_dict
421
423 """optik callback for option setting"""
424 if opt_name.startswith('--'):
425
426 opt_name = opt_name[2:]
427 else:
428
429 opt_name = self._short_options[opt_name[1:]]
430
431 if value is None:
432 value = 1
433 self.global_set_option(opt_name, value)
434
436 """set option on the correct option provider"""
437 self._all_options[opt_name].set_option(opt_name, value)
438
440 """write a configuration file according to the current configuration
441 into the given stream or stdout
442 """
443 stream = stream or sys.stdout
444 printed = False
445 for provider in self.options_providers:
446 default_options = []
447 sections = {}
448 for section, options in provider.options_by_section():
449 if section in skipsections:
450 continue
451 options = [(n, d, v) for (n, d, v) in options
452 if d.get('type') is not None]
453 if not options:
454 continue
455 if section is None:
456 section = provider.name
457 doc = provider.__doc__
458 else:
459 doc = None
460 if printed:
461 print >> stream, '\n'
462 format_section(stream, section.upper(), options, doc)
463 printed = True
464
465 - def generate_manpage(self, pkginfo, section=1, stream=None):
466 """write a man page for the current configuration into the given
467 stream or stdout
468 """
469 self._monkeypatch_expand_default()
470 try:
471 generate_manpage(self._optik_parser, pkginfo,
472 section, stream=stream or sys.stdout)
473 finally:
474 self._unmonkeypatch_expand_default()
475
476
477
479 """initialize configuration using default values"""
480 for provider in self.options_providers:
481 provider.load_defaults()
482
487
489 """read the configuration file but do not load it (ie dispatching
490 values to each options provider)
491 """
492 if config_file is None:
493 config_file = self.config_file
494 if config_file is not None:
495 config_file = expanduser(config_file)
496 if config_file and exists(config_file):
497 self._config_parser.read([config_file])
498 elif not self.quiet:
499 msg = 'No config file found, using default configuration'
500 print >> sys.stderr, msg
501 return
502
517
519 """dispatch values previously read from a configuration file to each
520 options provider)
521 """
522 parser = self._config_parser
523 for provider in self.options_providers:
524 for section, option, optdict in provider.all_options():
525 try:
526 value = parser.get(section, option)
527 provider.set_option(option, value, opt_dict=optdict)
528 except (NoSectionError, NoOptionError), ex:
529 continue
530
532 """override configuration according to given parameters
533 """
534 for opt_name, opt_value in kwargs.items():
535 opt_name = opt_name.replace('_', '-')
536 provider = self._all_options[opt_name]
537 provider.set_option(opt_name, opt_value)
538
540 """override configuration according to command line parameters
541
542 return additional arguments
543 """
544 self._monkeypatch_expand_default()
545 try:
546 if args is None:
547 args = sys.argv[1:]
548 else:
549 args = list(args)
550 (options, args) = self._optik_parser.parse_args(args=args)
551 for provider in self._nocallback_options.keys():
552 config = provider.config
553 for attr in config.__dict__.keys():
554 value = getattr(options, attr, None)
555 if value is None:
556 continue
557 setattr(config, attr, value)
558 return args
559 finally:
560 self._unmonkeypatch_expand_default()
561
562
563
564
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)
571
573
574 try:
575 self.__expand_default_backup = HelpFormatter.expand_default
576 HelpFormatter.expand_default = expand_default
577 except AttributeError:
578
579 pass
581
582 if hasattr(HelpFormatter, 'expand_default'):
583
584 HelpFormatter.expand_default = self.__expand_default_backup
585
587 """return the usage string for available options """
588 self._monkeypatch_expand_default()
589 try:
590 return self._optik_parser.format_help()
591 finally:
592 self._unmonkeypatch_expand_default()
593
594
596 """used to ease late binding of default method (so you can define options
597 on the class using default methods on the configuration instance)
598 """
600 self.method = methname
601 self._inst = None
602
603 - def bind(self, instance):
604 """bind the method to its instance"""
605 if self._inst is None:
606 self._inst = instance
607
609 assert self._inst, 'unbound method'
610 return getattr(self._inst, self.method)()
611
612
614 """Mixin to provide options to an OptionsManager"""
615
616
617 priority = -1
618 name = 'default'
619 options = ()
620
622 self.config = Values()
623 for option in self.options:
624 try:
625 option, optdict = option
626 except ValueError:
627 raise Exception('Bad option: %r' % option)
628 if isinstance(optdict.get('default'), Method):
629 optdict['default'].bind(self)
630 self.load_defaults()
631
633 """initialize the provider using default values"""
634 for opt_name, opt_dict in self.options:
635 action = opt_dict.get('action')
636 if action != 'callback':
637
638 default = self.option_default(opt_name, opt_dict)
639 if default is REQUIRED:
640 continue
641 self.set_option(opt_name, default, action, opt_dict)
642
644 """return the default value for an option"""
645 if opt_dict is None:
646 opt_dict = self.get_option_def(opt_name)
647 default = opt_dict.get('default')
648 if callable(default):
649 default = default()
650 return default
651
653 """get the config attribute corresponding to opt_name
654 """
655 if opt_dict is None:
656 opt_dict = self.get_option_def(opt_name)
657 return opt_dict.get('dest', opt_name.replace('-', '_'))
658
660 """get the current value for the given option"""
661 return getattr(self.config, self.option_name(opt_name), None)
662
663 - def set_option(self, opt_name, value, action=None, opt_dict=None):
664 """method called to set an option (registered in the options list)
665 """
666
667 if opt_dict is None:
668 opt_dict = self.get_option_def(opt_name)
669 if value is not None:
670 value = convert(value, opt_dict, opt_name)
671 if action is None:
672 action = opt_dict.get('action', 'store')
673 if opt_dict.get('type') == 'named':
674 optname = self.option_name(opt_name, opt_dict)
675 currentvalue = getattr(self.config, optname, None)
676 if currentvalue:
677 currentvalue.update(value)
678 value = currentvalue
679 if action == 'store':
680 setattr(self.config, self.option_name(opt_name, opt_dict), value)
681 elif action in ('store_true', 'count'):
682 setattr(self.config, self.option_name(opt_name, opt_dict), 0)
683 elif action == 'store_false':
684 setattr(self.config, self.option_name(opt_name, opt_dict), 1)
685 elif action == 'append':
686 opt_name = self.option_name(opt_name, opt_dict)
687 _list = getattr(self.config, opt_name, None)
688 if _list is None:
689 if type(value) in (type(()), type([])):
690 _list = value
691 elif value is not None:
692 _list = []
693 _list.append(value)
694 setattr(self.config, opt_name, _list)
695 elif type(_list) is type(()):
696 setattr(self.config, opt_name, _list + (value,))
697 else:
698 _list.append(value)
699 else:
700 raise UnsupportedAction(action)
701
723
725 """return the dictionary defining an option given it's name"""
726 assert self.options
727 for opt in self.options:
728 if opt[0] == opt_name:
729 return opt[1]
730 raise OptionError('no such option in section %r' % self.name, opt_name)
731
732
734 """return an iterator on available options for this provider
735 option are actually described by a 3-uple:
736 (section, option name, option dictionary)
737 """
738 for section, options in self.options_by_section():
739 if section is None:
740 section = self.name.upper()
741 for option, optiondict, value in options:
742 yield section, option, optiondict
743
745 """return an iterator on options grouped by section
746
747 (section, [list of (optname, optdict, optvalue)])
748 """
749 sections = {}
750 for optname, optdict in self.options:
751 sections.setdefault(optdict.get('group'), []).append(
752 (optname, optdict, self.option_value(optname)))
753 if None in sections:
754 yield None, sections.pop(None)
755 for section, options in sections.items():
756 yield section.upper(), options
757
758
760 """basic mixin for simple configurations which don't need the
761 manager / providers model
762 """
779
788
791
793 try:
794 return getattr(self.config, self.option_name(key))
795 except (OptionValueError, AttributeError):
796 raise KeyError(key)
797
800
801 - def get(self, key, default=None):
802 try:
803 return getattr(self.config, self.option_name(key))
804 except (OptionError, AttributeError):
805 return default
806
807
809 """class for simple configurations which don't need the
810 manager / providers model and prefer delegation to inheritance
811
812 configuration values are accessible through a dict like interface
813 """
814
815 - def __init__(self, config_file=None, options=None, name=None,
816 usage=None, doc=None, version=None):
824
825
827 """Adapt an option manager to behave like a
828 `logilab.common.configuration.Configuration` instance
829 """
831 self.config = provider
832
834 return getattr(self.config, key)
835
837 provider = self.config._all_options[key]
838 try:
839 return getattr(provider.config, provider.option_name(key))
840 except AttributeError:
841 raise KeyError(key)
842
845
846 - def get(self, key, default=None):
847 provider = self.config._all_options[key]
848 try:
849 return getattr(provider.config, provider.option_name(key))
850 except AttributeError:
851 return default
852
853
855 """initialize newconfig from a deprecated configuration file
856
857 possible changes:
858 * ('renamed', oldname, newname)
859 * ('moved', option, oldgroup, newgroup)
860 * ('typechanged', option, oldtype, newvalue)
861 """
862
863 changesindex = {}
864 for action in changes:
865 if action[0] == 'moved':
866 option, oldgroup, newgroup = action[1:]
867 changesindex.setdefault(option, []).append((action[0], oldgroup, newgroup))
868 continue
869 if action[0] == 'renamed':
870 oldname, newname = action[1:]
871 changesindex.setdefault(newname, []).append((action[0], oldname))
872 continue
873 if action[0] == 'typechanged':
874 option, oldtype, newvalue = action[1:]
875 changesindex.setdefault(option, []).append((action[0], oldtype, newvalue))
876 continue
877 if action[1] in ('added', 'removed'):
878 continue
879 raise Exception('unknown change %s' % action[0])
880
881 options = []
882 for optname, optdef in newconfig.options:
883 for action in changesindex.pop(optname, ()):
884 if action[0] == 'moved':
885 oldgroup, newgroup = action[1:]
886 optdef = optdef.copy()
887 optdef['group'] = oldgroup
888 elif action[0] == 'renamed':
889 optname = action[1]
890 elif action[0] == 'typechanged':
891 oldtype = action[1]
892 optdef = optdef.copy()
893 optdef['type'] = oldtype
894 options.append((optname, optdef))
895 if changesindex:
896 raise Exception('unapplied changes: %s' % changesindex)
897 oldconfig = Configuration(options=options, name=newconfig.name)
898
899 oldconfig.load_file_configuration(configfile)
900
901 changes.reverse()
902 done = set()
903 for action in changes:
904 if action[0] == 'renamed':
905 oldname, newname = action[1:]
906 newconfig[newname] = oldconfig[oldname]
907 done.add(newname)
908 elif action[0] == 'typechanged':
909 optname, oldtype, newvalue = action[1:]
910 newconfig[optname] = newvalue
911 done.add(optname)
912 for optname, optdef in newconfig.options:
913 if not optname in done:
914 newconfig.set_option(optname, oldconfig[optname], opt_dict=optdef)
915
916
918 """preprocess options to remove duplicate"""
919 alloptions = {}
920 options = list(options)
921 for i in range(len(options)-1, -1, -1):
922 optname, optdict = options[i]
923 if optname in alloptions:
924 options.pop(i)
925 alloptions[optname].update(optdict)
926 else:
927 alloptions[optname] = optdict
928 return tuple(options)
929