Index: third_party/logilab/common/optik_ext.py |
diff --git a/third_party/logilab/common/optik_ext.py b/third_party/logilab/common/optik_ext.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..39bbe18d1598522d33c4ba8f54180ea74a260483 |
--- /dev/null |
+++ b/third_party/logilab/common/optik_ext.py |
@@ -0,0 +1,397 @@ |
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
+# |
+# This file is part of logilab-common. |
+# |
+# logilab-common is free software: you can redistribute it and/or modify it under |
+# the terms of the GNU Lesser General Public License as published by the Free |
+# Software Foundation, either version 2.1 of the License, or (at your option) any |
+# later version. |
+# |
+# logilab-common is distributed in the hope that it will be useful, but WITHOUT |
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
+# details. |
+# |
+# You should have received a copy of the GNU Lesser General Public License along |
+# with logilab-common. If not, see <http://www.gnu.org/licenses/>. |
+"""Add an abstraction level to transparently import optik classes from optparse |
+(python >= 2.3) or the optik package. |
+ |
+It also defines three new types for optik/optparse command line parser : |
+ |
+ * regexp |
+ argument of this type will be converted using re.compile |
+ * csv |
+ argument of this type will be converted using split(',') |
+ * yn |
+ argument of this type will be true if 'y' or 'yes', false if 'n' or 'no' |
+ * named |
+ argument of this type are in the form <NAME>=<VALUE> or <NAME>:<VALUE> |
+ * password |
+ argument of this type wont be converted but this is used by other tools |
+ such as interactive prompt for configuration to double check value and |
+ use an invisible field |
+ * multiple_choice |
+ same as default "choice" type but multiple choices allowed |
+ * file |
+ argument of this type wont be converted but checked that the given file exists |
+ * color |
+ argument of this type wont be converted but checked its either a |
+ named color or a color specified using hexadecimal notation (preceded by a #) |
+ * time |
+ argument of this type will be converted to a float value in seconds |
+ according to time units (ms, s, min, h, d) |
+ * bytes |
+ argument of this type will be converted to a float value in bytes |
+ according to byte units (b, kb, mb, gb, tb) |
+""" |
+__docformat__ = "restructuredtext en" |
+ |
+import re |
+import sys |
+import time |
+from copy import copy |
+from os.path import exists |
+ |
+# python >= 2.3 |
+from optparse import OptionParser as BaseParser, Option as BaseOption, \ |
+ OptionGroup, OptionContainer, OptionValueError, OptionError, \ |
+ Values, HelpFormatter, NO_DEFAULT, SUPPRESS_HELP |
+ |
+try: |
+ from mx import DateTime |
+ HAS_MX_DATETIME = True |
+except ImportError: |
+ HAS_MX_DATETIME = False |
+ |
+ |
+OPTPARSE_FORMAT_DEFAULT = sys.version_info >= (2, 4) |
+ |
+from logilab.common.textutils import splitstrip |
+ |
+def check_regexp(option, opt, value): |
+ """check a regexp value by trying to compile it |
+ return the compiled regexp |
+ """ |
+ if hasattr(value, 'pattern'): |
+ return value |
+ try: |
+ return re.compile(value) |
+ except ValueError: |
+ raise OptionValueError( |
+ "option %s: invalid regexp value: %r" % (opt, value)) |
+ |
+def check_csv(option, opt, value): |
+ """check a csv value by trying to split it |
+ return the list of separated values |
+ """ |
+ if isinstance(value, (list, tuple)): |
+ return value |
+ try: |
+ return splitstrip(value) |
+ except ValueError: |
+ raise OptionValueError( |
+ "option %s: invalid csv value: %r" % (opt, value)) |
+ |
+def check_yn(option, opt, value): |
+ """check a yn value |
+ return true for yes and false for no |
+ """ |
+ if isinstance(value, int): |
+ return bool(value) |
+ if value in ('y', 'yes'): |
+ return True |
+ if value in ('n', 'no'): |
+ return False |
+ msg = "option %s: invalid yn value %r, should be in (y, yes, n, no)" |
+ raise OptionValueError(msg % (opt, value)) |
+ |
+def check_named(option, opt, value): |
+ """check a named value |
+ return a dictionary containing (name, value) associations |
+ """ |
+ if isinstance(value, dict): |
+ return value |
+ values = [] |
+ for value in check_csv(option, opt, value): |
+ if value.find('=') != -1: |
+ values.append(value.split('=', 1)) |
+ elif value.find(':') != -1: |
+ values.append(value.split(':', 1)) |
+ if values: |
+ return dict(values) |
+ msg = "option %s: invalid named value %r, should be <NAME>=<VALUE> or \ |
+<NAME>:<VALUE>" |
+ raise OptionValueError(msg % (opt, value)) |
+ |
+def check_password(option, opt, value): |
+ """check a password value (can't be empty) |
+ """ |
+ # no actual checking, monkey patch if you want more |
+ return value |
+ |
+def check_file(option, opt, value): |
+ """check a file value |
+ return the filepath |
+ """ |
+ if exists(value): |
+ return value |
+ msg = "option %s: file %r does not exist" |
+ raise OptionValueError(msg % (opt, value)) |
+ |
+# XXX use python datetime |
+def check_date(option, opt, value): |
+ """check a file value |
+ return the filepath |
+ """ |
+ try: |
+ return DateTime.strptime(value, "%Y/%m/%d") |
+ except DateTime.Error : |
+ raise OptionValueError( |
+ "expected format of %s is yyyy/mm/dd" % opt) |
+ |
+def check_color(option, opt, value): |
+ """check a color value and returns it |
+ /!\ does *not* check color labels (like 'red', 'green'), only |
+ checks hexadecimal forms |
+ """ |
+ # Case (1) : color label, we trust the end-user |
+ if re.match('[a-z0-9 ]+$', value, re.I): |
+ return value |
+ # Case (2) : only accepts hexadecimal forms |
+ if re.match('#[a-f0-9]{6}', value, re.I): |
+ return value |
+ # Else : not a color label neither a valid hexadecimal form => error |
+ msg = "option %s: invalid color : %r, should be either hexadecimal \ |
+ value or predefined color" |
+ raise OptionValueError(msg % (opt, value)) |
+ |
+def check_time(option, opt, value): |
+ from logilab.common.textutils import TIME_UNITS, apply_units |
+ if isinstance(value, (int, long, float)): |
+ return value |
+ return apply_units(value, TIME_UNITS) |
+ |
+def check_bytes(option, opt, value): |
+ from logilab.common.textutils import BYTE_UNITS, apply_units |
+ if hasattr(value, '__int__'): |
+ return value |
+ return apply_units(value, BYTE_UNITS) |
+ |
+import types |
+ |
+class Option(BaseOption): |
+ """override optik.Option to add some new option types |
+ """ |
+ TYPES = BaseOption.TYPES + ('regexp', 'csv', 'yn', 'named', 'password', |
+ 'multiple_choice', 'file', 'color', |
+ 'time', 'bytes') |
+ ATTRS = BaseOption.ATTRS + ['hide', 'level'] |
+ TYPE_CHECKER = copy(BaseOption.TYPE_CHECKER) |
+ TYPE_CHECKER['regexp'] = check_regexp |
+ TYPE_CHECKER['csv'] = check_csv |
+ TYPE_CHECKER['yn'] = check_yn |
+ TYPE_CHECKER['named'] = check_named |
+ TYPE_CHECKER['multiple_choice'] = check_csv |
+ TYPE_CHECKER['file'] = check_file |
+ TYPE_CHECKER['color'] = check_color |
+ TYPE_CHECKER['password'] = check_password |
+ TYPE_CHECKER['time'] = check_time |
+ TYPE_CHECKER['bytes'] = check_bytes |
+ if HAS_MX_DATETIME: |
+ TYPES += ('date',) |
+ TYPE_CHECKER['date'] = check_date |
+ |
+ def __init__(self, *opts, **attrs): |
+ BaseOption.__init__(self, *opts, **attrs) |
+ if hasattr(self, "hide") and self.hide: |
+ self.help = SUPPRESS_HELP |
+ |
+ def _check_choice(self): |
+ """FIXME: need to override this due to optik misdesign""" |
+ if self.type in ("choice", "multiple_choice"): |
+ if self.choices is None: |
+ raise OptionError( |
+ "must supply a list of choices for type 'choice'", self) |
+ elif type(self.choices) not in (types.TupleType, types.ListType): |
+ raise OptionError( |
+ "choices must be a list of strings ('%s' supplied)" |
+ % str(type(self.choices)).split("'")[1], self) |
+ elif self.choices is not None: |
+ raise OptionError( |
+ "must not supply choices for type %r" % self.type, self) |
+ BaseOption.CHECK_METHODS[2] = _check_choice |
+ |
+ |
+ def process(self, opt, value, values, parser): |
+ # First, convert the value(s) to the right type. Howl if any |
+ # value(s) are bogus. |
+ try: |
+ value = self.convert_value(opt, value) |
+ except AttributeError: # py < 2.4 |
+ value = self.check_value(opt, value) |
+ if self.type == 'named': |
+ existant = getattr(values, self.dest) |
+ if existant: |
+ existant.update(value) |
+ value = existant |
+ # And then take whatever action is expected of us. |
+ # This is a separate method to make life easier for |
+ # subclasses to add new actions. |
+ return self.take_action( |
+ self.action, self.dest, opt, value, values, parser) |
+ |
+ |
+class OptionParser(BaseParser): |
+ """override optik.OptionParser to use our Option class |
+ """ |
+ def __init__(self, option_class=Option, *args, **kwargs): |
+ BaseParser.__init__(self, option_class=Option, *args, **kwargs) |
+ |
+ def format_option_help(self, formatter=None): |
+ if formatter is None: |
+ formatter = self.formatter |
+ outputlevel = getattr(formatter, 'output_level', 0) |
+ formatter.store_option_strings(self) |
+ result = [] |
+ result.append(formatter.format_heading("Options")) |
+ formatter.indent() |
+ if self.option_list: |
+ result.append(OptionContainer.format_option_help(self, formatter)) |
+ result.append("\n") |
+ for group in self.option_groups: |
+ if group.level <= outputlevel and ( |
+ group.description or level_options(group, outputlevel)): |
+ result.append(group.format_help(formatter)) |
+ result.append("\n") |
+ formatter.dedent() |
+ # Drop the last "\n", or the header if no options or option groups: |
+ return "".join(result[:-1]) |
+ |
+ |
+OptionGroup.level = 0 |
+ |
+def level_options(group, outputlevel): |
+ return [option for option in group.option_list |
+ if (getattr(option, 'level', 0) or 0) <= outputlevel |
+ and not option.help is SUPPRESS_HELP] |
+ |
+def format_option_help(self, formatter): |
+ result = [] |
+ outputlevel = getattr(formatter, 'output_level', 0) or 0 |
+ for option in level_options(self, outputlevel): |
+ result.append(formatter.format_option(option)) |
+ return "".join(result) |
+OptionContainer.format_option_help = format_option_help |
+ |
+ |
+class ManHelpFormatter(HelpFormatter): |
+ """Format help using man pages ROFF format""" |
+ |
+ def __init__ (self, |
+ indent_increment=0, |
+ max_help_position=24, |
+ width=79, |
+ short_first=0): |
+ HelpFormatter.__init__ ( |
+ self, indent_increment, max_help_position, width, short_first) |
+ |
+ def format_heading(self, heading): |
+ return '.SH %s\n' % heading.upper() |
+ |
+ def format_description(self, description): |
+ return description |
+ |
+ def format_option(self, option): |
+ try: |
+ optstring = option.option_strings |
+ except AttributeError: |
+ optstring = self.format_option_strings(option) |
+ if option.help: |
+ help_text = self.expand_default(option) |
+ help = ' '.join([l.strip() for l in help_text.splitlines()]) |
+ else: |
+ help = '' |
+ return '''.IP "%s" |
+%s |
+''' % (optstring, help) |
+ |
+ def format_head(self, optparser, pkginfo, section=1): |
+ long_desc = "" |
+ try: |
+ pgm = optparser._get_prog_name() |
+ except AttributeError: |
+ # py >= 2.4.X (dunno which X exactly, at least 2) |
+ pgm = optparser.get_prog_name() |
+ short_desc = self.format_short_description(pgm, pkginfo.description) |
+ if hasattr(pkginfo, "long_desc"): |
+ long_desc = self.format_long_description(pgm, pkginfo.long_desc) |
+ return '%s\n%s\n%s\n%s' % (self.format_title(pgm, section), |
+ short_desc, self.format_synopsis(pgm), |
+ long_desc) |
+ |
+ def format_title(self, pgm, section): |
+ date = '-'.join([str(num) for num in time.localtime()[:3]]) |
+ return '.TH %s %s "%s" %s' % (pgm, section, date, pgm) |
+ |
+ def format_short_description(self, pgm, short_desc): |
+ return '''.SH NAME |
+.B %s |
+\- %s |
+''' % (pgm, short_desc.strip()) |
+ |
+ def format_synopsis(self, pgm): |
+ return '''.SH SYNOPSIS |
+.B %s |
+[ |
+.I OPTIONS |
+] [ |
+.I <arguments> |
+] |
+''' % pgm |
+ |
+ def format_long_description(self, pgm, long_desc): |
+ long_desc = '\n'.join([line.lstrip() |
+ for line in long_desc.splitlines()]) |
+ long_desc = long_desc.replace('\n.\n', '\n\n') |
+ if long_desc.lower().startswith(pgm): |
+ long_desc = long_desc[len(pgm):] |
+ return '''.SH DESCRIPTION |
+.B %s |
+%s |
+''' % (pgm, long_desc.strip()) |
+ |
+ def format_tail(self, pkginfo): |
+ tail = '''.SH SEE ALSO |
+/usr/share/doc/pythonX.Y-%s/ |
+ |
+.SH BUGS |
+Please report bugs on the project\'s mailing list: |
+%s |
+ |
+.SH AUTHOR |
+%s <%s> |
+''' % (getattr(pkginfo, 'debian_name', pkginfo.modname), |
+ pkginfo.mailinglist, pkginfo.author, pkginfo.author_email) |
+ |
+ if hasattr(pkginfo, "copyright"): |
+ tail += ''' |
+.SH COPYRIGHT |
+%s |
+''' % pkginfo.copyright |
+ |
+ return tail |
+ |
+def generate_manpage(optparser, pkginfo, section=1, stream=sys.stdout, level=0): |
+ """generate a man page from an optik parser""" |
+ formatter = ManHelpFormatter() |
+ formatter.output_level = level |
+ formatter.parser = optparser |
+ print >> stream, formatter.format_head(optparser, pkginfo, section) |
+ print >> stream, optparser.format_option_help(formatter) |
+ print >> stream, formatter.format_tail(pkginfo) |
+ |
+ |
+__all__ = ('OptionParser', 'Option', 'OptionGroup', 'OptionValueError', |
+ 'Values') |