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