| 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 |