Index: third_party/logilab/common/configuration.py |
=================================================================== |
--- third_party/logilab/common/configuration.py (revision 292986) |
+++ third_party/logilab/common/configuration.py (working copy) |
@@ -1,4 +1,4 @@ |
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
# |
# This file is part of logilab-common. |
@@ -96,8 +96,19 @@ |
multiple=4,5,6 |
number=3 |
- >>> |
+ |
+ Note : starting with Python 2.7 ConfigParser is able to take into |
+ account the order of occurrences of the options into a file (by |
+ using an OrderedDict). If you have two options changing some common |
+ state, like a 'disable-all-stuff' and a 'enable-some-stuff-a', their |
+ order of appearance will be significant : the last specified in the |
+ file wins. For earlier version of python and logilab.common newer |
+ than 0.61 the behaviour is unspecified. |
+ |
""" |
+ |
+from __future__ import print_function |
+ |
__docformat__ = "restructuredtext en" |
__all__ = ('OptionsManagerMixIn', 'OptionsProviderMixIn', |
@@ -109,16 +120,17 @@ |
import re |
from os.path import exists, expanduser |
from copy import copy |
-from ConfigParser import ConfigParser, NoOptionError, NoSectionError, \ |
- DuplicateSectionError |
from warnings import warn |
-from logilab.common.compat import callable, raw_input, str_encode as _encode |
+from six import string_types |
+from six.moves import range, configparser as cp, input |
+from logilab.common.compat import str_encode as _encode |
+from logilab.common.deprecation import deprecated |
from logilab.common.textutils import normalize_text, unquote |
-from logilab.common import optik_ext as optparse |
+from logilab.common import optik_ext |
-OptionError = optparse.OptionError |
+OptionError = optik_ext.OptionError |
REQUIRED = [] |
@@ -136,12 +148,15 @@ |
# validation functions ######################################################## |
+# validators will return the validated value or raise optparse.OptionValueError |
+# XXX add to documentation |
+ |
def choice_validator(optdict, name, value): |
"""validate and return a converted value for option of type 'choice' |
""" |
if not value in optdict['choices']: |
msg = "option %s: invalid value: %r, should be in %s" |
- raise optparse.OptionValueError(msg % (name, value, optdict['choices'])) |
+ raise optik_ext.OptionValueError(msg % (name, value, optdict['choices'])) |
return value |
def multiple_choice_validator(optdict, name, value): |
@@ -148,51 +163,51 @@ |
"""validate and return a converted value for option of type 'choice' |
""" |
choices = optdict['choices'] |
- values = optparse.check_csv(None, name, value) |
+ values = optik_ext.check_csv(None, name, value) |
for value in values: |
if not value in choices: |
msg = "option %s: invalid value: %r, should be in %s" |
- raise optparse.OptionValueError(msg % (name, value, choices)) |
+ raise optik_ext.OptionValueError(msg % (name, value, choices)) |
return values |
def csv_validator(optdict, name, value): |
"""validate and return a converted value for option of type 'csv' |
""" |
- return optparse.check_csv(None, name, value) |
+ return optik_ext.check_csv(None, name, value) |
def yn_validator(optdict, name, value): |
"""validate and return a converted value for option of type 'yn' |
""" |
- return optparse.check_yn(None, name, value) |
+ return optik_ext.check_yn(None, name, value) |
def named_validator(optdict, name, value): |
"""validate and return a converted value for option of type 'named' |
""" |
- return optparse.check_named(None, name, value) |
+ return optik_ext.check_named(None, name, value) |
def file_validator(optdict, name, value): |
"""validate and return a filepath for option of type 'file'""" |
- return optparse.check_file(None, name, value) |
+ return optik_ext.check_file(None, name, value) |
def color_validator(optdict, name, value): |
"""validate and return a valid color for option of type 'color'""" |
- return optparse.check_color(None, name, value) |
+ return optik_ext.check_color(None, name, value) |
def password_validator(optdict, name, value): |
"""validate and return a string for option of type 'password'""" |
- return optparse.check_password(None, name, value) |
+ return optik_ext.check_password(None, name, value) |
def date_validator(optdict, name, value): |
"""validate and return a mx DateTime object for option of type 'date'""" |
- return optparse.check_date(None, name, value) |
+ return optik_ext.check_date(None, name, value) |
def time_validator(optdict, name, value): |
"""validate and return a time object for option of type 'time'""" |
- return optparse.check_time(None, name, value) |
+ return optik_ext.check_time(None, name, value) |
def bytes_validator(optdict, name, value): |
"""validate and return an integer for option of type 'bytes'""" |
- return optparse.check_bytes(None, name, value) |
+ return optik_ext.check_bytes(None, name, value) |
VALIDATORS = {'string': unquote, |
@@ -222,14 +237,18 @@ |
except TypeError: |
try: |
return VALIDATORS[opttype](value) |
- except optparse.OptionValueError: |
+ except optik_ext.OptionValueError: |
raise |
except: |
- raise optparse.OptionValueError('%s value (%r) should be of type %s' % |
+ raise optik_ext.OptionValueError('%s value (%r) should be of type %s' % |
(option, value, opttype)) |
# user input functions ######################################################## |
+# user input functions will ask the user for input on stdin then validate |
+# the result and return the validated value or raise optparse.OptionValueError |
+# XXX add to documentation |
+ |
def input_password(optdict, question='password:'): |
from getpass import getpass |
while True: |
@@ -237,23 +256,23 @@ |
value2 = getpass('confirm: ') |
if value == value2: |
return value |
- print 'password mismatch, try again' |
+ print('password mismatch, try again') |
def input_string(optdict, question): |
- value = raw_input(question).strip() |
+ value = input(question).strip() |
return value or None |
def _make_input_function(opttype): |
def input_validator(optdict, question): |
while True: |
- value = raw_input(question) |
+ value = input(question) |
if not value.strip(): |
return None |
try: |
return _call_validator(opttype, optdict, None, value) |
- except optparse.OptionValueError, ex: |
+ except optik_ext.OptionValueError as ex: |
msg = str(ex).split(':', 1)[-1].strip() |
- print 'bad value: %s' % msg |
+ print('bad value: %s' % msg) |
return input_validator |
INPUT_FUNCTIONS = { |
@@ -264,6 +283,8 @@ |
for opttype in VALIDATORS.keys(): |
INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype)) |
+# utility functions ############################################################ |
+ |
def expand_default(self, option): |
"""monkey patch OptionParser.expand_default since we have a particular |
way to handle defaults to avoid overriding values in the configuration |
@@ -278,15 +299,15 @@ |
value = None |
else: |
optdict = provider.get_option_def(optname) |
- optname = provider.option_name(optname, optdict) |
+ optname = provider.option_attrname(optname, optdict) |
value = getattr(provider.config, optname, optdict) |
value = format_option_value(optdict, value) |
- if value is optparse.NO_DEFAULT or not value: |
+ if value is optik_ext.NO_DEFAULT or not value: |
value = self.NO_DEFAULT_VALUE |
return option.help.replace(self.default_tag, str(value)) |
-def convert(value, optdict, name=''): |
+def _validate(value, optdict, name=''): |
"""return a validated value for an option according to its type |
optional argument name is only used for error message formatting |
@@ -297,7 +318,10 @@ |
# FIXME |
return value |
return _call_validator(_type, optdict, name, value) |
+convert = deprecated('[0.60] convert() was renamed _validate()')(_validate) |
+# format and output functions ################################################## |
+ |
def comment(string): |
"""return string as a comment""" |
lines = [line.strip() for line in string.splitlines()] |
@@ -346,7 +370,7 @@ |
value = value.pattern |
elif optdict.get('type') == 'yn': |
value = value and 'yes' or 'no' |
- elif isinstance(value, (str, unicode)) and value.isspace(): |
+ elif isinstance(value, string_types) and value.isspace(): |
value = "'%s'" % value |
elif optdict.get('type') == 'time' and isinstance(value, (float, int, long)): |
value = format_time(value) |
@@ -358,8 +382,8 @@ |
"""format an options section using the INI format""" |
encoding = _get_encoding(encoding, stream) |
if doc: |
- print >> stream, _encode(comment(doc), encoding) |
- print >> stream, '[%s]' % section |
+ print(_encode(comment(doc), encoding), file=stream) |
+ print('[%s]' % section, file=stream) |
ini_format(stream, options, encoding) |
def ini_format(stream, options, encoding): |
@@ -369,20 +393,20 @@ |
help = optdict.get('help') |
if help: |
help = normalize_text(help, line_len=79, indent='# ') |
- print >> stream |
- print >> stream, _encode(help, encoding) |
+ print(file=stream) |
+ print(_encode(help, encoding), file=stream) |
else: |
- print >> stream |
+ print(file=stream) |
if value is None: |
- print >> stream, '#%s=' % optname |
+ print('#%s=' % optname, file=stream) |
else: |
value = _encode(value, encoding).strip() |
- print >> stream, '%s=%s' % (optname, value) |
+ print('%s=%s' % (optname, value), file=stream) |
format_section = ini_format_section |
def rest_format_section(stream, section, options, encoding=None, doc=None): |
- """format an options section using the INI format""" |
+ """format an options section using as ReST formatted output""" |
encoding = _get_encoding(encoding, stream) |
if section: |
print >> stream, '%s\n%s' % (section, "'"*len(section)) |
@@ -401,6 +425,7 @@ |
print >> stream, '' |
print >> stream, ' Default: ``%s``' % value.replace("`` ", "```` ``") |
+# Options Manager ############################################################## |
class OptionsManagerMixIn(object): |
"""MixIn to handle a configuration from both a configuration file and |
@@ -423,9 +448,9 @@ |
def reset_parsers(self, usage='', version=None): |
# configuration file parser |
- self.cfgfile_parser = ConfigParser() |
+ self.cfgfile_parser = cp.ConfigParser() |
# command line parser |
- self.cmdline_parser = optparse.OptionParser(usage=usage, version=version) |
+ self.cmdline_parser = optik_ext.OptionParser(usage=usage, version=version) |
self.cmdline_parser.options_manager = self |
self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) |
@@ -461,7 +486,7 @@ |
if group_name in self._mygroups: |
group = self._mygroups[group_name] |
else: |
- group = optparse.OptionGroup(self.cmdline_parser, |
+ group = optik_ext.OptionGroup(self.cmdline_parser, |
title=group_name.capitalize()) |
self.cmdline_parser.add_option_group(group) |
group.level = provider.level |
@@ -497,9 +522,9 @@ |
# default is handled here and *must not* be given to optik if you |
# want the whole machinery to work |
if 'default' in optdict: |
- if (optparse.OPTPARSE_FORMAT_DEFAULT and 'help' in optdict and |
- optdict.get('default') is not None and |
- not optdict['action'] in ('store_true', 'store_false')): |
+ if ('help' in optdict |
+ and optdict.get('default') is not None |
+ and not optdict['action'] in ('store_true', 'store_false')): |
optdict['help'] += ' [current: %default]' |
del optdict['default'] |
args = ['--' + str(opt)] |
@@ -508,7 +533,7 @@ |
args.append('-' + optdict['short']) |
del optdict['short'] |
# cleanup option definition dict before giving it to optik |
- for key in optdict.keys(): |
+ for key in list(optdict.keys()): |
if not key in self._optik_option_attrs: |
optdict.pop(key) |
return args, optdict |
@@ -555,7 +580,7 @@ |
printed = False |
for section in sections: |
if printed: |
- print >> stream, '\n' |
+ print('\n', file=stream) |
format_section(stream, section.upper(), options_by_section[section], |
encoding) |
printed = True |
@@ -566,7 +591,7 @@ |
""" |
self._monkeypatch_expand_default() |
try: |
- optparse.generate_manpage(self.cmdline_parser, pkginfo, |
+ optik_ext.generate_manpage(self.cmdline_parser, pkginfo, |
section, stream=stream or sys.stdout, |
level=self._maxlevel) |
finally: |
@@ -594,7 +619,7 @@ |
if opt in self._all_options: |
break # already processed |
def helpfunc(option, opt, val, p, level=helplevel): |
- print self.help(level) |
+ print(self.help(level)) |
sys.exit(0) |
helpmsg = '%s verbose help.' % ' '.join(['more'] * helplevel) |
optdict = {'action' : 'callback', 'callback' : helpfunc, |
@@ -616,7 +641,7 @@ |
parser._sections[sect.upper()] = values |
elif not self.quiet: |
msg = 'No config file found, using default configuration' |
- print >> sys.stderr, msg |
+ print(msg, file=sys.stderr) |
return |
def input_config(self, onlysection=None, inputlevel=0, stream=None): |
@@ -642,13 +667,13 @@ |
options provider) |
""" |
parser = self.cfgfile_parser |
- for provider in self.options_providers: |
- for section, option, optdict in provider.all_options(): |
- try: |
- value = parser.get(section, option) |
- provider.set_option(option, value, optdict=optdict) |
- except (NoSectionError, NoOptionError), ex: |
- continue |
+ for section in parser.sections(): |
+ for option, value in parser.items(section): |
+ try: |
+ self.global_set_option(option, value) |
+ except (KeyError, OptionError): |
+ # TODO handle here undeclared options appearing in the config file |
+ continue |
def load_configuration(self, **kwargs): |
"""override configuration according to given parameters |
@@ -686,7 +711,7 @@ |
def add_help_section(self, title, description, level=0): |
"""add a dummy option section for help purpose """ |
- group = optparse.OptionGroup(self.cmdline_parser, |
+ group = optik_ext.OptionGroup(self.cmdline_parser, |
title=title.capitalize(), |
description=description) |
group.level = level |
@@ -694,18 +719,18 @@ |
self.cmdline_parser.add_option_group(group) |
def _monkeypatch_expand_default(self): |
- # monkey patch optparse to deal with our default values |
+ # monkey patch optik_ext to deal with our default values |
try: |
- self.__expand_default_backup = optparse.HelpFormatter.expand_default |
- optparse.HelpFormatter.expand_default = expand_default |
+ self.__expand_default_backup = optik_ext.HelpFormatter.expand_default |
+ optik_ext.HelpFormatter.expand_default = expand_default |
except AttributeError: |
# python < 2.4: nothing to be done |
pass |
def _unmonkeypatch_expand_default(self): |
# remove monkey patch |
- if hasattr(optparse.HelpFormatter, 'expand_default'): |
- # unpatch optparse to avoid side effects |
- optparse.HelpFormatter.expand_default = self.__expand_default_backup |
+ if hasattr(optik_ext.HelpFormatter, 'expand_default'): |
+ # unpatch optik_ext to avoid side effects |
+ optik_ext.HelpFormatter.expand_default = self.__expand_default_backup |
def help(self, level=0): |
"""return the usage string for available options """ |
@@ -734,6 +759,7 @@ |
assert self._inst, 'unbound method' |
return getattr(self._inst, self.method)(*args, **kwargs) |
+# Options Provider ############################################################# |
class OptionsProviderMixIn(object): |
"""Mixin to provide options to an OptionsManager""" |
@@ -745,7 +771,7 @@ |
level = 0 |
def __init__(self): |
- self.config = optparse.Values() |
+ self.config = optik_ext.Values() |
for option in self.options: |
try: |
option, optdict = option |
@@ -777,41 +803,41 @@ |
default = default() |
return default |
- def option_name(self, opt, optdict=None): |
+ def option_attrname(self, opt, optdict=None): |
"""get the config attribute corresponding to opt |
""" |
if optdict is None: |
optdict = self.get_option_def(opt) |
return optdict.get('dest', opt.replace('-', '_')) |
+ option_name = deprecated('[0.60] OptionsProviderMixIn.option_name() was renamed to option_attrname()')(option_attrname) |
def option_value(self, opt): |
"""get the current value for the given option""" |
- return getattr(self.config, self.option_name(opt), None) |
+ return getattr(self.config, self.option_attrname(opt), None) |
def set_option(self, opt, value, action=None, optdict=None): |
"""method called to set an option (registered in the options list) |
""" |
- # print "************ setting option", opt," to value", value |
if optdict is None: |
optdict = self.get_option_def(opt) |
if value is not None: |
- value = convert(value, optdict, opt) |
+ value = _validate(value, optdict, opt) |
if action is None: |
action = optdict.get('action', 'store') |
if optdict.get('type') == 'named': # XXX need specific handling |
- optname = self.option_name(opt, optdict) |
+ optname = self.option_attrname(opt, optdict) |
currentvalue = getattr(self.config, optname, None) |
if currentvalue: |
currentvalue.update(value) |
value = currentvalue |
if action == 'store': |
- setattr(self.config, self.option_name(opt, optdict), value) |
+ setattr(self.config, self.option_attrname(opt, optdict), value) |
elif action in ('store_true', 'count'): |
- setattr(self.config, self.option_name(opt, optdict), 0) |
+ setattr(self.config, self.option_attrname(opt, optdict), 0) |
elif action == 'store_false': |
- setattr(self.config, self.option_name(opt, optdict), 1) |
+ setattr(self.config, self.option_attrname(opt, optdict), 1) |
elif action == 'append': |
- opt = self.option_name(opt, optdict) |
+ opt = self.option_attrname(opt, optdict) |
_list = getattr(self.config, opt, None) |
if _list is None: |
if isinstance(value, (list, tuple)): |
@@ -839,12 +865,12 @@ |
defaultstr = ': ' |
else: |
defaultstr = '(default: %s): ' % format_option_value(optdict, default) |
- print ':%s:' % option |
- print optdict.get('help') or option |
+ print(':%s:' % option) |
+ print(optdict.get('help') or option) |
inputfunc = INPUT_FUNCTIONS[optdict['type']] |
value = inputfunc(optdict, defaultstr) |
while default is REQUIRED and not value: |
- print 'please specify a value' |
+ print('please specify a value') |
value = inputfunc(optdict, '%s: ' % option) |
if value is None and default is not None: |
value = default |
@@ -893,6 +919,7 @@ |
for optname, optdict in options: |
yield (optname, optdict, self.option_value(optname)) |
+# configuration ################################################################ |
class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): |
"""basic mixin for simple configurations which don't need the |
@@ -913,7 +940,7 @@ |
continue |
if not gdef in self.option_groups: |
self.option_groups.append(gdef) |
- self.register_options_provider(self, own_group=0) |
+ self.register_options_provider(self, own_group=False) |
def register_options(self, options): |
"""add some options to the configuration""" |
@@ -932,8 +959,8 @@ |
def __getitem__(self, key): |
try: |
- return getattr(self.config, self.option_name(key)) |
- except (optparse.OptionValueError, AttributeError): |
+ return getattr(self.config, self.option_attrname(key)) |
+ except (optik_ext.OptionValueError, AttributeError): |
raise KeyError(key) |
def __setitem__(self, key, value): |
@@ -941,7 +968,7 @@ |
def get(self, key, default=None): |
try: |
- return getattr(self.config, self.option_name(key)) |
+ return getattr(self.config, self.option_attrname(key)) |
except (OptionError, AttributeError): |
return default |
@@ -977,20 +1004,21 @@ |
def __getitem__(self, key): |
provider = self.config._all_options[key] |
try: |
- return getattr(provider.config, provider.option_name(key)) |
+ return getattr(provider.config, provider.option_attrname(key)) |
except AttributeError: |
raise KeyError(key) |
def __setitem__(self, key, value): |
- self.config.global_set_option(self.config.option_name(key), value) |
+ self.config.global_set_option(self.config.option_attrname(key), value) |
def get(self, key, default=None): |
provider = self.config._all_options[key] |
try: |
- return getattr(provider.config, provider.option_name(key)) |
+ return getattr(provider.config, provider.option_attrname(key)) |
except AttributeError: |
return default |
+# other functions ############################################################## |
def read_old_config(newconfig, changes, configfile): |
"""initialize newconfig from a deprecated configuration file |
@@ -1055,8 +1083,13 @@ |
newconfig.set_option(optname, oldconfig[optname], optdict=optdef) |
-def merge_options(options): |
- """preprocess options to remove duplicate""" |
+def merge_options(options, optgroup=None): |
+ """preprocess a list of options and remove duplicates, returning a new list |
+ (tuple actually) of options. |
+ |
+ Options dictionaries are copied to avoid later side-effect. Also, if |
+ `otpgroup` argument is specified, ensure all options are in the given group. |
+ """ |
alloptions = {} |
options = list(options) |
for i in range(len(options)-1, -1, -1): |
@@ -1065,5 +1098,9 @@ |
options.pop(i) |
alloptions[optname].update(optdict) |
else: |
+ optdict = optdict.copy() |
+ options[i] = (optname, optdict) |
alloptions[optname] = optdict |
+ if optgroup is not None: |
+ alloptions[optname]['group'] = optgroup |
return tuple(options) |