Package common :: Module optik_ext
[frames] | no frames]

Source Code for Module common.optik_ext

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