configuration.py 39 KB

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