root / logilab.pylintinstaller / logilab / common / configuration.py

Revision 202:d67e86292521, 33.3 kB (checked in by tziade@…, 9 months ago)

added logilab.pylintinstaller

Line 
1# This program is free software; you can redistribute it and/or modify it under
2# the terms of the GNU General Public License as published by the Free Software
3# Foundation; either version 2 of the License, or (at your option) any later
4# version.
5#
6# This program is distributed in the hope that it will be useful, but WITHOUT
7# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
8# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc.,
12# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
13"""Some classes used to handle advanced configuration in simple to
14complex applications.
15
16It's able to load the configuration from a file and or command line
17options, to generate a sample configuration file or to display program's
18usage. It basically fill the gap between optik/optparse and ConfigParser,
19with some additional data types (available as standalone optik extension
20in the `optik_ext` module)
21
22
23Quick start: simplest usage
24```````````````````````````
25
26import sys
27from logilab.common.configuration import Configuration
28
29options = [('dothis', {'type':'yn', 'default': True, 'metavar': '<y or n>'}),
30           ('value', {'type': 'string', 'metavar': '<string>'}),
31           ('multiple', {'type': 'csv', 'default': ('yop',),
32                         'metavar': '<comma separated values>',
33                         'help': 'you can also document the option'}),
34           ('number', {'type': 'int', 'default':2, 'metavar':'<int>'}),
35           ]
36config = Configuration(options=options, name='My config')
37print config['dothis']
38print config['value']
39print config['multiple']
40print config['number']
41
42print config.help()
43
44f = open('myconfig.ini', 'w')
45f.write('''[MY CONFIG]
46number = 3
47dothis = no
48multiple = 1,2,3
49''')
50f.close()
51config.load_file_configuration('myconfig.ini')
52print config['dothis']
53print config['value']
54print config['multiple']
55print config['number']
56
57sys.argv = ['mon prog', '--value', 'bacon', '--multiple', '4,5,6',
58            'nonoptionargument']
59print config.load_command_line_configuration()
60print config['value']
61
62config.generate_config()
63
64
65:author:    Logilab
66:copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE)
67:contact:   http://www.logilab.fr/ -- mailto:python-projects@logilab.org
68"""
69
70from __future__ import generators 
71
72__docformat__ = "restructuredtext en"
73__all__ = ('OptionsManagerMixIn', 'OptionsProviderMixIn',
74           'ConfigurationMixIn', 'Configuration',
75           'OptionsManager2ConfigurationAdapter')
76
77import os
78import sys
79import re
80from os.path import exists
81from copy import copy
82from ConfigParser import ConfigParser, NoOptionError, NoSectionError
83
84from logilab.common.compat import set
85from logilab.common.textutils import normalize_text, unquote
86from logilab.common.optik_ext import OptionParser, OptionGroup, Values, \
87     OptionValueError, OptionError, HelpFormatter, generate_manpage, check_date, \
88     check_yn, check_csv, check_file, check_color, check_named, check_password,\
89     NO_DEFAULT, OPTPARSE_FORMAT_DEFAULT
90
91REQUIRED = []
92
93class UnsupportedAction(Exception):
94    """raised by set_option when it doesn't know what to do for an action"""
95
96# validation functions ########################################################
97
98def choice_validator(opt_dict, name, value):
99    """validate and return a converted value for option of type 'choice'
100    """
101    if not value in opt_dict['choices']:
102        msg = "option %s: invalid value: %r, should be in %s"
103        raise OptionValueError(msg % (name, value, opt_dict['choices']))
104    return value
105
106def multiple_choice_validator(opt_dict, name, value):
107    """validate and return a converted value for option of type 'choice'
108    """
109    choices = opt_dict['choices']
110    values = check_csv(None, name, value)
111    for value in values:
112        if not value in choices:
113            msg = "option %s: invalid value: %r, should be in %s"
114            raise OptionValueError(msg % (name, value, choices))
115    return values
116
117def csv_validator(opt_dict, name, value):
118    """validate and return a converted value for option of type 'csv'
119    """
120    return check_csv(None, name, value)
121
122def yn_validator(opt_dict, name, value):
123    """validate and return a converted value for option of type 'yn'
124    """
125    return check_yn(None, name, value)
126
127def named_validator(opt_dict, name, value):
128    """validate and return a converted value for option of type 'named'
129    """
130    return check_named(None, name, value)
131
132def file_validator(opt_dict, name, value):
133    """validate and return a filepath for option of type 'file'"""
134    return check_file(None, name, value)
135
136def color_validator(opt_dict, name, value):
137    """validate and return a valid color for option of type 'color'"""
138    return check_color(None, name, value)
139
140def password_validator(opt_dict, name, value):
141    """validate and return a string for option of type 'password'"""
142    return check_password(None, name, value)
143
144def date_validator(opt_dict, name, value):
145    """validate and return a mx DateTime object for option of type 'date'"""
146    return check_password(None, name, value)
147
148
149VALIDATORS = {'string' : unquote,
150              'int' : int,
151              'float': float,
152              'file': file_validator,
153              'font': unquote,
154              'color': color_validator,
155              'regexp': re.compile,
156              'csv': csv_validator,
157              'yn': yn_validator,
158              'bool': yn_validator,
159              'named': named_validator,
160              'password': password_validator,
161              'date': date_validator,
162              'choice': choice_validator,
163              'multiple_choice': multiple_choice_validator,
164              }
165
166def _call_validator(opttype, optdict, option, value):
167    if not VALIDATORS.has_key(opttype):
168        raise Exception('Unsupported type "%s"' % opttype)
169    try:
170        return VALIDATORS[opttype](optdict, option, value)
171    except TypeError:
172        try:
173            return VALIDATORS[opttype](value)
174        except OptionValueError:
175            raise
176        except:
177            raise OptionValueError('%s value (%r) should be of type %s' %
178                                   (option, value, opttype))
179
180# user input functions ########################################################
181
182def input_password(optdict, question='password:'):
183    from getpass import getpass
184    while True:
185        value = getpass(question)
186        value2 = getpass('confirm: ')
187        if value == value2:
188            return value
189        print 'password mismatch, try again'
190
191def input_string(optdict, question):
192    value = raw_input(question).strip()
193    return value or None
194
195def _make_input_function(opttype):
196    def input_validator(optdict, question):
197        while True:
198            value = raw_input(question)
199            if not value.strip():
200                return None
201            try:
202                return _call_validator(opttype, optdict, None, value)
203            except OptionValueError, ex:
204                msg = str(ex).split(':', 1)[-1].strip()
205                print 'bad value: %s' % msg
206    return input_validator
207
208INPUT_FUNCTIONS = {
209    'string': input_string,
210    'password': input_password,
211    }
212
213for opttype in VALIDATORS.keys():
214    INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype))
215   
216def expand_default(self, option):
217    """monkey patch OptionParser.expand_default since we have a particular
218    way to handle defaults to avoid overriding values in the configuration
219    file
220    """
221    if self.parser is None or not self.default_tag:
222        return option.help
223    optname = option._long_opts[0][2:]
224    try:
225        provider = self.parser.options_manager._all_options[optname]
226    except KeyError:
227        value = None
228    else:
229        optdict = provider.get_option_def(optname)
230        optname = provider.option_name(optname, optdict)
231        value = getattr(provider.config, optname, optdict)
232        value = format_option_value(optdict, value)
233    if value is NO_DEFAULT or not value:
234        value = self.NO_DEFAULT_VALUE
235    return option.help.replace(self.default_tag, str(value))
236
237   
238def convert(value, opt_dict, name=''):
239    """return a validated value for an option according to its type
240   
241    optional argument name is only used for error message formatting
242    """
243    try:
244        _type = opt_dict['type']
245    except KeyError:
246        # FIXME
247        return value
248    return _call_validator(_type, opt_dict, name, value)
249
250def comment(string):
251    """return string as a comment"""
252    lines = [line.strip() for line in string.splitlines()]
253    return '# ' + ('%s# ' % os.linesep).join(lines)
254
255def format_option_value(optdict, value):
256    """return the user input's value from a 'compiled' value"""
257    if isinstance(value, (list, tuple)):
258        value = ','.join(value)
259    elif isinstance(value, dict):
260        value = ','.join(['%s:%s' % (k,v) for k,v in value.items()])   
261    elif hasattr(value, 'match'): # optdict.get('type') == 'regexp'
262        # compiled regexp
263        value = value.pattern
264    elif optdict.get('type') == 'yn':
265        value = value and 'yes' or 'no'
266    elif isinstance(value, (str, unicode)) and value.isspace():
267        value = "'%s'" % value
268    return value
269
270def ini_format_section(stream, section, options, doc=None):
271    """format an options section using the INI format"""
272    if doc:
273        print >> stream, comment(doc)
274    print >> stream, '[%s]' % section
275    for optname, optdict, value in options:
276        value = format_option_value(optdict, value)
277        help = optdict.get('help')
278        if help:
279            print >> stream
280            print >> stream, normalize_text(help, line_len=79, indent='# ')
281        else:
282            print >> stream
283        if value is None:
284            print >> stream, '#%s=' % optname
285        else:
286            print >> stream, '%s=%s' % (optname, str(value).strip())
287       
288format_section = ini_format_section
289
290def rest_format_section(stream, section, options, doc=None):
291    """format an options section using the INI format"""
292    if section:
293        print >> stream, '%s\n%s' % (section, "'"*len(section))
294    if doc:
295        print >> stream, normalize_text(doc, line_len=79, indent='')
296        print >> stream
297    for optname, optdict, value in options:
298        help = optdict.get('help')
299        print >> stream, ':%s:' % optname
300        if help:
301            print >> stream, normalize_text(help, line_len=79, indent='  ')
302        if value:
303            print >> stream, '  Default: %s' % format_option_value(optdict, value)
304
305
306class OptionsManagerMixIn(object):
307    """MixIn to handle a configuration from both a configuration file and
308    command line options
309    """
310   
311    def __init__(self, usage, config_file=None, version=None, quiet=0):
312        self.config_file = config_file
313        self.reset_parsers(usage, version=version)
314        # list of registered options providers
315        self.options_providers = []
316        # dictionary assocating option name to checker
317        self._all_options = {}
318        self._short_options = {}
319        self._nocallback_options = {}
320        # verbosity
321        self.quiet = quiet
322
323    def reset_parsers(self, usage='', version=None):
324        # configuration file parser
325        self._config_parser = ConfigParser()
326        # command line parser
327        self._optik_parser = OptionParser(usage=usage, version=version)
328        self._optik_parser.options_manager = self
329       
330    def register_options_provider(self, provider, own_group=True):
331        """register an options provider"""
332        assert provider.priority <= 0, "provider's priority can't be >= 0"
333        for i in range(len(self.options_providers)):
334            if provider.priority > self.options_providers[i].priority:
335                self.options_providers.insert(i, provider)
336                break
337        else:
338            self.options_providers.append(provider)
339        non_group_spec_options = [option for option in provider.options
340                                  if not option[1].has_key('group')]
341        groups = getattr(provider, 'option_groups', ())
342        if own_group:
343            self.add_option_group(provider.name.upper(), provider.__doc__,
344                                  non_group_spec_options, provider)
345        else:
346            for opt_name, opt_dict in non_group_spec_options:
347                args, opt_dict = self.optik_option(provider, opt_name, opt_dict)
348                self._optik_parser.add_option(*args, **opt_dict)
349                self._all_options[opt_name] = provider               
350        for gname, gdoc in groups:
351            goptions = [option for option in provider.options
352                        if option[1].get('group') == gname]
353            self.add_option_group(gname, gdoc, goptions, provider)
354       
355    def add_option_group(self, group_name, doc, options, provider):
356        """add an option group including the listed options
357        """
358        # add section to the config file
359        self._config_parser.add_section(group_name)
360        # add option group to the command line parser
361        if options:
362            group = OptionGroup(self._optik_parser,
363                                title=group_name.capitalize())
364            self._optik_parser.add_option_group(group)
365        # add provider's specific options
366        for opt_name, opt_dict in options:
367            args, opt_dict = self.optik_option(provider, opt_name, opt_dict)
368            group.add_option(*args, **opt_dict)
369            self._all_options[opt_name] = provider
370           
371    def optik_option(self, provider, opt_name, opt_dict):
372        """get our personal option definition and return a suitable form for
373        use with optik/optparse
374        """
375        opt_dict = copy(opt_dict)
376        if opt_dict.has_key('action'):
377            self._nocallback_options[provider] = opt_name
378        else:
379            opt_dict['action'] = 'callback'
380            opt_dict['callback'] = self.cb_set_provider_option
381        for specific in ('default', 'group', 'inputlevel'):
382            if opt_dict.has_key(specific):
383                del opt_dict[specific]
384                if (OPTPARSE_FORMAT_DEFAULT
385                    and specific == 'default' and opt_dict.has_key('help')):
386                    opt_dict['help'] += ' [current: %default]'
387        args = ['--' + opt_name]
388        if opt_dict.has_key('short'):
389            self._short_options[opt_dict['short']] = opt_name
390            args.append('-' + opt_dict['short'])
391            del opt_dict['short']
392        available_keys = set(self._optik_parser.option_class.ATTRS)
393        for key in opt_dict.keys():
394            if not key in available_keys:
395                opt_dict.pop(key)
396        return args, opt_dict
397           
398    def cb_set_provider_option(self, option, opt_name, value, parser):
399        """optik callback for option setting"""
400        if opt_name.startswith('--'):
401            # remove -- on long option
402            opt_name = opt_name[2:]
403        else:
404            # short option, get its long equivalent
405            opt_name = self._short_options[opt_name[1:]]
406        # trick since we can't set action='store_true' on options
407        if value is None:
408            value = 1
409        self.global_set_option(opt_name, value)
410