PageRenderTime 58ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/packages/logilab-common/optik_ext.py

https://github.com/mozilla/doozer-lib
Python | 387 lines | 357 code | 0 blank | 30 comment | 2 complexity | 9b4429d47c734feaa745df4491962307 MD5 | raw file
  1. # copyright 2003-2010 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. """Add an abstraction level to transparently import optik classes from optparse
  19. (python >= 2.3) or the optik package.
  20. It also defines three new types for optik/optparse command line parser :
  21. * regexp
  22. argument of this type will be converted using re.compile
  23. * csv
  24. argument of this type will be converted using split(',')
  25. * yn
  26. argument of this type will be true if 'y' or 'yes', false if 'n' or 'no'
  27. * named
  28. argument of this type are in the form <NAME>=<VALUE> or <NAME>:<VALUE>
  29. * password
  30. argument of this type wont be converted but this is used by other tools
  31. such as interactive prompt for configuration to double check value and
  32. use an invisible field
  33. * multiple_choice
  34. same as default "choice" type but multiple choices allowed
  35. * file
  36. argument of this type wont be converted but checked that the given file exists
  37. * color
  38. argument of this type wont be converted but checked its either a
  39. named color or a color specified using hexadecimal notation (preceded by a #)
  40. * time
  41. argument of this type will be converted to a float value in seconds
  42. according to time units (ms, s, min, h, d)
  43. * bytes
  44. argument of this type will be converted to a float value in bytes
  45. according to byte units (b, kb, mb, gb, tb)
  46. """
  47. __docformat__ = "restructuredtext en"
  48. import re
  49. import sys
  50. import time
  51. from copy import copy
  52. from os.path import exists
  53. # python >= 2.3
  54. from optparse import OptionParser as BaseParser, Option as BaseOption, \
  55. OptionGroup, OptionContainer, OptionValueError, OptionError, \
  56. Values, HelpFormatter, NO_DEFAULT, SUPPRESS_HELP
  57. try:
  58. from mx import DateTime
  59. HAS_MX_DATETIME = True
  60. except ImportError:
  61. HAS_MX_DATETIME = False
  62. OPTPARSE_FORMAT_DEFAULT = sys.version_info >= (2, 4)
  63. from logilab.common.textutils import splitstrip
  64. def check_regexp(option, opt, value):
  65. """check a regexp value by trying to compile it
  66. return the compiled regexp
  67. """
  68. if hasattr(value, 'pattern'):
  69. return value
  70. try:
  71. return re.compile(value)
  72. except ValueError:
  73. raise OptionValueError(
  74. "option %s: invalid regexp value: %r" % (opt, value))
  75. def check_csv(option, opt, value):
  76. """check a csv value by trying to split it
  77. return the list of separated values
  78. """
  79. if isinstance(value, (list, tuple)):
  80. return value
  81. try:
  82. return splitstrip(value)
  83. except ValueError:
  84. raise OptionValueError(
  85. "option %s: invalid csv value: %r" % (opt, value))
  86. def check_yn(option, opt, value):
  87. """check a yn value
  88. return true for yes and false for no
  89. """
  90. if isinstance(value, int):
  91. return bool(value)
  92. if value in ('y', 'yes'):
  93. return True
  94. if value in ('n', 'no'):
  95. return False
  96. msg = "option %s: invalid yn value %r, should be in (y, yes, n, no)"
  97. raise OptionValueError(msg % (opt, value))
  98. def check_named(option, opt, value):
  99. """check a named value
  100. return a dictionary containing (name, value) associations
  101. """
  102. if isinstance(value, dict):
  103. return value
  104. values = []
  105. for value in check_csv(option, opt, value):
  106. if value.find('=') != -1:
  107. values.append(value.split('=', 1))
  108. elif value.find(':') != -1:
  109. values.append(value.split(':', 1))
  110. if values:
  111. return dict(values)
  112. msg = "option %s: invalid named value %r, should be <NAME>=<VALUE> or \
  113. <NAME>:<VALUE>"
  114. raise OptionValueError(msg % (opt, value))
  115. def check_password(option, opt, value):
  116. """check a password value (can't be empty)
  117. """
  118. # no actual checking, monkey patch if you want more
  119. return value
  120. def check_file(option, opt, value):
  121. """check a file value
  122. return the filepath
  123. """
  124. if exists(value):
  125. return value
  126. msg = "option %s: file %r does not exist"
  127. raise OptionValueError(msg % (opt, value))
  128. # XXX use python datetime
  129. def check_date(option, opt, value):
  130. """check a file value
  131. return the filepath
  132. """
  133. try:
  134. return DateTime.strptime(value, "%Y/%m/%d")
  135. except DateTime.Error :
  136. raise OptionValueError(
  137. "expected format of %s is yyyy/mm/dd" % opt)
  138. def check_color(option, opt, value):
  139. """check a color value and returns it
  140. /!\ does *not* check color labels (like 'red', 'green'), only
  141. checks hexadecimal forms
  142. """
  143. # Case (1) : color label, we trust the end-user
  144. if re.match('[a-z0-9 ]+$', value, re.I):
  145. return value
  146. # Case (2) : only accepts hexadecimal forms
  147. if re.match('#[a-f0-9]{6}', value, re.I):
  148. return value
  149. # Else : not a color label neither a valid hexadecimal form => error
  150. msg = "option %s: invalid color : %r, should be either hexadecimal \
  151. value or predefined color"
  152. raise OptionValueError(msg % (opt, value))
  153. def check_time(option, opt, value):
  154. from logilab.common.textutils import TIME_UNITS, apply_units
  155. if isinstance(value, (int, long, float)):
  156. return value
  157. return apply_units(value, TIME_UNITS)
  158. def check_bytes(option, opt, value):
  159. from logilab.common.textutils import BYTE_UNITS, apply_units
  160. if hasattr(value, '__int__'):
  161. return value
  162. return apply_units(value, BYTE_UNITS)
  163. import types
  164. class Option(BaseOption):
  165. """override optik.Option to add some new option types
  166. """
  167. TYPES = BaseOption.TYPES + ('regexp', 'csv', 'yn', 'named', 'password',
  168. 'multiple_choice', 'file', 'color',
  169. 'time', 'bytes')
  170. ATTRS = BaseOption.ATTRS + ['hide', 'level']
  171. TYPE_CHECKER = copy(BaseOption.TYPE_CHECKER)
  172. TYPE_CHECKER['regexp'] = check_regexp
  173. TYPE_CHECKER['csv'] = check_csv
  174. TYPE_CHECKER['yn'] = check_yn
  175. TYPE_CHECKER['named'] = check_named
  176. TYPE_CHECKER['multiple_choice'] = check_csv
  177. TYPE_CHECKER['file'] = check_file
  178. TYPE_CHECKER['color'] = check_color
  179. TYPE_CHECKER['password'] = check_password
  180. TYPE_CHECKER['time'] = check_time
  181. TYPE_CHECKER['bytes'] = check_bytes
  182. if HAS_MX_DATETIME:
  183. TYPES += ('date',)
  184. TYPE_CHECKER['date'] = check_date
  185. def __init__(self, *opts, **attrs):
  186. BaseOption.__init__(self, *opts, **attrs)
  187. if hasattr(self, "hide") and self.hide:
  188. self.help = SUPPRESS_HELP
  189. def _check_choice(self):
  190. """FIXME: need to override this due to optik misdesign"""
  191. if self.type in ("choice", "multiple_choice"):
  192. if self.choices is None:
  193. raise OptionError(
  194. "must supply a list of choices for type 'choice'", self)
  195. elif type(self.choices) not in (types.TupleType, types.ListType):
  196. raise OptionError(
  197. "choices must be a list of strings ('%s' supplied)"
  198. % str(type(self.choices)).split("'")[1], self)
  199. elif self.choices is not None:
  200. raise OptionError(
  201. "must not supply choices for type %r" % self.type, self)
  202. BaseOption.CHECK_METHODS[2] = _check_choice
  203. def process(self, opt, value, values, parser):
  204. # First, convert the value(s) to the right type. Howl if any
  205. # value(s) are bogus.
  206. try:
  207. value = self.convert_value(opt, value)
  208. except AttributeError: # py < 2.4
  209. value = self.check_value(opt, value)
  210. if self.type == 'named':
  211. existant = getattr(values, self.dest)
  212. if existant:
  213. existant.update(value)
  214. value = existant
  215. # And then take whatever action is expected of us.
  216. # This is a separate method to make life easier for
  217. # subclasses to add new actions.
  218. return self.take_action(
  219. self.action, self.dest, opt, value, values, parser)
  220. class OptionParser(BaseParser):
  221. """override optik.OptionParser to use our Option class
  222. """
  223. def __init__(self, option_class=Option, *args, **kwargs):
  224. BaseParser.__init__(self, option_class=Option, *args, **kwargs)
  225. def format_option_help(self, formatter=None):
  226. if formatter is None:
  227. formatter = self.formatter
  228. outputlevel = getattr(formatter, 'output_level', 0)
  229. formatter.store_option_strings(self)
  230. result = []
  231. result.append(formatter.format_heading("Options"))
  232. formatter.indent()
  233. if self.option_list:
  234. result.append(OptionContainer.format_option_help(self, formatter))
  235. result.append("\n")
  236. for group in self.option_groups:
  237. if group.level <= outputlevel:
  238. result.append(group.format_help(formatter))
  239. result.append("\n")
  240. formatter.dedent()
  241. # Drop the last "\n", or the header if no options or option groups:
  242. return "".join(result[:-1])
  243. OptionGroup.level = 0
  244. def format_option_help(self, formatter):
  245. result = []
  246. outputlevel = getattr(formatter, 'output_level', 0)
  247. for option in self.option_list:
  248. if getattr(option, 'level', 0) <= outputlevel and not option.help is SUPPRESS_HELP:
  249. result.append(formatter.format_option(option))
  250. return "".join(result)
  251. OptionContainer.format_option_help = format_option_help
  252. OptionContainer.format_option_help = format_option_help
  253. class ManHelpFormatter(HelpFormatter):
  254. """Format help using man pages ROFF format"""
  255. def __init__ (self,
  256. indent_increment=0,
  257. max_help_position=24,
  258. width=79,
  259. short_first=0):
  260. HelpFormatter.__init__ (
  261. self, indent_increment, max_help_position, width, short_first)
  262. def format_heading(self, heading):
  263. return '.SH %s\n' % heading.upper()
  264. def format_description(self, description):
  265. return description
  266. def format_option(self, option):
  267. try:
  268. optstring = option.option_strings
  269. except AttributeError:
  270. optstring = self.format_option_strings(option)
  271. if option.help:
  272. help_text = self.expand_default(option)
  273. help = ' '.join([l.strip() for l in help_text.splitlines()])
  274. else:
  275. help = ''
  276. return '''.IP "%s"
  277. %s
  278. ''' % (optstring, help)
  279. def format_head(self, optparser, pkginfo, section=1):
  280. try:
  281. pgm = optparser._get_prog_name()
  282. except AttributeError:
  283. # py >= 2.4.X (dunno which X exactly, at least 2)
  284. pgm = optparser.get_prog_name()
  285. short_desc = self.format_short_description(pgm, pkginfo.description)
  286. return '%s\n%s\n%s' % (self.format_title(pgm, section), short_desc,
  287. self.format_synopsis(pgm))
  288. def format_title(self, pgm, section):
  289. date = '-'.join([str(num) for num in time.localtime()[:3]])
  290. return '.TH %s %s "%s" %s' % (pgm, section, date, pgm)
  291. def format_short_description(self, pgm, short_desc):
  292. return '''.SH NAME
  293. .B %s
  294. \- %s
  295. ''' % (pgm, short_desc.strip())
  296. def format_synopsis(self, pgm):
  297. return '''.SH SYNOPSIS
  298. .B %s
  299. [
  300. .I OPTIONS
  301. ] [
  302. .I <arguments>
  303. ]
  304. ''' % pgm
  305. def format_long_description(self, pgm, long_desc):
  306. long_desc = '\n'.join([line.lstrip()
  307. for line in long_desc.splitlines()])
  308. long_desc = long_desc.replace('\n.\n', '\n\n')
  309. if long_desc.lower().startswith(pgm):
  310. long_desc = long_desc[len(pgm):]
  311. return '''.SH DESCRIPTION
  312. .B %s
  313. %s
  314. ''' % (pgm, long_desc.strip())
  315. def format_tail(self, pkginfo):
  316. tail = '''.SH SEE ALSO
  317. /usr/share/doc/pythonX.Y-%s/
  318. .SH BUGS
  319. Please report bugs on the project\'s mailing list:
  320. %s
  321. .SH AUTHOR
  322. %s <%s>
  323. ''' % (getattr(pkginfo, 'debian_name', pkginfo.modname),
  324. pkginfo.mailinglist, pkginfo.author, pkginfo.author_email)
  325. if hasattr(pkginfo, "copyright"):
  326. tail += '''
  327. .SH COPYRIGHT
  328. %s
  329. ''' % pkginfo.copyright
  330. return tail
  331. def generate_manpage(optparser, pkginfo, section=1, stream=sys.stdout, level=0):
  332. """generate a man page from an optik parser"""
  333. formatter = ManHelpFormatter()
  334. formatter.output_level = level
  335. formatter.parser = optparser
  336. print >> stream, formatter.format_head(optparser, pkginfo, section)
  337. print >> stream, optparser.format_option_help(formatter)
  338. print >> stream, formatter.format_tail(pkginfo)
  339. __all__ = ('OptionParser', 'Option', 'OptionGroup', 'OptionValueError',
  340. 'Values')