diff options
Diffstat (limited to 'logilab/common/optik_ext.py')
-rw-r--r-- | logilab/common/optik_ext.py | 133 |
1 files changed, 84 insertions, 49 deletions
diff --git a/logilab/common/optik_ext.py b/logilab/common/optik_ext.py index 07365a7..11e2155 100644 --- a/logilab/common/optik_ext.py +++ b/logilab/common/optik_ext.py @@ -55,6 +55,11 @@ import sys import time from copy import copy from os.path import exists +from logilab.common import attrdict + +from typing import Any, Union, List, Optional, Tuple, Dict +from optparse import Values, IndentedHelpFormatter, OptionGroup +from _io import StringIO # python >= 2.3 from optparse import OptionParser as BaseParser, Option as BaseOption, \ @@ -83,7 +88,7 @@ def check_regexp(option, opt, value): raise OptionValueError( "option %s: invalid regexp value: %r" % (opt, value)) -def check_csv(option, opt, value): +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 """ @@ -95,7 +100,7 @@ def check_csv(option, opt, value): raise OptionValueError( "option %s: invalid csv value: %r" % (opt, value)) -def check_yn(option, opt, value): +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 """ @@ -108,18 +113,21 @@ def check_yn(option, opt, value): 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): +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 """ if isinstance(value, dict): return value - values = [] + values: List[Tuple[str, str]] = [] for value in check_csv(option, opt, value): + # 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)) + values.append(value.split('=', 1)) # type: ignore elif value.find(':') != -1: - values.append(value.split(':', 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 \ @@ -173,10 +181,12 @@ def check_time(option, opt, value): return value return apply_units(value, TIME_UNITS) -def check_bytes(option, opt, value): +def check_bytes(option: Optional['Option'], opt: str, value: Any) -> int: if hasattr(value, '__int__'): return value - return apply_units(value, BYTE_UNITS, final=int) + # mypy: Incompatible return value type (got "Union[float, int]", expected "int") + # we force "int" using "final=int" + return apply_units(value, BYTE_UNITS, final=int) # type: ignore class Option(BaseOption): @@ -201,50 +211,62 @@ class Option(BaseOption): TYPES += ('date',) TYPE_CHECKER['date'] = check_date - def __init__(self, *opts, **attrs): + def __init__(self, *opts: str, **attrs: Any) -> None: BaseOption.__init__(self, *opts, **attrs) - if hasattr(self, "hide") and self.hide: + # mypy: "Option" has no attribute "hide" + # we test that in the if + if hasattr(self, "hide") and self.hide: # type: ignore self.help = SUPPRESS_HELP - def _check_choice(self): + def _check_choice(self) -> None: """FIXME: need to override this due to optik misdesign""" if self.type in ("choice", "multiple_choice"): - if self.choices is None: + # 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) - elif not isinstance(self.choices, (tuple, list)): + 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) - elif self.choices is not None: + % 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) - BaseOption.CHECK_METHODS[2] = _check_choice - + # mypy: Unsupported target for indexed assignment + # black magic? + BaseOption.CHECK_METHODS[2] = _check_choice # type: ignore - def process(self, opt, value, values, parser): + def process(self, opt: str, value: str, values: Values, parser: BaseParser) -> int: # 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': + assert self.dest is not None existant = getattr(values, self.dest) if existant: existant.update(value) value = existant - # And then take whatever action is expected of us. + # And then take whatever action is expected of us. # This is a separate method to make life easier for # subclasses to add new actions. + # 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) + 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=Option, *args, **kwargs): - BaseParser.__init__(self, option_class=Option, *args, **kwargs) + 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" + # mypy is doing really weird things with *args/**kwargs and looks buggy + BaseParser.__init__(self, option_class=option_class, *args, **kwargs) # type: ignore - def format_option_help(self, formatter=None): + def format_option_help(self, formatter: Optional[HelpFormatter] = None) -> str: if formatter is None: formatter = self.formatter outputlevel = getattr(formatter, 'output_level', 0) @@ -256,7 +278,9 @@ class OptionParser(BaseParser): result.append(OptionContainer.format_option_help(self, formatter)) result.append("\n") for group in self.option_groups: - if group.level <= outputlevel and ( + # 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)): result.append(group.format_help(formatter)) result.append("\n") @@ -265,12 +289,16 @@ class OptionParser(BaseParser): return "".join(result[:-1]) -OptionGroup.level = 0 +# mypy error: error: "Type[OptionGroup]" has no attribute "level" +# monkeypatching +OptionGroup.level = 0 # type: ignore -def level_options(group, outputlevel): +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] + and not option.help is SUPPRESS_HELP] # type: ignore def format_option_help(self, formatter): result = [] @@ -278,33 +306,42 @@ def format_option_help(self, formatter): for option in level_options(self, outputlevel): result.append(formatter.format_option(option)) return "".join(result) -OptionContainer.format_option_help = format_option_help +# mypy error: Cannot assign to a method +# but we still do it because magic +OptionContainer.format_option_help = format_option_help # type: ignore 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): + 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): + def format_heading(self, heading: str) -> str: return '.SH %s\n' % heading.upper() def format_description(self, description): return description - def format_option(self, option): + def format_option(self, option: BaseParser) -> str: try: - optstring = option.option_strings + # mypy: "Option" has no attribute "option_strings" + # we handle if it doesn't + optstring = option.option_strings # type: ignore except AttributeError: optstring = self.format_option_strings(option) - if option.help: - help_text = self.expand_default(option) + # mypy: "OptionParser" has no attribute "help" + # it does + if option.help: # type: ignore + # mypy: Argument 1 to "expand_default" of "HelpFormatter" has incompatible type + # 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()]) else: help = '' @@ -312,13 +349,9 @@ class ManHelpFormatter(HelpFormatter): %s ''' % (optstring, help) - def format_head(self, optparser, pkginfo, section=1): + def format_head(self, optparser: OptionParser, pkginfo: attrdict, section: int = 1) -> str: 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() + 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) @@ -326,17 +359,17 @@ class ManHelpFormatter(HelpFormatter): short_desc, self.format_synopsis(pgm), long_desc) - def format_title(self, pgm, section): + def format_title(self, pgm: str, section: int) -> str: 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): + def format_short_description(self, pgm: str, short_desc: str) -> str: return '''.SH NAME .B %s \- %s ''' % (pgm, short_desc.strip()) - def format_synopsis(self, pgm): + def format_synopsis(self, pgm: str) -> str: return '''.SH SYNOPSIS .B %s [ @@ -357,7 +390,7 @@ class ManHelpFormatter(HelpFormatter): %s ''' % (pgm, long_desc.strip()) - def format_tail(self, pkginfo): + def format_tail(self, pkginfo: attrdict) -> str: tail = '''.SH SEE ALSO /usr/share/doc/pythonX.Y-%s/ @@ -378,10 +411,12 @@ Please report bugs on the project\'s mailing list: return tail -def generate_manpage(optparser, pkginfo, section=1, stream=sys.stdout, level=0): +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() - formatter.output_level = level + # mypy: "ManHelpFormatter" has no attribute "output_level" + # dynamic attribute? + formatter.output_level = level # type: ignore formatter.parser = optparser print(formatter.format_head(optparser, pkginfo, section), file=stream) print(optparser.format_option_help(formatter), file=stream) |