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