Back to index

python-cliapp  1.20120630
settings.py
Go to the documentation of this file.
00001 # Copyright (C) 2009-2012  Lars Wirzenius
00002 # 
00003 # This program is free software; you can redistribute it and/or modify
00004 # it under the terms of the GNU General Public License as published by
00005 # the Free Software Foundation; either version 2 of the License, or
00006 # (at your option) any later version.
00007 # 
00008 # This program is distributed in the hope that it will be useful,
00009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 # GNU General Public License for more details.
00012 # 
00013 # You should have received a copy of the GNU General Public License along
00014 # with this program; if not, write to the Free Software Foundation, Inc.,
00015 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
00016 
00017 
00018 import ConfigParser
00019 import optparse
00020 import os
00021 import re
00022 import sys
00023 
00024 import cliapp
00025 from cliapp.genman import ManpageGenerator
00026 
00027 class Setting(object):
00028 
00029     action = 'store'
00030     type = 'string'
00031     nargs = 1
00032     choices = None
00033 
00034     def __init__(self, names, default, help, metavar=None, group=None):
00035         self.names = names
00036         self.set_value(default)
00037         self.help = help
00038         self.metavar = metavar or self.default_metavar()
00039         self.group = group
00040 
00041     def default_metavar(self):
00042         return None
00043 
00044     def get_value(self):
00045         return self._string_value
00046         
00047     def set_value(self, value):
00048         self._string_value = value
00049         
00050     def call_get_value(self):
00051         return self.get_value()
00052         
00053     def call_set_value(self, value):
00054         self.set_value(value)
00055 
00056     value = property(call_get_value, call_set_value)
00057 
00058     def has_value(self):
00059         return self.value is not None
00060 
00061     def parse_value(self, string):
00062         self.value = string
00063 
00064     def format(self): # pragma: no cover
00065         return str(self.value)
00066 
00067 
00068 class StringSetting(Setting):
00069 
00070     def default_metavar(self):
00071         return self.names[0].upper()
00072 
00073 
00074 class StringListSetting(Setting):
00075 
00076     action = 'append'
00077     
00078     def __init__(self, names, default, help, metavar=None, group=None):
00079         Setting.__init__(self, names, [], help, metavar=metavar, group=group)
00080         self.default = default
00081         self.using_default_value = True
00082 
00083     def default_metavar(self):
00084         return self.names[0].upper()
00085 
00086     def get_value(self):
00087         if self._string_value.strip():
00088             return [s.strip() for s in self._string_value.split(',')]
00089         else:
00090             return self.default
00091         
00092     def set_value(self, strings):
00093         self._string_value = ','.join(strings)
00094         self.using_default_value = False
00095 
00096     def has_value(self):
00097         return self.value != []
00098         
00099     def parse_value(self, string):
00100         self.value = [s.strip() for s in string.split(',')]
00101         
00102     def format(self): # pragma: no cover
00103         return ', '.join(self.value)
00104 
00105 
00106 class ChoiceSetting(Setting):
00107 
00108     type = 'choice'
00109     
00110     def __init__(self, names, choices, help, metavar=None, group=None):
00111         Setting.__init__(self, names, choices[0], help, metavar=metavar,
00112                          group=group)
00113         self.choices = choices
00114 
00115     def default_metavar(self):
00116         return self.names[0].upper()
00117 
00118     
00119 class BooleanSetting(Setting):
00120 
00121     action = 'store_true'
00122     nargs = None
00123     type = None
00124 
00125     _trues = ['yes', 'on', '1', 'true']
00126     _false = 'no'
00127 
00128     def get_value(self):
00129         return self._string_value.lower() in self._trues
00130         
00131     def set_value(self, value):
00132         def is_true():
00133             if value is True or value is False:
00134                 return value
00135             if type(value) in [str, unicode]:
00136                 return value.lower() in self._trues
00137             return value
00138         if is_true():
00139             self._string_value = self._trues[0]
00140         else:
00141             self._string_value = self._false
00142 
00143 
00144 class ByteSizeSetting(Setting):
00145 
00146     def parse_human_size(self, size):
00147         '''Parse a size using suffix into plain bytes.'''
00148         
00149         m = re.match(r'''(?P<number>\d+(\.\d+)?) \s* 
00150                          (?P<unit>k|ki|m|mi|g|gi|t|ti)? b? \s*$''',
00151                      size.lower(), flags=re.X)
00152         if not m:
00153             return 0
00154         else:
00155             number = float(m.group('number'))
00156             unit = m.group('unit')
00157             units = {
00158                 'k': 10**3,
00159                 'm': 10**6,
00160                 'g': 10**9,
00161                 't': 10**12,
00162                 'ki': 2**10,
00163                 'mi': 2**20,
00164                 'gi': 2**30,
00165                 'ti': 2**40,
00166             }
00167             return long(number * units.get(unit, 1))
00168 
00169     def default_metavar(self):
00170         return 'SIZE'
00171 
00172     def get_value(self):
00173         return long(self._string_value)
00174         
00175     def set_value(self, value):
00176         if type(value) == str:
00177             value = self.parse_human_size(value)
00178         self._string_value = str(value)
00179 
00180 
00181 class IntegerSetting(Setting):
00182 
00183     type = 'int'
00184 
00185     def default_metavar(self):
00186         return self.names[0].upper()
00187 
00188     def get_value(self):
00189         return long(self._string_value)
00190         
00191     def set_value(self, value):
00192         self._string_value = str(value)
00193 
00194 
00195 class FormatHelpParagraphs(optparse.IndentedHelpFormatter):
00196 
00197     def _format_text(self, text): # pragma: no cover
00198         '''Like the default, except handle paragraphs.'''
00199         
00200         def format_para(lines):
00201             para = '\n'.join(lines)
00202             return optparse.IndentedHelpFormatter._format_text(self,  para)
00203         
00204         paras = []
00205         cur = []
00206         for line in text.splitlines():
00207             if line.strip():
00208                 cur.append(line)
00209             elif cur:
00210                 paras.append(format_para(cur))
00211                 cur = []
00212         if cur:
00213             paras.append(format_para(cur))
00214         return '\n\n'.join(paras)
00215 
00216 
00217 class Settings(object):
00218 
00219     '''Settings for a cliapp application.
00220 
00221     You probably don't need to create a settings object yourself,
00222     since ``cliapp.Application`` does it for you.
00223     
00224     Settings are read from configuration files, and parsed from the
00225     command line. Every setting has a type, name, and help text,
00226     and may have a default value as well.
00227     
00228     For example::
00229     
00230         settings.boolean(['verbose', 'v'], 'show what is going on')
00231         
00232     This would create a new setting, ``verbose``, with a shorter alias
00233     ``v``. On the command line, the options ``--verbose`` and
00234     ``-v`` would work equally well. There can be any number of aliases. 
00235 
00236     The help text is shown if the user uses ``--help`` or
00237     ``--generate-manpage``.
00238     You can use the ``metavar`` keyword argument to set the name shown
00239     in the generated option lists; the default name is whatever
00240     ``optparse`` decides (i.e., name of option).
00241     
00242     Use ``load_configs`` to read configuration files, and
00243     ``parse_args`` to parse command line arguments.
00244     
00245     The current value of a setting can be accessed by indexing
00246     the settings class::
00247     
00248         settings['verbose']
00249 
00250     The list of configuration files for the appliation is stored
00251     in ``config_files``. Add or remove from the list if you wish.
00252     The files need to exist: those that don't are silently ignored.
00253     
00254     '''
00255 
00256     def __init__(self, progname, version, usage=None, description=None, 
00257                  epilog=None):
00258         self._settingses = dict()
00259         self._canonical_names = list()
00260 
00261         self.version = version
00262         self.progname = progname
00263         self.usage = usage
00264         self.description = description
00265         self.epilog = epilog
00266         
00267         self._add_default_settings()
00268         
00269         self._config_files = None
00270         self._cp = ConfigParser.ConfigParser()
00271 
00272     def _add_default_settings(self):
00273         self.string(['output'], 
00274                     'write output to FILE, instead of standard output',
00275                     metavar='FILE')
00276 
00277         self.string(['log'], 
00278                     'write log entries to FILE (default is to not write log '
00279                         'files at all); use "syslog" to log to system log, '
00280                         'or "none" to disable logging',
00281                     metavar='FILE')
00282         self.choice(['log-level'], 
00283                     ['debug', 'info', 'warning', 'error', 'critical', 'fatal'],
00284                     'log at LEVEL, one of debug, info, warning, '
00285                         'error, critical, fatal (default: %default)',
00286                     metavar='LEVEL')
00287         self.bytesize(['log-max'], 
00288                       'rotate logs larger than SIZE, '
00289                         'zero for never (default: %default)',
00290                       metavar='SIZE', default=0)
00291         self.integer(['log-keep'], 'keep last N logs (%default)',
00292                      metavar='N', default=10)
00293         self.string(['log-mode'], 
00294                     'set permissions of new log files to MODE (octal; '
00295                         'default %default)',
00296                     metavar='MODE', default='0600')
00297 
00298         self.choice(['dump-memory-profile'],
00299                     ['simple', 'none', 'meliae', 'heapy'],
00300                     'make memory profiling dumps using METHOD, which is one '
00301                         'of: none, simple, meliae, or heapy '
00302                         '(default: %default)',
00303                     metavar='METHOD')
00304 
00305     def _add_setting(self, setting):
00306         '''Add a setting to self._cp.'''
00307 
00308         self._canonical_names.append(setting.names[0])
00309         for name in setting.names:
00310             self._settingses[name] = setting
00311 
00312     def string(self, names, help, default='', **kwargs):
00313         '''Add a setting with a string value.'''
00314         self._add_setting(StringSetting(names, default, help, **kwargs))
00315 
00316     def string_list(self, names, help, default=None, **kwargs):
00317         '''Add a setting which have multiple string values.
00318         
00319         An example would be an option that can be given multiple times
00320         on the command line, e.g., "--exclude=foo --exclude=bar".
00321         
00322         '''
00323 
00324         self._add_setting(StringListSetting(names, default or [], help,
00325                                             **kwargs))
00326 
00327     def choice(self, names, possibilities, help, **kwargs):
00328         '''Add a setting which chooses from list of acceptable values.
00329         
00330         An example would be an option to set debugging level to be
00331         one of a set of accepted names: debug, info, warning, etc.
00332         
00333         The default value is the first possibility.
00334         
00335         '''
00336 
00337         self._add_setting(ChoiceSetting(names, possibilities, help, **kwargs))
00338 
00339     def boolean(self, names, help, default=False, **kwargs):
00340         '''Add a setting with a boolean value.'''
00341         self._add_setting(BooleanSetting(names, default, help, **kwargs))
00342 
00343     def bytesize(self, names, help, default=0, **kwargs):
00344         '''Add a setting with a size in bytes.
00345         
00346         The user can use suffixes for kilo/mega/giga/tera/kibi/mibi/gibi/tibi.
00347         
00348         '''
00349         
00350         self._add_setting(ByteSizeSetting(names, default, help, **kwargs))
00351 
00352     def integer(self, names, help, default=0, **kwargs):
00353         '''Add an integer setting.'''
00354         self._add_setting(IntegerSetting(names, default, help, **kwargs))
00355 
00356     def __getitem__(self, name):
00357         return self._settingses[name].value
00358 
00359     def __setitem__(self, name, value):
00360         self._settingses[name].value = value
00361 
00362     def __contains__(self, name):
00363         return name in self._settingses
00364         
00365     def __iter__(self):
00366         '''Iterate over canonical settings names.'''
00367         for name in self._canonical_names:
00368             yield name
00369 
00370     def keys(self):
00371         '''Return canonical settings names.'''
00372         return self._canonical_names[:]
00373 
00374     def require(self, name):
00375         '''Raise exception if setting has not been set.
00376         
00377         Option must have a value, and a default value is OK.
00378         
00379         '''
00380         
00381         if not self._settingses[name].has_value():
00382             raise cliapp.AppException('Setting %s has no value, '
00383                                         'but one is required' % name)
00384         
00385     def _option_names(self, names):
00386         '''Turn setting names into option names.
00387         
00388         Names with a single letter are short options, and get prefixed
00389         with one dash. The rest get prefixed with two dashes.
00390         
00391         '''
00392 
00393         return ['--%s' % name if len(name) > 1 else '-%s' % name
00394                 for name in names]
00395 
00396     def _destname(self, name):
00397         name = '_'.join(name.split('-'))
00398         return name
00399 
00400     def build_parser(self, configs_only=False, arg_synopsis=None,
00401                      cmd_synopsis=None):
00402         '''Build OptionParser for parsing command line.'''
00403 
00404         maybe = lambda func: (lambda *args: None) if configs_only else func
00405 
00406         def getit(x):
00407             if x is None or type(x) in [str, unicode]:
00408                 return x
00409             else:
00410                 return x()
00411         usage = getit(self.usage)
00412         description = getit(self.description)
00413         p = optparse.OptionParser(prog=self.progname, version=self.version,
00414                                   formatter=FormatHelpParagraphs(),
00415                                   usage=usage,
00416                                   description=description,
00417                                   epilog=self.epilog)
00418         
00419         def dump_setting_names(*args): # pragma: no cover
00420             for name in self._canonical_names:
00421                 sys.stdout.write('%s\n' % name)
00422             sys.exit(0)
00423 
00424         p.add_option('--dump-setting-names',
00425                      action='callback',
00426                      nargs=0,
00427                      callback=maybe(dump_setting_names),
00428                      help='write out all names of settings and quit')
00429 
00430         def call_dump_config(*args): # pragma: no cover
00431             self.dump_config(sys.stdout)
00432             sys.exit(0)
00433 
00434         p.add_option('--dump-config',
00435                      action='callback',
00436                      nargs=0,
00437                      callback=maybe(call_dump_config),
00438                      help='write out the entire current configuration')
00439 
00440         def reset_configs(option, opt_str, value, parser):
00441             self.config_files = []
00442 
00443         p.add_option('--no-default-configs',
00444                      action='callback',
00445                      nargs=0,
00446                      callback=reset_configs,
00447                      help='clear list of configuration files to read')
00448 
00449         def append_to_configs(option, opt_str, value, parser):
00450             self.config_files.append(value)
00451 
00452         p.add_option('--config',
00453                      action='callback',
00454                      nargs=1,
00455                      type='string',
00456                      callback=append_to_configs,
00457                      help='add FILE to config files',
00458                      metavar='FILE')
00459 
00460         def list_config_files(*args): # pragma: no cover
00461             for filename in self.config_files:
00462                 print filename
00463             sys.exit(0)
00464 
00465         p.add_option('--list-config-files',
00466                      action='callback',
00467                      nargs=0,
00468                      callback=maybe(list_config_files),
00469                      help='list all possible config files')
00470 
00471         self._arg_synopsis = arg_synopsis
00472         self._cmd_synopsis = cmd_synopsis
00473         p.add_option('--generate-manpage',
00474                      action='callback',
00475                      nargs=1,
00476                      type='string',
00477                      callback=maybe(self._generate_manpage),
00478                      help='fill in manual page TEMPLATE',
00479                      metavar='TEMPLATE')
00480 
00481         def set_value(option, opt_str, value, parser, setting):
00482             if setting.action == 'append':
00483                 if setting.using_default_value:
00484                     setting.value = [value]
00485                 else:
00486                     setting.value += [value]
00487             elif setting.action == 'store_true':
00488                 setting.value = True
00489             else:
00490                 assert setting.action == 'store'
00491                 setting.value = value
00492 
00493         def add_option(obj, s):
00494             option_names = self._option_names(s.names)
00495             obj.add_option(*option_names, 
00496                            action='callback',
00497                            callback=maybe(set_value),
00498                            callback_args=(s,),
00499                            type=s.type,
00500                            nargs=s.nargs,
00501                            choices=s.choices,
00502                            help=s.help,
00503                            metavar=s.metavar)
00504         
00505         for name in self._canonical_names:
00506             s = self._settingses[name]
00507             if s.group is None:
00508                 add_option(p, s)
00509                 p.set_defaults(**{self._destname(name): s.value})
00510 
00511         groups = {}
00512         for name in self._canonical_names:
00513             s = self._settingses[name]
00514             if s.group is not None:
00515                 groups[s.group] = groups.get(s.group, []) + [(name, s)]
00516 
00517         groupnames = sorted(groups.keys())
00518         for groupname in groupnames:
00519             group = optparse.OptionGroup(p, groupname)
00520             p.add_option_group(group)
00521             for name, s in groups[groupname]:
00522                 add_option(group, s)
00523                 p.set_defaults(**{self._destname(name): s.value})
00524 
00525         return p
00526 
00527     def parse_args(self, args, parser=None, suppress_errors=False,
00528                     configs_only=False, arg_synopsis=None,
00529                     cmd_synopsis=None):
00530         '''Parse the command line.
00531         
00532         Return list of non-option arguments. ``args`` would usually
00533         be ``sys.argv[1:]``.
00534         
00535         '''
00536 
00537         p = parser or self.build_parser(configs_only=configs_only,
00538                                         arg_synopsis=arg_synopsis,
00539                                         cmd_synopsis=cmd_synopsis)
00540 
00541         if suppress_errors:
00542             p.error = lambda msg: sys.exit(1)
00543 
00544         options, args = p.parse_args(args)
00545         return args
00546 
00547     @property
00548     def _default_config_files(self):
00549         '''Return list of default config files to read.
00550         
00551         The names of the files are dependent on the name of the program,
00552         as set in the progname attribute.
00553         
00554         The files may or may not exist.
00555         
00556         '''
00557         
00558         configs = []
00559         
00560         configs.append('/etc/%s.conf' % self.progname)
00561         configs += self._listconfs('/etc/%s' % self.progname)
00562         configs.append(os.path.expanduser('~/.%s.conf' % self.progname))
00563         configs += self._listconfs(
00564                         os.path.expanduser('~/.config/%s' % self.progname))
00565         
00566         return configs
00567 
00568     def _listconfs(self, dirname, listdir=os.listdir):
00569         '''Return list of pathnames to config files in dirname.
00570         
00571         Config files are expectd to have names ending in '.conf'.
00572         
00573         If dirname does not exist or is not a directory, 
00574         return empty list.
00575         
00576         '''
00577         
00578         if not os.path.isdir(dirname):
00579             return []
00580 
00581         basenames = listdir(dirname)
00582         basenames.sort(key=lambda s: [ord(c) for c in s])
00583         return [os.path.join(dirname, x)
00584                 for x in basenames
00585                 if x.endswith('.conf')]
00586 
00587     def _get_config_files(self):
00588         if self._config_files is None:
00589             self._config_files = self._default_config_files
00590         return self._config_files
00591 
00592     def _set_config_files(self, config_files):
00593         self._config_files = config_files
00594         
00595     config_files = property(_get_config_files, _set_config_files)
00596 
00597     def set_from_raw_string(self, name, raw_string):
00598         '''Set value of a setting from a raw, unparsed string value.'''
00599         s = self._settingses[name]
00600         s.parse_value(raw_string)
00601         return s
00602 
00603     def load_configs(self, open=open):
00604         '''Load all config files in self.config_files.
00605         
00606         Silently ignore files that do not exist.
00607         
00608         '''
00609 
00610         cp = ConfigParser.ConfigParser()
00611         cp.add_section('config')
00612 
00613         for pathname in self.config_files:
00614             try:
00615                 f = open(pathname)
00616             except IOError:
00617                 pass
00618             else:
00619                 cp.readfp(f)
00620                 f.close()
00621 
00622         for name in cp.options('config'):
00623             value = cp.get('config', name)
00624             s = self.set_from_raw_string(name, value)
00625             if hasattr(s, 'using_default_value'):
00626                 s.using_default_value = True
00627 
00628         # Remember the ConfigParser for use in as_cp later on.
00629         self._cp = cp
00630 
00631     def _generate_manpage(self, o, os, value, p): # pragma: no cover
00632         template = open(value).read()
00633         generator = ManpageGenerator(template, p, self._arg_synopsis,
00634                                      self._cmd_synopsis)
00635         sys.stdout.write(generator.format_template())
00636         sys.exit(0)
00637 
00638     def as_cp(self):
00639         '''Return a ConfigParser instance with current values of settings.'''
00640         cp = ConfigParser.ConfigParser()
00641         cp.add_section('config')
00642         for name in self._canonical_names:
00643             cp.set('config', name, self._settingses[name].format())
00644 
00645         for section in self._cp.sections():
00646             if section != 'config':
00647                 cp.add_section(section)
00648                 for option in self._cp.options(section):
00649                     value = self._cp.get(section, option)
00650                     cp.set(section, option, value)
00651 
00652         return cp
00653 
00654     def dump_config(self, output): # pragma: no cover
00655         cp = self.as_cp()
00656         cp.write(output)
00657