1 # vim: set encoding=utf-8 et sw=4 sts=4 :
5 # * add 'append' mode to ListOption (like ConfigOption).
7 # * update/write documentation
14 from formencode import Schema, ForEach, Invalid, validators
15 from ConfigParser import SafeConfigParser, InterpolationMissingOptionError, \
16 InterpolationSyntaxError, InterpolationDepthError, \
17 MissingSectionHeaderError, ParsingError
18 import logging ; log = logging.getLogger('pymin.config')
21 __all__ = ('ConfigError', 'MissingSectionHeaderError', 'ParsingError',
22 'Options', 'OptionGroup', 'Option',
23 'VOID', 'load_config', 'config', 'options')
26 # regular expression to check if a name is a valid python identifier
27 identifier_re = re.compile(r'^[a-zA-Z_]\w*$')
30 class ConfigError(RuntimeError):
32 Raised when the problem is due to an user error.
41 "For boolean expression evaluation"
48 Represent a set of options a program can have.
50 Both command-line and configuration file options are handled.
52 .. attr:: default_group
54 The name of the default :class:`OptionGroup`. Options that
55 don't belong to any group are looked in the section with this
56 name in the config file.
58 .. attr:: default_group
60 Default :class:`OptionGroup` description.
64 A list of :class:`Option` or :class:`OptionGroup`
65 instances. Groups are translated to a section when parsing the
66 config file, and to prefixes in the long options when parsing
70 def __init__(self, schemacls=None):
71 self.default_group = ''
72 self.default_group_desc = ''
76 self.schemacls = schemacls
77 self.schema = None # filled in process()
79 def add(self, options):
80 log.debug('Options.add(%r)', options)
81 if isinstance(options, (list, tuple)):
82 self.options.extend(options)
84 self.options.append(options)
86 def add_group(self, *args, **kwargs):
87 g = OptionGroup(*args, **kwargs)
88 log.debug('Options.add_group(%r)', g)
91 def get_group(self, group_name):
92 for g in self.options:
93 if isinstance(g, OptionGroup) and g.name == group_name:
96 def init(self, default_group=None, default_group_desc=None,
99 Initialize the class. Since the class is instantiated by the
100 config framework, you should use this method to set it's values.
102 Arguments are directly mapped to the class attributes, with
103 the particularity of options being appended to existing options
104 (instead of replaced).
106 log.debug('Options.init(default_group=%r, default_group_desc=%r, '
107 'options=%r)', default_group, default_group_desc, options)
108 if not identifier_re.match(default_group):
109 raise ValueError("Invalid default group name '%s' (group names "
110 "must be valid python identifiers" % name)
111 if default_group is not None:
112 self.default_group = default_group
113 if default_group_desc is not None:
114 self.default_group_desc = default_group_desc
115 if options is not None:
116 self.options.extend(options)
118 def process(self, parser):
120 Process the command-line options.
122 ``parser`` should be a :class:`optparse.OptionParser` instance.
124 log.debug('Options.process()')
125 self.schema = self.schemacls()
127 for o in self.options:
128 o.process(parser, self.schema, config_opts, self.default_group)
131 def values(self, confparser, parseropts, ignore_errors):
133 Post process the command-line options.
135 ``config`` should be an object where options are stored. If
136 ``prefix`` is specified, the prefix will be used to look for
139 log.debug('Options.values()')
140 # first populate the confparser with the values from the command-line
141 # options to make the variable interpolation works as expected.
142 for o in self.options:
143 o.set_value(confparser, parseropts, self.default_group)
144 # then, get the actual values
146 for o in self.options:
147 v[o.name] = o.value(confparser, parseropts, self.default_group,
149 log.debug('Options.values() -> %r', v)
154 log.debug('Options.defaults()')
156 for o in self.options:
157 defaults.update(o.defaults)
158 log.debug('Options.defaults() -> %r', defaults)
164 def __init__(self, name, description=None, options=None):
165 log.debug('OptionGroup(name=%r, description=%r, options=%r)', name,
166 description, options)
167 if not identifier_re.match(name):
168 raise ValueError("Invalid group name '%s' (group names must be "
169 "valid python identifiers" % name)
171 self.oname = name.replace('_', '-')
172 self.description = description
174 if options is not None:
177 def add(self, options):
178 log.debug('OptionGroup.add(%r)', options)
179 if not isinstance(options, (list, tuple)):
182 if isinstance(o, OptionGroup):
183 raise ConfigError("Groups can't be nested (group '%s' found "
184 "inside group '%s')" % (o.name, self.name))
185 self.options.extend(options)
187 def process(self, parser, schema, config_opts, default_group):
189 Process the command-line options.
191 ``parser`` should be a :class:`optparse.OptionParser` instance.
193 subschema = schema.__class__()
194 group = optparse.OptionGroup(parser, self.description)
195 for o in self.options:
197 raise ValueError("Grouped options can't have short name "
198 "(option '%s' in grupo '%s' have this short "
199 "names: %s)" % (o.name, self.name,
200 ','.join('-' + o for o in o.short)))
201 o.process(group, subschema, config_opts, self)
202 log.debug('Adding group %s to optparse')
203 parser.add_option_group(group)
204 schema.add_field(self.name, subschema)
206 def value(self, confparser, parseropts, default_group, ignore_errors):
210 for o in self.options:
211 v[o.name] = o.value(confparser, parseropts, self, ignore_errors)
214 def set_value(self, confparser, parseropts, default_group):
218 for o in self.options:
219 o.set_value(confparser, parseropts, self)
225 for o in self.options:
226 defaults.update(o.defaults)
227 defaults = { self.oname: defaults }
231 return 'OptionGroup<%s>%r' % (self.name, self.options)
240 The name of the option. The config object will have an attribute
241 with this name. When parsing configuration and command-line
242 options, the ``_`` in this name are replace with ``-``.
246 See :mod:`formencode.validators` for available validators.
250 Short aliases (only for command-line options).
262 Help message for the option.
266 Name of the option argument used in the help message.
270 def __init__(self, name, validator=None, short=(), long=(), default=VOID,
271 help=VOID, metavar=VOID):
272 log.debug('Option(name=%r, validator=%r, short=%r, long=%r, '
273 'default=%r, help=%r, metavar=%r)', name, validator, short,
274 long, default, help, metavar)
275 if not identifier_re.match(name):
276 raise ValueError("Invalid option name '%s' (option names must be "
277 "valid python identifiers" % name)
279 self.oname = name.replace('_', '-')
280 self.validator = validator
281 if isinstance(short, basestring):
283 self.short = list(short)
284 if isinstance(long, basestring):
286 self.long = [self.oname] + list(long)
287 self.default = default
289 self.metavar = metavar
291 def process(self, parser, schema, config_opts, group):
293 Process the command-line options.
295 ``parser`` should be a :class:`optparse.OptionParser` instance. If
296 ``prefix`` is specified, all long command-line options are
297 prefixed with that. For example: ``some_option`` will become
298 ``--prefix-some-option``.
300 if isinstance(group, basestring):
302 parser.add_option(type='string', dest=self.dest(group), default=VOID,
303 metavar=self.metavar, help=self.help,
304 *self.optparser_args(group))
305 log.debug('Option<%s>.process() -> add_option(%s, type=%r, dest=%r, '
306 'default=%r, metavar=%r, help=%r)', self.name,
307 ', '.join(repr(i) for i in self.optparser_args(group)),
308 'string', self.dest(group), VOID, self.metavar, self.help)
309 schema.add_field(self.name, self.validator)
311 def value(self, confparser, parseropts, group, ignore_errors):
313 Post process the command-line options.
315 ``config`` should be an object where options are stored. If
316 ``prefix`` is specified, the prefix will be used to look for
320 if isinstance(group, OptionGroup):
321 section = group.oname
322 if (confparser.has_section(section)
323 and confparser.has_option(section, self.oname)):
324 return confparser.get(section, self.oname)
325 if self.default is VOID:
328 raise ConfigError('mandatory option "%s" not present' % self.name)
331 def set_value(self, confparser, parseropts, group):
332 val = getattr(parseropts, self.dest(group))
335 if isinstance(group, OptionGroup):
336 section = group.oname
337 if not confparser.has_section(section):
338 confparser.add_section(section)
339 confparser.set(section, self.oname, val)
341 def optparser_args(self, group=None):
343 args.extend('-' + s for s in self.short)
346 prefix = group.oname + '-'
347 args.extend('--' + prefix + l for l in self.long)
350 def dest(self, group=None):
352 if group and not isinstance(group, basestring):
353 prefix = group.name + '.'
354 return prefix + self.name
359 if self.default is not VOID:
360 default[self.oname] = str(self.default)
364 return 'Option<%s>' % (self.name)
367 class ListOption(Option):
369 def process(self, parser, schema, config_opts, group):
372 if isinstance(group, basestring):
374 parser.add_option(type='string', dest=self.dest(group), default=[],
375 metavar=self.metavar, help=self.help,
376 action='append', *self.optparser_args(group))
377 log.debug('Option<%s>.process() -> add_option(%s, type=%r, dest=%r, '
378 'action=%r, default=%r, metavar=%r, help=%r)', self.name,
379 ', '.join(repr(i) for i in self.optparser_args(group)),
380 'string', self.dest(group), 'append', [], self.metavar,
382 schema.add_field(self.name, ForEach(self.validator, if_empty=[]))
384 def value(self, confparser, parseropts, group, ignore_errors):
387 value = Option.value(self, confparser, parseropts, group, ignore_errors)
388 if not isinstance(value, (list, tuple)):
389 value = shlex.split(value)
392 def set_value(self, confparser, parseropts, group):
393 val = getattr(parseropts, self.dest(group))
395 val = ' '.join([repr(i) for i in val])
396 setattr(parseropts, self.dest(group), val)
397 Option.set_value(self, confparser, parseropts, group)
400 return 'ListOption<%s>' % (self.name)
403 class ConfigOption(ListOption):
405 def __init__(self, name, short=(), long=(), default=VOID, help=VOID,
406 metavar=VOID, override=False):
407 ListOption.__init__(self, name, validators.String, short, long, default,
409 self.override = override
411 def process(self, parser, schema, config_opts, group):
412 ListOption.process(self, parser, schema, config_opts, group)
413 config_opts.append(self)
415 def value(self, confparser, parseropts, group, ignore_errors):
418 def set_value(self, confparser, parseropts, group):
424 Dummy object that stores all the configuration data.
428 return 'Config(%r)' % self.__dict__
438 class LazyOptionParser(optparse.OptionParser):
440 ignore_errors = False
444 def exit(self, status=0, msg=None):
445 if self.ignore_errors:
447 optparse.OptionParser.exit(self, status, msg)
449 def error(self, msg):
450 if self.ignore_errors:
452 self.print_usage(sys.stderr)
453 self.exit(self.exit_status, "%s: error: %s\n"
454 % (self.get_prog_name(), msg))
456 def print_help(self, file=None):
457 if self.ignore_errors:
459 optparse.OptionParser.print_help(self, file)
461 def _process_short_opts(self, rargs, values):
463 return optparse.OptionParser._process_short_opts(self, rargs,
466 if not self.ignore_errors:
469 def _process_long_opt(self, rargs, values):
471 return optparse.OptionParser._process_long_opt(self, rargs, values)
473 if not self.ignore_errors:
476 def _process_short_opts(self, rargs, values):
478 return optparse.OptionParser._process_short_opts(self, rargs, values)
480 if not self.ignore_errors:
484 def load_options(version=None, description=None, ignore_errors=False):
485 # load command-line options
486 optparser = LazyOptionParser(version=version, description=description)
487 optparser.ignore_errors = ignore_errors
488 config_opts = options.process(optparser)
489 (opts, args) = optparser.parse_args()
490 log.debug('load_options() -> %r %r', opts, args)
493 return (opts, config_opts, args)
496 def make_config(values):
497 log.debug('make_config()')
499 for (name, value) in values.items():
500 if isinstance(value, dict):
501 log.debug('make_config() -> processing group %s: %r', name, value)
502 setattr(config, name, make_config(value))
504 log.debug('make_config() -> processing value %s: %r', name, value)
505 setattr(config, name, value)
506 log.debug('make_config() -> config = %r', config)
510 def load_conf(config_file_paths, version, description, defaults, ignore_errors):
511 log.debug('load_conf(%r, version=%r, description=%r, ignore_errors=%r)',
512 config_file_paths, version, description, ignore_errors)
515 # load command-line options
516 (opts, config_opts, args) = load_options(version, description,
517 ignore_errors=ignore_errors)
519 # process config file options to see what config files to load
520 for opt in config_opts:
521 files = getattr(opts, opt.name)
522 log.debug('load_conf() -> processing configuration file option '
523 '"%s": %r', opt.name, files)
525 log.debug('load_conf() -> option not set! looking for the next')
528 log.debug('load_conf() -> overriding default config files')
529 config_file_paths = files
531 log.debug('load_conf() -> appending to default config files')
532 config_file_paths.extend(files)
534 confparser = SafeConfigParser(defaults)
535 readed = confparser.read(config_file_paths)
536 log.debug('load_conf() -> readed config files: %r', readed)
539 log.debug('load_conf() -> sections: %r', confparser.sections())
540 log.debug('load_conf() -> readed values from config files: %r',
541 [confparser.items(s) for s in confparser.sections()])
542 values = options.values(confparser, opts, ignore_errors)
543 except InterpolationMissingOptionError, e:
544 raise ConfigError('bad value substitution for option "%s" in '
545 'section [%s] (references an unexistent option '
546 '"%s")' % (e.option, e.section, e.reference))
547 except InterpolationDepthError, e:
548 raise ConfigError('value interpolation too deeply recursive in '
549 'option "%s", section [%s]' % (e.option, e.section))
550 except InterpolationSyntaxError, e:
551 raise ConfigError('bad syntax for interpolation variable in option '
552 '"%s", section [%s]' % (e.option, e.section))
554 values = options.schema.to_python(values)
555 log.debug('load_conf() -> validated values: %r', values)
557 config = make_config(values)
559 # TODO options.check_orphans(confparser, config)
561 return (config, args)
564 def load_config(config_file_paths, version=None, description=None,
565 add_plugin_opts=None, defaults=None):
566 "callback signature: add_plugin_opts(config, args)"
567 log.debug('load_config(%r, version=%r, description=%r, add_plugin_opts=%r)',
568 config_file_paths, version, description, add_plugin_opts)
572 (config, args) = load_conf(config_file_paths, version, description,
573 defaults, add_plugin_opts is not None)
574 while add_plugin_opts:
575 log.debug('load_config() -> calling %r', add_plugin_opts)
576 add_plugin_opts = add_plugin_opts(config, args)
577 log.debug('load_config() -> got config=%r / args=%r', config, args)
578 (config, args) = load_conf(config_file_paths, version, description,
579 defaults, add_plugin_opts is not None)
581 return (config, args)
584 if __name__ == '__main__':
588 from formencode import validators as V
591 level = logging.DEBUG,
592 format = '%(levelname)-8s %(message)s',
595 def print_config(config, prefix=''):
596 for attr in dir(config):
597 if not attr.startswith('__'):
598 val = getattr(config, attr)
599 if isinstance(val, Config):
600 print prefix, 'Group %s:' % attr
601 print_config(val, '\t')
603 print prefix, attr, '=', val
605 options.init('pymind', 'Default group description', [
606 Option('dry_run', V.StringBool, 'd', default=True,
607 help="pretend, don't execute commands"),
608 Option('bind_addr', V.CIDR, 'a', default='127.0.0.1', metavar='ADDR',
609 help='IP address to bind to'),
610 Option('bind_port', V.Int, 'p', default=9999, metavar='PORT',
611 help="port to bind to"),
612 Option('mandatory', V.Int, 'm', metavar='N',
613 help="mandatory parameter"),
614 ConfigOption('config_file', 'c', metavar="FILE",
615 help="configuration file"),
616 ConfigOption('override_config_file', 'o', metavar="FILE",
617 help="configuration file", override=True),
618 ListOption('plugins', V.String, 's', metavar="SERV", default=[],
619 help="plugins pymin should use"),
620 OptionGroup('test_grp', 'A test group', [
621 Option('test', V.StringBool, default=False,
622 help="test group option"),
623 Option('oneof', V.OneOf(['tcp', 'udp']), default='tcp',
624 help="test option for OneOf validator"),
625 ListOption('list', V.StringBool, default=[True, True, False],
626 help="test ListOption"),
630 def add_more_plugin_opts(config, args):
631 print 'add_more_plugin_opts'
632 print '---------------'
634 print '---------------'
636 g = options.get_group('plugin')
637 g.add(ListOption('other', V.Int, default=[1,2], metavar='OTHER',
638 help="a list of numbers"))
640 def add_plugin_opts(config, args):
641 print 'add_plugin_opts'
642 print '---------------'
644 print '---------------'
646 if 'plugin' in config.plugins:
648 OptionGroup('plugin', 'some plug-in options', [
649 Option('active', V.StringBool, default=True, metavar='ACT',
650 help="if ACT is true, the plug-in is active"),
653 return add_more_plugin_opts
656 f = tempfile.NamedTemporaryFile()
662 log-config-file: /etc/pymin/log.conf
674 (c, a) = load_config([f.name], '%prog 0.1', 'this is a program test',
684 except ConfigError, e:
686 except MissingSectionHeaderError, e:
687 print "%s:%s: missing section header near: %s" \
688 % (e.filename, e.lineno, e.line)
689 except ParsingError, e:
690 for (lineno, line) in e.errors:
691 print "%s:%s: invalid syntax near: %s" % (e.filename, lineno, line)