1# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
3#
4# This file is part of logilab-common.
5#
6# logilab-common is free software: you can redistribute it and/or modify it under
7# the terms of the GNU Lesser General Public License as published by the Free
8# Software Foundation, either version 2.1 of the License, or (at your option) any
9# later version.
10#
11# logilab-common is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
14# details.
15#
16# You should have received a copy of the GNU Lesser General Public License along
17# with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
18"""Classes to handle advanced configuration in simple to complex applications.
19
20Allows to load the configuration from a file or from command line
21options, to generate a sample configuration file or to display
22program's usage. Fills the gap between optik/optparse and ConfigParser
23by adding data types (which are also available as a standalone optik
24extension in the `optik_ext` module).
25
26
27Quick start: simplest usage
28---------------------------
29
30.. python ::
31
32  >>> import sys
33  >>> from logilab.common.configuration import Configuration
34  >>> options = [('dothis', {'type':'yn', 'default': True, 'metavar': '<y or n>'}),
35  ...            ('value', {'type': 'string', 'metavar': '<string>'}),
36  ...            ('multiple', {'type': 'csv', 'default': ('yop',),
37  ...                          'metavar': '<comma separated values>',
38  ...                          'help': 'you can also document the option'}),
39  ...            ('number', {'type': 'int', 'default':2, 'metavar':'<int>'}),
40  ...           ]
41  >>> config = Configuration(options=options, name='My config')
42  >>> print config['dothis']
43  True
44  >>> print config['value']
45  None
46  >>> print config['multiple']
47  ('yop',)
48  >>> print config['number']
49  2
50  >>> print config.help()
51  Usage:  [options]
52
53  Options:
54    -h, --help            show this help message and exit
55    --dothis=<y or n>
56    --value=<string>
57    --multiple=<comma separated values>
58                          you can also document the option [current: none]
59    --number=<int>
60
61  >>> f = open('myconfig.ini', 'w')
62  >>> f.write('''[MY CONFIG]
63  ... number = 3
64  ... dothis = no
65  ... multiple = 1,2,3
66  ... ''')
67  >>> f.close()
68  >>> config.load_file_configuration('myconfig.ini')
69  >>> print config['dothis']
70  False
71  >>> print config['value']
72  None
73  >>> print config['multiple']
74  ['1', '2', '3']
75  >>> print config['number']
76  3
77  >>> sys.argv = ['mon prog', '--value', 'bacon', '--multiple', '4,5,6',
78  ...             'nonoptionargument']
79  >>> print config.load_command_line_configuration()
80  ['nonoptionargument']
81  >>> print config['value']
82  bacon
83  >>> config.generate_config()
84  # class for simple configurations which don't need the
85  # manager / providers model and prefer delegation to inheritance
86  #
87  # configuration values are accessible through a dict like interface
88  #
89  [MY CONFIG]
90
91  dothis=no
92
93  value=bacon
94
95  # you can also document the option
96  multiple=4,5,6
97
98  number=3
99
100  Note : starting with Python 2.7 ConfigParser is able to take into
101  account the order of occurrences of the options into a file (by
102  using an OrderedDict). If you have two options changing some common
103  state, like a 'disable-all-stuff' and a 'enable-some-stuff-a', their
104  order of appearance will be significant : the last specified in the
105  file wins. For earlier version of python and logilab.common newer
106  than 0.61 the behaviour is unspecified.
107
108"""
109
110from __future__ import print_function
111
112__docformat__ = "restructuredtext en"
113
114__all__ = ('OptionsManagerMixIn', 'OptionsProviderMixIn',
115           'ConfigurationMixIn', 'Configuration',
116           'OptionsManager2ConfigurationAdapter')
117
118import os
119import sys
120import re
121from os.path import exists, expanduser
122from copy import copy
123from warnings import warn
124
125from six import string_types
126from six.moves import range, configparser as cp, input
127
128from logilab.common.compat import str_encode as _encode
129from logilab.common.deprecation import deprecated
130from logilab.common.textutils import normalize_text, unquote
131from logilab.common import optik_ext
132
133OptionError = optik_ext.OptionError
134
135REQUIRED = []
136
137class UnsupportedAction(Exception):
138    """raised by set_option when it doesn't know what to do for an action"""
139
140
141def _get_encoding(encoding, stream):
142    encoding = encoding or getattr(stream, 'encoding', None)
143    if not encoding:
144        import locale
145        encoding = locale.getpreferredencoding()
146    return encoding
147
148
149# validation functions ########################################################
150
151# validators will return the validated value or raise optparse.OptionValueError
152# XXX add to documentation
153
154def choice_validator(optdict, name, value):
155    """validate and return a converted value for option of type 'choice'
156    """
157    if not value in optdict['choices']:
158        msg = "option %s: invalid value: %r, should be in %s"
159        raise optik_ext.OptionValueError(msg % (name, value, optdict['choices']))
160    return value
161
162def multiple_choice_validator(optdict, name, value):
163    """validate and return a converted value for option of type 'choice'
164    """
165    choices = optdict['choices']
166    values = optik_ext.check_csv(None, name, value)
167    for value in values:
168        if not value in choices:
169            msg = "option %s: invalid value: %r, should be in %s"
170            raise optik_ext.OptionValueError(msg % (name, value, choices))
171    return values
172
173def csv_validator(optdict, name, value):
174    """validate and return a converted value for option of type 'csv'
175    """
176    return optik_ext.check_csv(None, name, value)
177
178def yn_validator(optdict, name, value):
179    """validate and return a converted value for option of type 'yn'
180    """
181    return optik_ext.check_yn(None, name, value)
182
183def named_validator(optdict, name, value):
184    """validate and return a converted value for option of type 'named'
185    """
186    return optik_ext.check_named(None, name, value)
187
188def file_validator(optdict, name, value):
189    """validate and return a filepath for option of type 'file'"""
190    return optik_ext.check_file(None, name, value)
191
192def color_validator(optdict, name, value):
193    """validate and return a valid color for option of type 'color'"""
194    return optik_ext.check_color(None, name, value)
195
196def password_validator(optdict, name, value):
197    """validate and return a string for option of type 'password'"""
198    return optik_ext.check_password(None, name, value)
199
200def date_validator(optdict, name, value):
201    """validate and return a mx DateTime object for option of type 'date'"""
202    return optik_ext.check_date(None, name, value)
203
204def time_validator(optdict, name, value):
205    """validate and return a time object for option of type 'time'"""
206    return optik_ext.check_time(None, name, value)
207
208def bytes_validator(optdict, name, value):
209    """validate and return an integer for option of type 'bytes'"""
210    return optik_ext.check_bytes(None, name, value)
211
212
213VALIDATORS = {'string': unquote,
214              'int': int,
215              'float': float,
216              'file': file_validator,
217              'font': unquote,
218              'color': color_validator,
219              'regexp': re.compile,
220              'csv': csv_validator,
221              'yn': yn_validator,
222              'bool': yn_validator,
223              'named': named_validator,
224              'password': password_validator,
225              'date': date_validator,
226              'time': time_validator,
227              'bytes': bytes_validator,
228              'choice': choice_validator,
229              'multiple_choice': multiple_choice_validator,
230              }
231
232def _call_validator(opttype, optdict, option, value):
233    if opttype not in VALIDATORS:
234        raise Exception('Unsupported type "%s"' % opttype)
235    try:
236        return VALIDATORS[opttype](optdict, option, value)
237    except TypeError:
238        try:
239            return VALIDATORS[opttype](value)
240        except optik_ext.OptionValueError:
241            raise
242        except:
243            raise optik_ext.OptionValueError('%s value (%r) should be of type %s' %
244                                   (option, value, opttype))
245
246# user input functions ########################################################
247
248# user input functions will ask the user for input on stdin then validate
249# the result and return the validated value or raise optparse.OptionValueError
250# XXX add to documentation
251
252def input_password(optdict, question='password:'):
253    from getpass import getpass
254    while True:
255        value = getpass(question)
256        value2 = getpass('confirm: ')
257        if value == value2:
258            return value
259        print('password mismatch, try again')
260
261def input_string(optdict, question):
262    value = input(question).strip()
263    return value or None
264
265def _make_input_function(opttype):
266    def input_validator(optdict, question):
267        while True:
268            value = input(question)
269            if not value.strip():
270                return None
271            try:
272                return _call_validator(opttype, optdict, None, value)
273            except optik_ext.OptionValueError as ex:
274                msg = str(ex).split(':', 1)[-1].strip()
275                print('bad value: %s' % msg)
276    return input_validator
277
278INPUT_FUNCTIONS = {
279    'string': input_string,
280    'password': input_password,
281    }
282
283for opttype in VALIDATORS.keys():
284    INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype))
285
286# utility functions ############################################################
287
288def expand_default(self, option):
289    """monkey patch OptionParser.expand_default since we have a particular
290    way to handle defaults to avoid overriding values in the configuration
291    file
292    """
293    if self.parser is None or not self.default_tag:
294        return option.help
295    optname = option._long_opts[0][2:]
296    try:
297        provider = self.parser.options_manager._all_options[optname]
298    except KeyError:
299        value = None
300    else:
301        optdict = provider.get_option_def(optname)
302        optname = provider.option_attrname(optname, optdict)
303        value = getattr(provider.config, optname, optdict)
304        value = format_option_value(optdict, value)
305    if value is optik_ext.NO_DEFAULT or not value:
306        value = self.NO_DEFAULT_VALUE
307    return option.help.replace(self.default_tag, str(value))
308
309
310def _validate(value, optdict, name=''):
311    """return a validated value for an option according to its type
312
313    optional argument name is only used for error message formatting
314    """
315    try:
316        _type = optdict['type']
317    except KeyError:
318        # FIXME
319        return value
320    return _call_validator(_type, optdict, name, value)
321convert = deprecated('[0.60] convert() was renamed _validate()')(_validate)
322
323# format and output functions ##################################################
324
325def comment(string):
326    """return string as a comment"""
327    lines = [line.strip() for line in string.splitlines()]
328    return '# ' + ('%s# ' % os.linesep).join(lines)
329
330def format_time(value):
331    if not value:
332        return '0'
333    if value != int(value):
334        return '%.2fs' % value
335    value = int(value)
336    nbmin, nbsec = divmod(value, 60)
337    if nbsec:
338        return '%ss' % value
339    nbhour, nbmin_ = divmod(nbmin, 60)
340    if nbmin_:
341        return '%smin' % nbmin
342    nbday, nbhour_ = divmod(nbhour, 24)
343    if nbhour_:
344        return '%sh' % nbhour
345    return '%sd' % nbday
346
347def format_bytes(value):
348    if not value:
349        return '0'
350    if value != int(value):
351        return '%.2fB' % value
352    value = int(value)
353    prevunit = 'B'
354    for unit in ('KB', 'MB', 'GB', 'TB'):
355        next, remain = divmod(value, 1024)
356        if remain:
357            return '%s%s' % (value, prevunit)
358        prevunit = unit
359        value = next
360    return '%s%s' % (value, unit)
361
362def format_option_value(optdict, value):
363    """return the user input's value from a 'compiled' value"""
364    if isinstance(value, (list, tuple)):
365        value = ','.join(value)
366    elif isinstance(value, dict):
367        value = ','.join(['%s:%s' % (k, v) for k, v in value.items()])
368    elif hasattr(value, 'match'): # optdict.get('type') == 'regexp'
369        # compiled regexp
370        value = value.pattern
371    elif optdict.get('type') == 'yn':
372        value = value and 'yes' or 'no'
373    elif isinstance(value, string_types) and value.isspace():
374        value = "'%s'" % value
375    elif optdict.get('type') == 'time' and isinstance(value, (float, int, long)):
376        value = format_time(value)
377    elif optdict.get('type') == 'bytes' and hasattr(value, '__int__'):
378        value = format_bytes(value)
379    return value
380
381def ini_format_section(stream, section, options, encoding=None, doc=None):
382    """format an options section using the INI format"""
383    encoding = _get_encoding(encoding, stream)
384    if doc:
385        print(_encode(comment(doc), encoding), file=stream)
386    print('[%s]' % section, file=stream)
387    ini_format(stream, options, encoding)
388
389def ini_format(stream, options, encoding):
390    """format options using the INI format"""
391    for optname, optdict, value in options:
392        value = format_option_value(optdict, value)
393        help = optdict.get('help')
394        if help:
395            help = normalize_text(help, line_len=79, indent='# ')
396            print(file=stream)
397            print(_encode(help, encoding), file=stream)
398        else:
399            print(file=stream)
400        if value is None:
401            print('#%s=' % optname, file=stream)
402        else:
403            value = _encode(value, encoding).strip()
404            print('%s=%s' % (optname, value), file=stream)
405
406format_section = ini_format_section
407
408def rest_format_section(stream, section, options, encoding=None, doc=None):
409    """format an options section using as ReST formatted output"""
410    encoding = _get_encoding(encoding, stream)
411    if section:
412        print('%s\n%s' % (section, "'"*len(section)), file=stream)
413    if doc:
414        print(_encode(normalize_text(doc, line_len=79, indent=''), encoding), file=stream)
415        print(file=stream)
416    for optname, optdict, value in options:
417        help = optdict.get('help')
418        print(':%s:' % optname, file=stream)
419        if help:
420            help = normalize_text(help, line_len=79, indent='  ')
421            print(_encode(help, encoding), file=stream)
422        if value:
423            value = _encode(format_option_value(optdict, value), encoding)
424            print(file=stream)
425            print('  Default: ``%s``' % value.replace("`` ", "```` ``"), file=stream)
426
427# Options Manager ##############################################################
428
429class OptionsManagerMixIn(object):
430    """MixIn to handle a configuration from both a configuration file and
431    command line options
432    """
433
434    def __init__(self, usage, config_file=None, version=None, quiet=0):
435        self.config_file = config_file
436        self.reset_parsers(usage, version=version)
437        # list of registered options providers
438        self.options_providers = []
439        # dictionary associating option name to checker
440        self._all_options = {}
441        self._short_options = {}
442        self._nocallback_options = {}
443        self._mygroups = dict()
444        # verbosity
445        self.quiet = quiet
446        self._maxlevel = 0
447
448    def reset_parsers(self, usage='', version=None):
449        # configuration file parser
450        self.cfgfile_parser = cp.ConfigParser()
451        # command line parser
452        self.cmdline_parser = optik_ext.OptionParser(usage=usage, version=version)
453        self.cmdline_parser.options_manager = self
454        self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS)
455
456    def register_options_provider(self, provider, own_group=True):
457        """register an options provider"""
458        assert provider.priority <= 0, "provider's priority can't be >= 0"
459        for i in range(len(self.options_providers)):
460            if provider.priority > self.options_providers[i].priority:
461                self.options_providers.insert(i, provider)
462                break
463        else:
464            self.options_providers.append(provider)
465        non_group_spec_options = [option for option in provider.options
466                                  if 'group' not in option[1]]
467        groups = getattr(provider, 'option_groups', ())
468        if own_group and non_group_spec_options:
469            self.add_option_group(provider.name.upper(), provider.__doc__,
470                                  non_group_spec_options, provider)
471        else:
472            for opt, optdict in non_group_spec_options:
473                self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
474        for gname, gdoc in groups:
475            gname = gname.upper()
476            goptions = [option for option in provider.options
477                        if option[1].get('group', '').upper() == gname]
478            self.add_option_group(gname, gdoc, goptions, provider)
479
480    def add_option_group(self, group_name, doc, options, provider):
481        """add an option group including the listed options
482        """
483        assert options
484        # add option group to the command line parser
485        if group_name in self._mygroups:
486            group = self._mygroups[group_name]
487        else:
488            group = optik_ext.OptionGroup(self.cmdline_parser,
489                                         title=group_name.capitalize())
490            self.cmdline_parser.add_option_group(group)
491            group.level = provider.level
492            self._mygroups[group_name] = group
493            # add section to the config file
494            if group_name != "DEFAULT":
495                self.cfgfile_parser.add_section(group_name)
496        # add provider's specific options
497        for opt, optdict in options:
498            self.add_optik_option(provider, group, opt, optdict)
499
500    def add_optik_option(self, provider, optikcontainer, opt, optdict):
501        if 'inputlevel' in optdict:
502            warn('[0.50] "inputlevel" in option dictionary for %s is deprecated,'
503                 ' use "level"' % opt, DeprecationWarning)
504            optdict['level'] = optdict.pop('inputlevel')
505        args, optdict = self.optik_option(provider, opt, optdict)
506        option = optikcontainer.add_option(*args, **optdict)
507        self._all_options[opt] = provider
508        self._maxlevel = max(self._maxlevel, option.level or 0)
509
510    def optik_option(self, provider, opt, optdict):
511        """get our personal option definition and return a suitable form for
512        use with optik/optparse
513        """
514        optdict = copy(optdict)
515        others = {}
516        if 'action' in optdict:
517            self._nocallback_options[provider] = opt
518        else:
519            optdict['action'] = 'callback'
520            optdict['callback'] = self.cb_set_provider_option
521        # default is handled here and *must not* be given to optik if you
522        # want the whole machinery to work
523        if 'default' in optdict:
524            if ('help' in optdict
525                and optdict.get('default') is not None
526                and not optdict['action'] in ('store_true', 'store_false')):
527                optdict['help'] += ' [current: %default]'
528            del optdict['default']
529        args = ['--' + str(opt)]
530        if 'short' in optdict:
531            self._short_options[optdict['short']] = opt
532            args.append('-' + optdict['short'])
533            del optdict['short']
534        # cleanup option definition dict before giving it to optik
535        for key in list(optdict.keys()):
536            if not key in self._optik_option_attrs:
537                optdict.pop(key)
538        return args, optdict
539
540    def cb_set_provider_option(self, option, opt, value, parser):
541        """optik callback for option setting"""
542        if opt.startswith('--'):
543            # remove -- on long option
544            opt = opt[2:]
545        else:
546            # short option, get its long equivalent
547            opt = self._short_options[opt[1:]]
548        # trick since we can't set action='store_true' on options
549        if value is None:
550            value = 1
551        self.global_set_option(opt, value)
552
553    def global_set_option(self, opt, value):
554        """set option on the correct option provider"""
555        self._all_options[opt].set_option(opt, value)
556
557    def generate_config(self, stream=None, skipsections=(), encoding=None):
558        """write a configuration file according to the current configuration
559        into the given stream or stdout
560        """
561        options_by_section = {}
562        sections = []
563        for provider in self.options_providers:
564            for section, options in provider.options_by_section():
565                if section is None:
566                    section = provider.name
567                if section in skipsections:
568                    continue
569                options = [(n, d, v) for (n, d, v) in options
570                           if d.get('type') is not None]
571                if not options:
572                    continue
573                if not section in sections:
574                    sections.append(section)
575                alloptions = options_by_section.setdefault(section, [])
576                alloptions += options
577        stream = stream or sys.stdout
578        encoding = _get_encoding(encoding, stream)
579        printed = False
580        for section in sections:
581            if printed:
582                print('\n', file=stream)
583            format_section(stream, section.upper(), options_by_section[section],
584                           encoding)
585            printed = True
586
587    def generate_manpage(self, pkginfo, section=1, stream=None):
588        """write a man page for the current configuration into the given
589        stream or stdout
590        """
591        self._monkeypatch_expand_default()
592        try:
593            optik_ext.generate_manpage(self.cmdline_parser, pkginfo,
594                                      section, stream=stream or sys.stdout,
595                                      level=self._maxlevel)
596        finally:
597            self._unmonkeypatch_expand_default()
598
599    # initialization methods ##################################################
600
601    def load_provider_defaults(self):
602        """initialize configuration using default values"""
603        for provider in self.options_providers:
604            provider.load_defaults()
605
606    def load_file_configuration(self, config_file=None):
607        """load the configuration from file"""
608        self.read_config_file(config_file)
609        self.load_config_file()
610
611    def read_config_file(self, config_file=None):
612        """read the configuration file but do not load it (i.e. dispatching
613        values to each options provider)
614        """
615        helplevel = 1
616        while helplevel <= self._maxlevel:
617            opt = '-'.join(['long'] * helplevel) + '-help'
618            if opt in self._all_options:
619                break # already processed
620            def helpfunc(option, opt, val, p, level=helplevel):
621                print(self.help(level))
622                sys.exit(0)
623            helpmsg = '%s verbose help.' % ' '.join(['more'] * helplevel)
624            optdict = {'action' : 'callback', 'callback' : helpfunc,
625                       'help' : helpmsg}
626            provider = self.options_providers[0]
627            self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
628            provider.options += ( (opt, optdict), )
629            helplevel += 1
630        if config_file is None:
631            config_file = self.config_file
632        if config_file is not None:
633            config_file = expanduser(config_file)
634        if config_file and exists(config_file):
635            parser = self.cfgfile_parser
636            parser.read([config_file])
637            # normalize sections'title
638            for sect, values in parser._sections.items():
639                if not sect.isupper() and values:
640                    parser._sections[sect.upper()] = values
641        elif not self.quiet:
642            msg = 'No config file found, using default configuration'
643            print(msg, file=sys.stderr)
644            return
645
646    def input_config(self, onlysection=None, inputlevel=0, stream=None):
647        """interactively get configuration values by asking to the user and generate
648        a configuration file
649        """
650        if onlysection is not None:
651            onlysection = onlysection.upper()
652        for provider in self.options_providers:
653            for section, option, optdict in provider.all_options():
654                if onlysection is not None and section != onlysection:
655                    continue
656                if not 'type' in optdict:
657                    # ignore action without type (callback, store_true...)
658                    continue
659                provider.input_option(option, optdict, inputlevel)
660        # now we can generate the configuration file
661        if stream is not None:
662            self.generate_config(stream)
663
664    def load_config_file(self):
665        """dispatch values previously read from a configuration file to each
666        options provider)
667        """
668        parser = self.cfgfile_parser
669        for section in parser.sections():
670             for option, value in parser.items(section):
671                  try:
672                       self.global_set_option(option, value)
673                  except (KeyError, OptionError):
674                       # TODO handle here undeclared options appearing in the config file
675                       continue
676
677    def load_configuration(self, **kwargs):
678        """override configuration according to given parameters
679        """
680        for opt, opt_value in kwargs.items():
681            opt = opt.replace('_', '-')
682            provider = self._all_options[opt]
683            provider.set_option(opt, opt_value)
684
685    def load_command_line_configuration(self, args=None):
686        """override configuration according to command line parameters
687
688        return additional arguments
689        """
690        self._monkeypatch_expand_default()
691        try:
692            if args is None:
693                args = sys.argv[1:]
694            else:
695                args = list(args)
696            (options, args) = self.cmdline_parser.parse_args(args=args)
697            for provider in self._nocallback_options.keys():
698                config = provider.config
699                for attr in config.__dict__.keys():
700                    value = getattr(options, attr, None)
701                    if value is None:
702                        continue
703                    setattr(config, attr, value)
704            return args
705        finally:
706            self._unmonkeypatch_expand_default()
707
708
709    # help methods ############################################################
710
711    def add_help_section(self, title, description, level=0):
712        """add a dummy option section for help purpose """
713        group = optik_ext.OptionGroup(self.cmdline_parser,
714                                     title=title.capitalize(),
715                                     description=description)
716        group.level = level
717        self._maxlevel = max(self._maxlevel, level)
718        self.cmdline_parser.add_option_group(group)
719
720    def _monkeypatch_expand_default(self):
721        # monkey patch optik_ext to deal with our default values
722        try:
723            self.__expand_default_backup = optik_ext.HelpFormatter.expand_default
724            optik_ext.HelpFormatter.expand_default = expand_default
725        except AttributeError:
726            # python < 2.4: nothing to be done
727            pass
728    def _unmonkeypatch_expand_default(self):
729        # remove monkey patch
730        if hasattr(optik_ext.HelpFormatter, 'expand_default'):
731            # unpatch optik_ext to avoid side effects
732            optik_ext.HelpFormatter.expand_default = self.__expand_default_backup
733
734    def help(self, level=0):
735        """return the usage string for available options """
736        self.cmdline_parser.formatter.output_level = level
737        self._monkeypatch_expand_default()
738        try:
739            return self.cmdline_parser.format_help()
740        finally:
741            self._unmonkeypatch_expand_default()
742
743
744class Method(object):
745    """used to ease late binding of default method (so you can define options
746    on the class using default methods on the configuration instance)
747    """
748    def __init__(self, methname):
749        self.method = methname
750        self._inst = None
751
752    def bind(self, instance):
753        """bind the method to its instance"""
754        if self._inst is None:
755            self._inst = instance
756
757    def __call__(self, *args, **kwargs):
758        assert self._inst, 'unbound method'
759        return getattr(self._inst, self.method)(*args, **kwargs)
760
761# Options Provider #############################################################
762
763class OptionsProviderMixIn(object):
764    """Mixin to provide options to an OptionsManager"""
765
766    # those attributes should be overridden
767    priority = -1
768    name = 'default'
769    options = ()
770    level = 0
771
772    def __init__(self):
773        self.config = optik_ext.Values()
774        for option in self.options:
775            try:
776                option, optdict = option
777            except ValueError:
778                raise Exception('Bad option: %r' % option)
779            if isinstance(optdict.get('default'), Method):
780                optdict['default'].bind(self)
781            elif isinstance(optdict.get('callback'), Method):
782                optdict['callback'].bind(self)
783        self.load_defaults()
784
785    def load_defaults(self):
786        """initialize the provider using default values"""
787        for opt, optdict in self.options:
788            action = optdict.get('action')
789            if action != 'callback':
790                # callback action have no default
791                default = self.option_default(opt, optdict)
792                if default is REQUIRED:
793                    continue
794                self.set_option(opt, default, action, optdict)
795
796    def option_default(self, opt, optdict=None):
797        """return the default value for an option"""
798        if optdict is None:
799            optdict = self.get_option_def(opt)
800        default = optdict.get('default')
801        if callable(default):
802            default = default()
803        return default
804
805    def option_attrname(self, opt, optdict=None):
806        """get the config attribute corresponding to opt
807        """
808        if optdict is None:
809            optdict = self.get_option_def(opt)
810        return optdict.get('dest', opt.replace('-', '_'))
811    option_name = deprecated('[0.60] OptionsProviderMixIn.option_name() was renamed to option_attrname()')(option_attrname)
812
813    def option_value(self, opt):
814        """get the current value for the given option"""
815        return getattr(self.config, self.option_attrname(opt), None)
816
817    def set_option(self, opt, value, action=None, optdict=None):
818        """method called to set an option (registered in the options list)
819        """
820        if optdict is None:
821            optdict = self.get_option_def(opt)
822        if value is not None:
823            value = _validate(value, optdict, opt)
824        if action is None:
825            action = optdict.get('action', 'store')
826        if optdict.get('type') == 'named': # XXX need specific handling
827            optname = self.option_attrname(opt, optdict)
828            currentvalue = getattr(self.config, optname, None)
829            if currentvalue:
830                currentvalue.update(value)
831                value = currentvalue
832        if action == 'store':
833            setattr(self.config, self.option_attrname(opt, optdict), value)
834        elif action in ('store_true', 'count'):
835            setattr(self.config, self.option_attrname(opt, optdict), 0)
836        elif action == 'store_false':
837            setattr(self.config, self.option_attrname(opt, optdict), 1)
838        elif action == 'append':
839            opt = self.option_attrname(opt, optdict)
840            _list = getattr(self.config, opt, None)
841            if _list is None:
842                if isinstance(value, (list, tuple)):
843                    _list = value
844                elif value is not None:
845                    _list = []
846                    _list.append(value)
847                setattr(self.config, opt, _list)
848            elif isinstance(_list, tuple):
849                setattr(self.config, opt, _list + (value,))
850            else:
851                _list.append(value)
852        elif action == 'callback':
853            optdict['callback'](None, opt, value, None)
854        else:
855            raise UnsupportedAction(action)
856
857    def input_option(self, option, optdict, inputlevel=99):
858        default = self.option_default(option, optdict)
859        if default is REQUIRED:
860            defaultstr = '(required): '
861        elif optdict.get('level', 0) > inputlevel:
862            return
863        elif optdict['type'] == 'password' or default is None:
864            defaultstr = ': '
865        else:
866            defaultstr = '(default: %s): ' % format_option_value(optdict, default)
867        print(':%s:' % option)
868        print(optdict.get('help') or option)
869        inputfunc = INPUT_FUNCTIONS[optdict['type']]
870        value = inputfunc(optdict, defaultstr)
871        while default is REQUIRED and not value:
872            print('please specify a value')
873            value = inputfunc(optdict, '%s: ' % option)
874        if value is None and default is not None:
875            value = default
876        self.set_option(option, value, optdict=optdict)
877
878    def get_option_def(self, opt):
879        """return the dictionary defining an option given it's name"""
880        assert self.options
881        for option in self.options:
882            if option[0] == opt:
883                return option[1]
884        raise OptionError('no such option %s in section %r'
885                          % (opt, self.name), opt)
886
887
888    def all_options(self):
889        """return an iterator on available options for this provider
890        option are actually described by a 3-uple:
891        (section, option name, option dictionary)
892        """
893        for section, options in self.options_by_section():
894            if section is None:
895                if self.name is None:
896                    continue
897                section = self.name.upper()
898            for option, optiondict, value in options:
899                yield section, option, optiondict
900
901    def options_by_section(self):
902        """return an iterator on options grouped by section
903
904        (section, [list of (optname, optdict, optvalue)])
905        """
906        sections = {}
907        for optname, optdict in self.options:
908            sections.setdefault(optdict.get('group'), []).append(
909                (optname, optdict, self.option_value(optname)))
910        if None in sections:
911            yield None, sections.pop(None)
912        for section, options in sections.items():
913            yield section.upper(), options
914
915    def options_and_values(self, options=None):
916        if options is None:
917            options = self.options
918        for optname, optdict in options:
919            yield (optname, optdict, self.option_value(optname))
920
921# configuration ################################################################
922
923class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn):
924    """basic mixin for simple configurations which don't need the
925    manager / providers model
926    """
927    def __init__(self, *args, **kwargs):
928        if not args:
929            kwargs.setdefault('usage', '')
930        kwargs.setdefault('quiet', 1)
931        OptionsManagerMixIn.__init__(self, *args, **kwargs)
932        OptionsProviderMixIn.__init__(self)
933        if not getattr(self, 'option_groups', None):
934            self.option_groups = []
935            for option, optdict in self.options:
936                try:
937                    gdef = (optdict['group'].upper(), '')
938                except KeyError:
939                    continue
940                if not gdef in self.option_groups:
941                    self.option_groups.append(gdef)
942        self.register_options_provider(self, own_group=False)
943
944    def register_options(self, options):
945        """add some options to the configuration"""
946        options_by_group = {}
947        for optname, optdict in options:
948            options_by_group.setdefault(optdict.get('group', self.name.upper()), []).append((optname, optdict))
949        for group, options in options_by_group.items():
950            self.add_option_group(group, None, options, self)
951        self.options += tuple(options)
952
953    def load_defaults(self):
954        OptionsProviderMixIn.load_defaults(self)
955
956    def __iter__(self):
957        return iter(self.config.__dict__.iteritems())
958
959    def __getitem__(self, key):
960        try:
961            return getattr(self.config, self.option_attrname(key))
962        except (optik_ext.OptionValueError, AttributeError):
963            raise KeyError(key)
964
965    def __setitem__(self, key, value):
966        self.set_option(key, value)
967
968    def get(self, key, default=None):
969        try:
970            return getattr(self.config, self.option_attrname(key))
971        except (OptionError, AttributeError):
972            return default
973
974
975class Configuration(ConfigurationMixIn):
976    """class for simple configurations which don't need the
977    manager / providers model and prefer delegation to inheritance
978
979    configuration values are accessible through a dict like interface
980    """
981
982    def __init__(self, config_file=None, options=None, name=None,
983                 usage=None, doc=None, version=None):
984        if options is not None:
985            self.options = options
986        if name is not None:
987            self.name = name
988        if doc is not None:
989            self.__doc__ = doc
990        super(Configuration, self).__init__(config_file=config_file, usage=usage, version=version)
991
992
993class OptionsManager2ConfigurationAdapter(object):
994    """Adapt an option manager to behave like a
995    `logilab.common.configuration.Configuration` instance
996    """
997    def __init__(self, provider):
998        self.config = provider
999
1000    def __getattr__(self, key):
1001        return getattr(self.config, key)
1002
1003    def __getitem__(self, key):
1004        provider = self.config._all_options[key]
1005        try:
1006            return getattr(provider.config, provider.option_attrname(key))
1007        except AttributeError:
1008            raise KeyError(key)
1009
1010    def __setitem__(self, key, value):
1011        self.config.global_set_option(self.config.option_attrname(key), value)
1012
1013    def get(self, key, default=None):
1014        provider = self.config._all_options[key]
1015        try:
1016            return getattr(provider.config, provider.option_attrname(key))
1017        except AttributeError:
1018            return default
1019
1020# other functions ##############################################################
1021
1022def read_old_config(newconfig, changes, configfile):
1023    """initialize newconfig from a deprecated configuration file
1024
1025    possible changes:
1026    * ('renamed', oldname, newname)
1027    * ('moved', option, oldgroup, newgroup)
1028    * ('typechanged', option, oldtype, newvalue)
1029    """
1030    # build an index of changes
1031    changesindex = {}
1032    for action in changes:
1033        if action[0] == 'moved':
1034            option, oldgroup, newgroup = action[1:]
1035            changesindex.setdefault(option, []).append((action[0], oldgroup, newgroup))
1036            continue
1037        if action[0] == 'renamed':
1038            oldname, newname = action[1:]
1039            changesindex.setdefault(newname, []).append((action[0], oldname))
1040            continue
1041        if action[0] == 'typechanged':
1042            option, oldtype, newvalue = action[1:]
1043            changesindex.setdefault(option, []).append((action[0], oldtype, newvalue))
1044            continue
1045        if action[1] in ('added', 'removed'):
1046            continue # nothing to do here
1047        raise Exception('unknown change %s' % action[0])
1048    # build a config object able to read the old config
1049    options = []
1050    for optname, optdef in newconfig.options:
1051        for action in changesindex.pop(optname, ()):
1052            if action[0] == 'moved':
1053                oldgroup, newgroup = action[1:]
1054                optdef = optdef.copy()
1055                optdef['group'] = oldgroup
1056            elif action[0] == 'renamed':
1057                optname = action[1]
1058            elif action[0] == 'typechanged':
1059                oldtype = action[1]
1060                optdef = optdef.copy()
1061                optdef['type'] = oldtype
1062        options.append((optname, optdef))
1063    if changesindex:
1064        raise Exception('unapplied changes: %s' % changesindex)
1065    oldconfig = Configuration(options=options, name=newconfig.name)
1066    # read the old config
1067    oldconfig.load_file_configuration(configfile)
1068    # apply values reverting changes
1069    changes.reverse()
1070    done = set()
1071    for action in changes:
1072        if action[0] == 'renamed':
1073            oldname, newname = action[1:]
1074            newconfig[newname] = oldconfig[oldname]
1075            done.add(newname)
1076        elif action[0] == 'typechanged':
1077            optname, oldtype, newvalue = action[1:]
1078            newconfig[optname] = newvalue
1079            done.add(optname)
1080    for optname, optdef in newconfig.options:
1081        if optdef.get('type') and not optname in done:
1082            newconfig.set_option(optname, oldconfig[optname], optdict=optdef)
1083
1084
1085def merge_options(options, optgroup=None):
1086    """preprocess a list of options and remove duplicates, returning a new list
1087    (tuple actually) of options.
1088
1089    Options dictionaries are copied to avoid later side-effect. Also, if
1090    `otpgroup` argument is specified, ensure all options are in the given group.
1091    """
1092    alloptions = {}
1093    options = list(options)
1094    for i in range(len(options)-1, -1, -1):
1095        optname, optdict = options[i]
1096        if optname in alloptions:
1097            options.pop(i)
1098            alloptions[optname].update(optdict)
1099        else:
1100            optdict = optdict.copy()
1101            options[i] = (optname, optdict)
1102            alloptions[optname] = optdict
1103        if optgroup is not None:
1104            alloptions[optname]['group'] = optgroup
1105    return tuple(options)
1106