diff options
Diffstat (limited to 'logilab/common/optik_ext.py')
-rw-r--r-- | logilab/common/optik_ext.py | 245 |
1 files changed, 155 insertions, 90 deletions
diff --git a/logilab/common/optik_ext.py b/logilab/common/optik_ext.py index 11e2155..3f321b5 100644 --- a/logilab/common/optik_ext.py +++ b/logilab/common/optik_ext.py @@ -62,33 +62,44 @@ from optparse import Values, IndentedHelpFormatter, OptionGroup from _io import StringIO # python >= 2.3 -from optparse import OptionParser as BaseParser, Option as BaseOption, \ - OptionGroup, OptionContainer, OptionValueError, OptionError, \ - Values, HelpFormatter, NO_DEFAULT, SUPPRESS_HELP +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 -from logilab.common.textutils import splitstrip, TIME_UNITS, BYTE_UNITS, \ - apply_units +from logilab.common.textutils import splitstrip, TIME_UNITS, BYTE_UNITS, apply_units def check_regexp(option, opt, value): """check a regexp value by trying to compile it return the compiled regexp """ - if hasattr(value, 'pattern'): + if hasattr(value, "pattern"): return value try: return re.compile(value) except ValueError: - raise OptionValueError( - "option %s: invalid regexp value: %r" % (opt, value)) + raise OptionValueError("option %s: invalid regexp value: %r" % (opt, value)) + -def check_csv(option: Optional['Option'], opt: str, value: Union[List[str], Tuple[str, ...], str]) -> Union[List[str], Tuple[str, ...]]: +def check_csv( + option: Optional["Option"], opt: str, value: Union[List[str], Tuple[str, ...], str] +) -> Union[List[str], Tuple[str, ...]]: """check a csv value by trying to split it return the list of separated values """ @@ -97,23 +108,26 @@ def check_csv(option: Optional['Option'], opt: str, value: Union[List[str], Tupl try: return splitstrip(value) except ValueError: - raise OptionValueError( - "option %s: invalid csv value: %r" % (opt, value)) + raise OptionValueError("option %s: invalid csv value: %r" % (opt, value)) + -def check_yn(option: Optional['Option'], opt: str, value: Union[bool, str]) -> bool: +def check_yn(option: Optional["Option"], opt: str, value: Union[bool, str]) -> bool: """check a yn value return true for yes and false for no """ if isinstance(value, int): return bool(value) - if value in ('y', 'yes'): + if value in ("y", "yes"): return True - if value in ('n', 'no'): + 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: Optional[Any], opt: str, value: Union[Dict[str, str], str]) -> Dict[str, str]: + +def check_named( + option: Optional[Any], opt: str, value: Union[Dict[str, str], str] +) -> Dict[str, str]: """check a named value return a dictionary containing (name, value) associations """ @@ -124,22 +138,24 @@ def check_named(option: Optional[Any], opt: str, value: Union[Dict[str, str], st # mypy: Argument 1 to "append" of "list" has incompatible type "List[str]"; # mypy: expected "Tuple[str, str]" # we know that the split will give a 2 items list - if value.find('=') != -1: - values.append(value.split('=', 1)) # type: ignore - elif value.find(':') != -1: - values.append(value.split(':', 1)) # type: ignore + if value.find("=") != -1: + values.append(value.split("=", 1)) # type: ignore + elif value.find(":") != -1: + values.append(value.split(":", 1)) # type: ignore 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 @@ -149,6 +165,7 @@ def check_file(option, opt, 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 @@ -156,9 +173,9 @@ def check_date(option, opt, value): """ try: return DateTime.strptime(value, "%Y/%m/%d") - except DateTime.Error : - raise OptionValueError( - "expected format of %s is yyyy/mm/dd" % opt) + 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 @@ -166,23 +183,25 @@ def check_color(option, opt, value): checks hexadecimal forms """ # Case (1) : color label, we trust the end-user - if re.match('[a-z0-9 ]+$', value, re.I): + 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): + 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): if isinstance(value, (int, float)): return value return apply_units(value, TIME_UNITS) -def check_bytes(option: Optional['Option'], opt: str, value: Any) -> int: - if hasattr(value, '__int__'): + +def check_bytes(option: Optional["Option"], opt: str, value: Any) -> int: + if hasattr(value, "__int__"): return value # mypy: Incompatible return value type (got "Union[float, int]", expected "int") # we force "int" using "final=int" @@ -192,24 +211,34 @@ def check_bytes(option: Optional['Option'], opt: str, value: Any) -> int: 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'] + + 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 + 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 + TYPES += ("date",) + TYPE_CHECKER["date"] = check_date def __init__(self, *opts: str, **attrs: Any) -> None: BaseOption.__init__(self, *opts, **attrs) @@ -224,15 +253,16 @@ class Option(BaseOption): # mypy: "Option" has no attribute "choices" # we know that option of this type has this attribute if self.choices is None: # type: ignore - raise OptionError( - "must supply a list of choices for type 'choice'", self) + raise OptionError("must supply a list of choices for type 'choice'", self) elif not isinstance(self.choices, (tuple, list)): # type: ignore raise OptionError( "choices must be a list of strings ('%s' supplied)" - % str(type(self.choices)).split("'")[1], self) # type: ignore + % str(type(self.choices)).split("'")[1], + self, + ) # type: ignore elif self.choices is not None: # type: ignore - raise OptionError( - "must not supply choices for type %r" % self.type, self) + raise OptionError("must not supply choices for type %r" % self.type, self) + # mypy: Unsupported target for indexed assignment # black magic? BaseOption.CHECK_METHODS[2] = _check_choice # type: ignore @@ -241,7 +271,7 @@ class Option(BaseOption): # First, convert the value(s) to the right type. Howl if any # value(s) are bogus. value = self.convert_value(opt, value) - if self.type == 'named': + if self.type == "named": assert self.dest is not None existant = getattr(values, self.dest) if existant: @@ -253,13 +283,13 @@ class Option(BaseOption): # mypy: Argument 2 to "take_action" of "Option" has incompatible type "Optional[str]"; # mypy: expected "str" # is it ok? - return self.take_action( - self.action, self.dest, opt, value, values, parser) # type: ignore + return self.take_action(self.action, self.dest, opt, value, values, parser) # type: ignore class OptionParser(BaseParser): """override optik.OptionParser to use our Option class """ + def __init__(self, option_class: type = Option, *args: Any, **kwargs: Any) -> None: # mypy: Argument "option_class" to "__init__" of "OptionParser" has incompatible type # mypy: "type"; expected "Option" @@ -269,7 +299,7 @@ class OptionParser(BaseParser): def format_option_help(self, formatter: Optional[HelpFormatter] = None) -> str: if formatter is None: formatter = self.formatter - outputlevel = getattr(formatter, 'output_level', 0) + outputlevel = getattr(formatter, "output_level", 0) formatter.store_option_strings(self) result = [] result.append(formatter.format_heading("Options")) @@ -281,7 +311,8 @@ class OptionParser(BaseParser): # mypy: "OptionParser" has no attribute "level" # but it has one no? if group.level <= outputlevel and ( # type: ignore - group.description or level_options(group, outputlevel)): + group.description or level_options(group, outputlevel) + ): result.append(group.format_help(formatter)) result.append("\n") formatter.dedent() @@ -293,19 +324,25 @@ class OptionParser(BaseParser): # monkeypatching OptionGroup.level = 0 # type: ignore + def level_options(group: BaseParser, outputlevel: int) -> List[BaseOption]: # mypy: "Option" has no attribute "help" # but it does - return [option for option in group.option_list - if (getattr(option, 'level', 0) or 0) <= outputlevel - and not option.help is SUPPRESS_HELP] # type: ignore + return [ + option + for option in group.option_list + if (getattr(option, "level", 0) or 0) <= outputlevel and not option.help is SUPPRESS_HELP + ] # type: ignore + def format_option_help(self, formatter): result = [] - outputlevel = getattr(formatter, 'output_level', 0) or 0 + outputlevel = getattr(formatter, "output_level", 0) or 0 for option in level_options(self, outputlevel): result.append(formatter.format_option(option)) return "".join(result) + + # mypy error: Cannot assign to a method # but we still do it because magic OptionContainer.format_option_help = format_option_help # type: ignore @@ -314,16 +351,17 @@ OptionContainer.format_option_help = format_option_help # type: ignore class ManHelpFormatter(HelpFormatter): """Format help using man pages ROFF format""" - def __init__ (self, - indent_increment: int = 0, - max_help_position: int = 24, - width: int = 79, - short_first: int = 0) -> None: - HelpFormatter.__init__ ( - self, indent_increment, max_help_position, width, short_first) + def __init__( + self, + indent_increment: int = 0, + max_help_position: int = 24, + width: int = 79, + short_first: int = 0, + ) -> None: + HelpFormatter.__init__(self, indent_increment, max_help_position, width, short_first) def format_heading(self, heading: str) -> str: - return '.SH %s\n' % heading.upper() + return ".SH %s\n" % heading.upper() def format_description(self, description): return description @@ -342,12 +380,15 @@ class ManHelpFormatter(HelpFormatter): # mypy: "OptionParser"; expected "Option" # it still works? help_text = self.expand_default(option) # type: ignore - help = ' '.join([l.strip() for l in help_text.splitlines()]) + help = " ".join([l.strip() for l in help_text.splitlines()]) else: - help = '' - return '''.IP "%s" + help = "" + return """.IP "%s" %s -''' % (optstring, help) +""" % ( + optstring, + help, + ) def format_head(self, optparser: OptionParser, pkginfo: attrdict, section: int = 1) -> str: long_desc = "" @@ -355,43 +396,54 @@ class ManHelpFormatter(HelpFormatter): 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) + 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: str, section: int) -> str: - date = '-'.join([str(num) for num in time.localtime()[:3]]) + 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: str, short_desc: str) -> str: - return '''.SH NAME + return """.SH NAME .B %s \- %s -''' % (pgm, short_desc.strip()) +""" % ( + pgm, + short_desc.strip(), + ) def format_synopsis(self, pgm: str) -> str: - return '''.SH SYNOPSIS + return ( + """.SH SYNOPSIS .B %s [ .I OPTIONS ] [ .I <arguments> ] -''' % pgm +""" + % 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') + 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 + long_desc = long_desc[len(pgm) :] + return """.SH DESCRIPTION .B %s %s -''' % (pgm, long_desc.strip()) +""" % ( + pgm, + long_desc.strip(), + ) def format_tail(self, pkginfo: attrdict) -> str: - tail = '''.SH SEE ALSO + tail = """.SH SEE ALSO /usr/share/doc/pythonX.Y-%s/ .SH BUGS @@ -400,18 +452,32 @@ Please report bugs on the project\'s mailing list: .SH AUTHOR %s <%s> -''' % (getattr(pkginfo, 'debian_name', pkginfo.modname), - pkginfo.mailinglist, pkginfo.author, pkginfo.author_email) +""" % ( + getattr(pkginfo, "debian_name", pkginfo.modname), + pkginfo.mailinglist, + pkginfo.author, + pkginfo.author_email, + ) if hasattr(pkginfo, "copyright"): - tail += ''' + tail += ( + """ .SH COPYRIGHT %s -''' % pkginfo.copyright +""" + % pkginfo.copyright + ) return tail -def generate_manpage(optparser: OptionParser, pkginfo: attrdict, section: int = 1, stream: StringIO = sys.stdout, level: int = 0) -> None: + +def generate_manpage( + optparser: OptionParser, + pkginfo: attrdict, + section: int = 1, + stream: StringIO = sys.stdout, + level: int = 0, +) -> None: """generate a man page from an optik parser""" formatter = ManHelpFormatter() # mypy: "ManHelpFormatter" has no attribute "output_level" @@ -423,5 +489,4 @@ def generate_manpage(optparser: OptionParser, pkginfo: attrdict, section: int = print(formatter.format_tail(pkginfo), file=stream) -__all__ = ('OptionParser', 'Option', 'OptionGroup', 'OptionValueError', - 'Values') +__all__ = ("OptionParser", "Option", "OptionGroup", "OptionValueError", "Values") |