diff options
Diffstat (limited to 'src/snakeoil/cli/arghparse.py')
-rw-r--r-- | src/snakeoil/cli/arghparse.py | 481 |
1 files changed, 291 insertions, 190 deletions
diff --git a/src/snakeoil/cli/arghparse.py b/src/snakeoil/cli/arghparse.py index 1c832c3e..774699ed 100644 --- a/src/snakeoil/cli/arghparse.py +++ b/src/snakeoil/cli/arghparse.py @@ -9,8 +9,18 @@ import pkgutil import subprocess import sys import traceback -from argparse import (_UNRECOGNIZED_ARGS_ATTR, OPTIONAL, PARSER, REMAINDER, SUPPRESS, ZERO_OR_MORE, - ArgumentError, _, _get_action_name, _SubParsersAction) +from argparse import ( + _UNRECOGNIZED_ARGS_ATTR, + OPTIONAL, + PARSER, + REMAINDER, + SUPPRESS, + ZERO_OR_MORE, + ArgumentError, + _, + _get_action_name, + _SubParsersAction, +) from collections import Counter from functools import partial from itertools import chain @@ -31,11 +41,11 @@ from ..version import get_version _generate_docs = False -@klass.patch('argparse.ArgumentParser.add_subparsers') -@klass.patch('argparse._SubParsersAction.add_parser') -@klass.patch('argparse._ActionsContainer.add_mutually_exclusive_group') -@klass.patch('argparse._ActionsContainer.add_argument_group') -@klass.patch('argparse._ActionsContainer.add_argument') +@klass.patch("argparse.ArgumentParser.add_subparsers") +@klass.patch("argparse._SubParsersAction.add_parser") +@klass.patch("argparse._ActionsContainer.add_mutually_exclusive_group") +@klass.patch("argparse._ActionsContainer.add_argument_group") +@klass.patch("argparse._ActionsContainer.add_argument") def _add_argument_docs(orig_func, self, *args, **kwargs): """Enable docs keyword argument support for argparse arguments. @@ -48,16 +58,16 @@ def _add_argument_docs(orig_func, self, *args, **kwargs): enable the global _generate_docs variable in order to replace the summarized help strings with the extended doc strings. """ - docs = kwargs.pop('docs', None) + docs = kwargs.pop("docs", None) obj = orig_func(self, *args, **kwargs) if _generate_docs and docs is not None: if isinstance(docs, (list, tuple)): # list args are often used if originator wanted to strip # off first description summary line - docs = '\n'.join(docs) - docs = '\n'.join(dedent(docs).strip().split('\n')) + docs = "\n".join(docs) + docs = "\n".join(dedent(docs).strip().split("\n")) - if orig_func.__name__ == 'add_subparsers': + if orig_func.__name__ == "add_subparsers": # store original description before overriding it with extended # docs for general subparsers argument groups self._subparsers._description = self._subparsers.description @@ -93,7 +103,7 @@ class ParseNonblockingStdin(argparse.Action): """Accept arguments from standard input in a non-blocking fashion.""" def __init__(self, *args, **kwargs): - self.filter_func = kwargs.pop('filter_func', lambda x: x.strip()) + self.filter_func = kwargs.pop("filter_func", lambda x: x.strip()) super().__init__(*args, **kwargs) def _stdin(self): @@ -106,9 +116,11 @@ class ParseNonblockingStdin(argparse.Action): break def __call__(self, parser, namespace, values, option_string=None): - if values is not None and len(values) == 1 and values[0] == '-': + if values is not None and len(values) == 1 and values[0] == "-": if sys.stdin.isatty(): - raise argparse.ArgumentError(self, "'-' is only valid when piping data in") + raise argparse.ArgumentError( + self, "'-' is only valid when piping data in" + ) values = self._stdin() setattr(namespace, self.dest, values) @@ -117,16 +129,18 @@ class ParseStdin(ExtendAction): """Accept arguments from standard input in a blocking fashion.""" def __init__(self, *args, **kwargs): - self.filter_func = kwargs.pop('filter_func', lambda x: x.strip()) + self.filter_func = kwargs.pop("filter_func", lambda x: x.strip()) super().__init__(*args, **kwargs) def __call__(self, parser, namespace, values, option_string=None): - if values is not None and len(values) == 1 and values[0] == '-': + if values is not None and len(values) == 1 and values[0] == "-": if sys.stdin.isatty(): - raise argparse.ArgumentError(self, "'-' is only valid when piping data in") + raise argparse.ArgumentError( + self, "'-' is only valid when piping data in" + ) values = [x.rstrip() for x in sys.stdin.readlines() if self.filter_func(x)] # reassign stdin to allow interactivity (currently only works for unix) - sys.stdin = open('/dev/tty') + sys.stdin = open("/dev/tty") super().__call__(parser, namespace, values, option_string) @@ -136,10 +150,10 @@ class CommaSeparatedValues(argparse._AppendAction): def parse_values(self, values): items = [] if isinstance(values, str): - items.extend(x for x in values.split(',') if x) + items.extend(x for x in values.split(",") if x) else: for value in values: - items.extend(x for x in value.split(',') if x) + items.extend(x for x in value.split(",") if x) return items def __call__(self, parser, namespace, values, option_string=None): @@ -174,16 +188,16 @@ class CommaSeparatedNegations(argparse._AppendAction): values = [values] for value in values: try: - neg, pos = split_negations(x for x in value.split(',') if x) + neg, pos = split_negations(x for x in value.split(",") if x) except ValueError as e: raise argparse.ArgumentTypeError(e) disabled.extend(neg) enabled.extend(pos) if colliding := set(disabled).intersection(enabled): - collisions = ', '.join(map(repr, sorted(colliding))) + collisions = ", ".join(map(repr, sorted(colliding))) s = pluralism(colliding) - msg = f'colliding value{s}: {collisions}' + msg = f"colliding value{s}: {collisions}" raise argparse.ArgumentError(self, msg) return disabled, enabled @@ -222,7 +236,7 @@ class CommaSeparatedElements(argparse._AppendAction): values = [values] for value in values: try: - neg, neu, pos = split_elements(x for x in value.split(',') if x) + neg, neu, pos = split_elements(x for x in value.split(",") if x) except ValueError as e: raise argparse.ArgumentTypeError(e) disabled.extend(neg) @@ -231,9 +245,9 @@ class CommaSeparatedElements(argparse._AppendAction): elements = [set(x) for x in (disabled, neutral, enabled) if x] if len(elements) > 1 and (colliding := set.intersection(*elements)): - collisions = ', '.join(map(repr, sorted(colliding))) + collisions = ", ".join(map(repr, sorted(colliding))) s = pluralism(colliding) - msg = f'colliding value{s}: {collisions}' + msg = f"colliding value{s}: {collisions}" raise argparse.ArgumentError(self, msg) return disabled, neutral, enabled @@ -260,14 +274,14 @@ class ManHelpAction(argparse._HelpAction): """Display man pages for long --help option and abbreviated output for -h.""" def __call__(self, parser, namespace, values, option_string=None): - if option_string == '--help': + if option_string == "--help": # Try spawning man page -- assumes one level deep for subcommand # specific man pages with commands separated by hyphen. For example # running `pinspect profile --help` tries to open pinspect-profile # man page, but `pinspect profile masks --help` also tries to open # pinspect-profile. - man_page = '-'.join(parser.prog.split()[:2]) - p = subprocess.Popen(['man', man_page], stderr=subprocess.DEVNULL) + man_page = "-".join(parser.prog.split()[:2]) + p = subprocess.Popen(["man", man_page], stderr=subprocess.DEVNULL) p.communicate() if p.returncode == 0: parser.exit() @@ -279,16 +293,17 @@ class ManHelpAction(argparse._HelpAction): class StoreBool(argparse._StoreAction): - - def __init__(self, - option_strings, - dest, - nargs=None, - const=None, - default=None, - required=False, - help=None, - metavar='BOOLEAN'): + def __init__( + self, + option_strings, + dest, + nargs=None, + const=None, + default=None, + required=False, + help=None, + metavar="BOOLEAN", + ): super().__init__( option_strings=option_strings, dest=dest, @@ -298,38 +313,42 @@ class StoreBool(argparse._StoreAction): type=self.boolean, required=required, help=help, - metavar=metavar) + metavar=metavar, + ) @staticmethod def boolean(value): value = value.lower() - if value in ('y', 'yes', 'true', '1'): + if value in ("y", "yes", "true", "1"): return True - elif value in ('n', 'no', 'false', '0'): + elif value in ("n", "no", "false", "0"): return False raise ValueError("value %r must be [y|yes|true|1|n|no|false|0]" % (value,)) class EnableDebug(argparse._StoreTrueAction): - def __call__(self, parser, namespace, values, option_string=None): super().__call__(parser, namespace, values, option_string=option_string) logging.root.setLevel(logging.DEBUG) class Verbosity(argparse.Action): - def __init__(self, option_strings, dest, default=None, required=False, help=None): super().__init__( - option_strings=option_strings, dest=dest, nargs=0, - default=default, required=required, help=help) + option_strings=option_strings, + dest=dest, + nargs=0, + default=default, + required=required, + help=help, + ) # map verbose/quiet args to increment/decrement the underlying verbosity value self.value_map = { - '-q': -1, - '--quiet': -1, - '-v': 1, - '--verbose': 1, + "-q": -1, + "--quiet": -1, + "-v": 1, + "--verbose": 1, } def __call__(self, parser, namespace, values, option_string=None): @@ -343,7 +362,6 @@ class Verbosity(argparse.Action): class DelayedValue: - def __init__(self, invokable, priority=0): self.priority = priority if not callable(invokable): @@ -355,7 +373,6 @@ class DelayedValue: class DelayedDefault(DelayedValue): - @classmethod def wipe(cls, attrs, priority): if isinstance(attrs, str): @@ -376,20 +393,17 @@ class DelayedDefault(DelayedValue): class DelayedParse(DelayedValue): - def __call__(self, namespace, attr): self.invokable() class OrderedParse(DelayedValue): - def __call__(self, namespace, attr): self.invokable(namespace) delattr(namespace, attr) class Delayed(argparse.Action): - def __init__(self, option_strings, dest, target=None, priority=0, **kwargs): if target is None: raise ValueError("target must be non None for Delayed") @@ -397,21 +411,30 @@ class Delayed(argparse.Action): self.priority = int(priority) self.target = target(option_strings=option_strings, dest=dest, **kwargs.copy()) super().__init__( - option_strings=option_strings[:], dest=dest, - nargs=kwargs.get("nargs", None), required=kwargs.get("required", None), - help=kwargs.get("help", None), metavar=kwargs.get("metavar", None), - default=kwargs.get("default", None)) + option_strings=option_strings[:], + dest=dest, + nargs=kwargs.get("nargs", None), + required=kwargs.get("required", None), + help=kwargs.get("help", None), + metavar=kwargs.get("metavar", None), + default=kwargs.get("default", None), + ) def __call__(self, parser, namespace, values, option_string=None): - setattr(namespace, self.dest, DelayedParse( - partial(self.target, parser, namespace, values, option_string), - self.priority)) + setattr( + namespace, + self.dest, + DelayedParse( + partial(self.target, parser, namespace, values, option_string), + self.priority, + ), + ) class Expansion(argparse.Action): - - def __init__(self, option_strings, dest, nargs=None, help=None, - required=None, subst=None): + def __init__( + self, option_strings, dest, nargs=None, help=None, required=None, subst=None + ): if subst is None: raise TypeError("substitution string must be set") # simple aliases with no required arguments shouldn't need to specify nargs @@ -424,7 +447,8 @@ class Expansion(argparse.Action): help=help, required=required, default=False, - nargs=nargs) + nargs=nargs, + ) self.subst = tuple(subst) def __call__(self, parser, namespace, values, option_string=None): @@ -434,7 +458,7 @@ class Expansion(argparse.Action): if isinstance(values, str): vals = [vals] dvals = {str(idx): val for idx, val in enumerate(vals)} - dvals['*'] = ' '.join(vals) + dvals["*"] = " ".join(vals) for action in actions: action_map.update((option, action) for option in action.option_strings) @@ -445,8 +469,8 @@ class Expansion(argparse.Action): args = [x % dvals for x in args] if not action: raise ValueError( - "unable to find option %r for %r" % - (option, self.option_strings)) + "unable to find option %r for %r" % (option, self.option_strings) + ) if action.type is not None: args = list(map(action.type, args)) if action.nargs in (1, None): @@ -456,7 +480,6 @@ class Expansion(argparse.Action): class _SubParser(argparse._SubParsersAction): - def add_parser(self, name, cls=None, **kwargs): """Subparser that links description/help if one is specified.""" description = kwargs.get("description") @@ -465,7 +488,7 @@ class _SubParser(argparse._SubParsersAction): if help_txt is not None: kwargs["description"] = help_txt elif help_txt is None: - kwargs["help"] = description.split('\n', 1)[0] + kwargs["help"] = description.split("\n", 1)[0] # support using a custom parser class for the subparser orig_class = self._parser_class @@ -486,7 +509,7 @@ class _SubParser(argparse._SubParsersAction): Note that this assumes a specific module naming and layout scheme for commands. """ prog = self._prog_prefix - module = f'{prog}.scripts.{prog}_{subcmd}' + module = f"{prog}.scripts.{prog}_{subcmd}" func = partial(self._lazy_parser, module, subcmd) self._name_parser_map[subcmd] = lazy_object_proxy.Proxy(func) @@ -507,8 +530,8 @@ class _SubParser(argparse._SubParsersAction): try: parser = self._name_parser_map[parser_name] except KeyError: - tup = parser_name, ', '.join(self._name_parser_map) - msg = _('unknown parser %r (choices: %s)') % tup + tup = parser_name, ", ".join(self._name_parser_map) + msg = _("unknown parser %r (choices: %s)") % tup raise argparse.ArgumentError(self, msg) # parse all the remaining options into the namespace @@ -526,11 +549,13 @@ class CsvHelpFormatter(argparse.HelpFormatter): def _format_args(self, action, default_metavar): get_metavar = self._metavar_formatter(action, default_metavar) if isinstance(action, (CommaSeparatedValues, CommaSeparatedValuesAppend)): - result = '%s[,%s,...]' % get_metavar(2) - elif isinstance(action, (CommaSeparatedNegations, CommaSeparatedNegationsAppend)): - result = '%s[,-%s,...]' % get_metavar(2) + result = "%s[,%s,...]" % get_metavar(2) + elif isinstance( + action, (CommaSeparatedNegations, CommaSeparatedNegationsAppend) + ): + result = "%s[,-%s,...]" % get_metavar(2) elif isinstance(action, (CommaSeparatedElements, CommaSeparatedElementsAppend)): - result = '%s[,-%s,+%s...]' % get_metavar(3) + result = "%s[,-%s,+%s...]" % get_metavar(3) else: result = super()._format_args(action, default_metavar) return result @@ -540,7 +565,7 @@ class SortedHelpFormatter(CsvHelpFormatter): """Help formatter that sorts arguments by option strings.""" def add_arguments(self, actions): - actions = sorted(actions, key=attrgetter('option_strings')) + actions = sorted(actions, key=attrgetter("option_strings")) super().add_arguments(actions) @@ -576,7 +601,7 @@ class SubcmdAbbrevArgumentParser(argparse.ArgumentParser): # for everything but PARSER, REMAINDER args, strip out first '--' if action.nargs not in [PARSER, REMAINDER]: try: - arg_strings.remove('--') + arg_strings.remove("--") except ValueError: pass @@ -592,8 +617,11 @@ class SubcmdAbbrevArgumentParser(argparse.ArgumentParser): # when nargs='*' on a positional, if there were no command-line # args, use the default if it is anything other than None - elif (not arg_strings and action.nargs == ZERO_OR_MORE and - not action.option_strings): + elif ( + not arg_strings + and action.nargs == ZERO_OR_MORE + and not action.option_strings + ): if action.default is not None: value = action.default else: @@ -602,7 +630,7 @@ class SubcmdAbbrevArgumentParser(argparse.ArgumentParser): # single argument or optional argument produces a single value elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]: - arg_string, = arg_strings + (arg_string,) = arg_strings value = self._get_value(action, arg_string) self._check_value(action, value) @@ -688,7 +716,7 @@ class OptionalsParser(argparse.ArgumentParser): for i, mutex_action in enumerate(mutex_group._group_actions): conflicts = action_conflicts.setdefault(mutex_action, []) conflicts.extend(group_actions[:i]) - conflicts.extend(group_actions[i + 1:]) + conflicts.extend(group_actions[i + 1 :]) # find all option indices, and determine the arg_string_pattern # which has an 'O' if there is an option at an index, @@ -699,24 +727,24 @@ class OptionalsParser(argparse.ArgumentParser): for i, arg_string in enumerate(arg_strings_iter): # all args after -- are non-options - if arg_string == '--': - arg_string_pattern_parts.append('-') + if arg_string == "--": + arg_string_pattern_parts.append("-") for arg_string in arg_strings_iter: - arg_string_pattern_parts.append('A') + arg_string_pattern_parts.append("A") # otherwise, add the arg to the arg strings # and note the index if it was an option else: option_tuple = self._parse_optional(arg_string) if option_tuple is None: - pattern = 'A' + pattern = "A" else: option_string_indices[i] = option_tuple - pattern = 'O' + pattern = "O" arg_string_pattern_parts.append(pattern) # join the pieces together to form the pattern - arg_strings_pattern = ''.join(arg_string_pattern_parts) + arg_strings_pattern = "".join(arg_string_pattern_parts) # converts arg strings to the appropriate and then takes the action seen_actions = set() @@ -733,7 +761,7 @@ class OptionalsParser(argparse.ArgumentParser): seen_non_default_actions.add(action) for conflict_action in action_conflicts.get(action, []): if conflict_action in seen_non_default_actions: - msg = _('not allowed with argument %s') + msg = _("not allowed with argument %s") action_name = _get_action_name(conflict_action) raise ArgumentError(action, msg % action_name) @@ -762,14 +790,14 @@ class OptionalsParser(argparse.ArgumentParser): # if we match help options, skip them for now so subparsers # show up in the help output - if arg_strings[start_index] in ('-h', '--help'): + if arg_strings[start_index] in ("-h", "--help"): extras.append(arg_strings[start_index]) return start_index + 1 # if there is an explicit argument, try to match the # optional's string arguments to only this if explicit_arg is not None: - arg_count = match_argument(action, 'A') + arg_count = match_argument(action, "A") # if the action is a single-dash option and takes no # arguments, try to parse more single-dash options out @@ -785,7 +813,7 @@ class OptionalsParser(argparse.ArgumentParser): action = optionals_map[option_string] explicit_arg = new_explicit_arg else: - msg = _('ignored explicit argument %r') + msg = _("ignored explicit argument %r") raise ArgumentError(action, msg % explicit_arg) # if the action expect exactly one argument, we've @@ -799,7 +827,7 @@ class OptionalsParser(argparse.ArgumentParser): # error if a double-dash option did not use the # explicit argument else: - msg = _('ignored explicit argument %r') + msg = _("ignored explicit argument %r") raise ArgumentError(action, msg % explicit_arg) # if there is no explicit argument, try to match the @@ -835,13 +863,13 @@ class OptionalsParser(argparse.ArgumentParser): # slice off the appropriate arg strings for each Positional # and add the Positional and its args to the list for action, arg_count in zip(positionals, arg_counts): - args = arg_strings[start_index: start_index + arg_count] + args = arg_strings[start_index : start_index + arg_count] start_index += arg_count take_action(action, args) # slice off the Positionals that we just parsed and return the # index at which the Positionals' string args stopped - positionals[:] = positionals[len(arg_counts):] + positionals[:] = positionals[len(arg_counts) :] return start_index # consume Positionals and Optionals alternately, until we have @@ -855,10 +883,9 @@ class OptionalsParser(argparse.ArgumentParser): while start_index <= max_option_string_index: # consume any Positionals preceding the next option - next_option_string_index = min([ - index - for index in option_string_indices - if index >= start_index]) + next_option_string_index = min( + [index for index in option_string_indices if index >= start_index] + ) if start_index != next_option_string_index: # positionals_end_index = consume_positionals(start_index) positionals_end_index = start_index @@ -894,7 +921,9 @@ class OptionalsParser(argparse.ArgumentParser): for action in self._actions: if action not in seen_actions: # ignore required subcommands and positionals as they'll be handled later - skip = not action.option_strings or isinstance(action, _SubParsersAction) + skip = not action.option_strings or isinstance( + action, _SubParsersAction + ) if action.required and not skip: required_actions.append(_get_action_name(action)) else: @@ -902,16 +931,23 @@ class OptionalsParser(argparse.ArgumentParser): # parsing arguments to avoid calling convert functions # twice (which may fail) if the argument was given, but # only if it was defined already in the namespace - if (action.default is not None and - isinstance(action.default, str) and - hasattr(namespace, action.dest) and - action.default is getattr(namespace, action.dest)): - setattr(namespace, action.dest, - self._get_value(action, action.default)) + if ( + action.default is not None + and isinstance(action.default, str) + and hasattr(namespace, action.dest) + and action.default is getattr(namespace, action.dest) + ): + setattr( + namespace, + action.dest, + self._get_value(action, action.default), + ) if required_actions: - self.error(_('the following arguments are required: %s') % - ', '.join(required_actions)) + self.error( + _("the following arguments are required: %s") + % ", ".join(required_actions) + ) # make sure all required groups had one option present for group in self._mutually_exclusive_groups: @@ -922,11 +958,13 @@ class OptionalsParser(argparse.ArgumentParser): # if no actions were used, report the error else: - names = [_get_action_name(action) - for action in group._group_actions - if action.help is not SUPPRESS] - msg = _('one of the arguments %s is required') - self.error(msg % ' '.join(names)) + names = [ + _get_action_name(action) + for action in group._group_actions + if action.help is not SUPPRESS + ] + msg = _("one of the arguments %s is required") + self.error(msg % " ".join(names)) # return the updated namespace and the extra arguments return namespace, extras @@ -937,31 +975,49 @@ class CsvActionsParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.register('action', 'csv', CommaSeparatedValues) - self.register('action', 'csv_append', CommaSeparatedValuesAppend) - self.register('action', 'csv_negations', CommaSeparatedNegations) - self.register('action', 'csv_negations_append', CommaSeparatedNegationsAppend) - self.register('action', 'csv_elements', CommaSeparatedElements) - self.register('action', 'csv_elements_append', CommaSeparatedElementsAppend) + self.register("action", "csv", CommaSeparatedValues) + self.register("action", "csv_append", CommaSeparatedValuesAppend) + self.register("action", "csv_negations", CommaSeparatedNegations) + self.register("action", "csv_negations_append", CommaSeparatedNegationsAppend) + self.register("action", "csv_elements", CommaSeparatedElements) + self.register("action", "csv_elements_append", CommaSeparatedElementsAppend) class ArgumentParser(OptionalsParser, CsvActionsParser): """Extended, argparse-compatible argument parser.""" - def __init__(self, suppress=False, subcmds=False, color=True, debug=True, quiet=True, - verbose=True, version=True, add_help=True, sorted_help=False, - description=None, docs=None, script=None, prog=None, **kwargs): - self.debug = debug and '--debug' in sys.argv[1:] + def __init__( + self, + suppress=False, + subcmds=False, + color=True, + debug=True, + quiet=True, + verbose=True, + version=True, + add_help=True, + sorted_help=False, + description=None, + docs=None, + script=None, + prog=None, + **kwargs, + ): + self.debug = debug and "--debug" in sys.argv[1:] self.verbosity = int(verbose) if self.verbosity: argv = Counter(sys.argv[1:]) # Only supports single, short opts (i.e. -vv isn't recognized), # post argparsing the proper value supporting those kind of args is # in the options namespace. - self.verbosity = sum(chain.from_iterable(( - (-1 for x in range(argv['-q'] + argv['--quiet'])), - (1 for x in range(argv['-v'] + argv['--verbose'])), - ))) + self.verbosity = sum( + chain.from_iterable( + ( + (-1 for x in range(argv["-q"] + argv["--quiet"])), + (1 for x in range(argv["-v"] + argv["--verbose"])), + ) + ) + ) # subparsers action object from calling add_subparsers() self.__subparsers = None @@ -979,7 +1035,7 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): # usage such as adding conflicting options to both the root command and # subcommands without causing issues in addition to helping support # default subparsers. - self._parents = tuple(kwargs.get('parents', ())) + self._parents = tuple(kwargs.get("parents", ())) # extract the description to use and set docs for doc generation description = self._update_desc(description, docs) @@ -993,11 +1049,12 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): raise TypeError except TypeError: raise ValueError( - "invalid script parameter, should be (__file__, __name__)") + "invalid script parameter, should be (__file__, __name__)" + ) - project = script_module.split('.')[0] + project = script_module.split(".")[0] if prog is None: - prog = script_module.split('.')[-1] + prog = script_module.split(".")[-1] if sorted_help: formatter = SortedHelpFormatter @@ -1005,27 +1062,36 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): formatter = CsvHelpFormatter super().__init__( - description=description, formatter_class=formatter, - prog=prog, add_help=False, **kwargs) + description=description, + formatter_class=formatter, + prog=prog, + add_help=False, + **kwargs, + ) # register custom actions - self.register('action', 'parsers', _SubParser) + self.register("action", "parsers", _SubParser) if not suppress: - base_opts = self.add_argument_group('base options') + base_opts = self.add_argument_group("base options") if add_help: base_opts.add_argument( - '-h', '--help', action=ManHelpAction, default=argparse.SUPPRESS, - help='show this help message and exit', + "-h", + "--help", + action=ManHelpAction, + default=argparse.SUPPRESS, + help="show this help message and exit", docs=""" Show this help message and exit. To get more information see the related man page. - """) + """, + ) if version and script is not None: # Note that this option will currently only be available on the # base command, not on subcommands. base_opts.add_argument( - '--version', action='version', + "--version", + action="version", version=get_version(project, script_path), help="show this program's version info and exit", docs=""" @@ -1034,39 +1100,58 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): When running from within a git repo or a version installed from git the latest commit hash and date will be shown. - """) + """, + ) if debug: base_opts.add_argument( - '--debug', action=EnableDebug, help='enable debugging checks', - docs='Enable debug checks and show verbose debug output.') + "--debug", + action=EnableDebug, + help="enable debugging checks", + docs="Enable debug checks and show verbose debug output.", + ) if quiet: base_opts.add_argument( - '-q', '--quiet', action=Verbosity, dest='verbosity', default=0, - help='suppress non-error messages', - docs="Suppress non-error, informational messages.") + "-q", + "--quiet", + action=Verbosity, + dest="verbosity", + default=0, + help="suppress non-error messages", + docs="Suppress non-error, informational messages.", + ) if verbose: base_opts.add_argument( - '-v', '--verbose', action=Verbosity, dest='verbosity', default=0, - help='show verbose output', - docs="Increase the verbosity of various output.") + "-v", + "--verbose", + action=Verbosity, + dest="verbosity", + default=0, + help="show verbose output", + docs="Increase the verbosity of various output.", + ) if color: base_opts.add_argument( - '--color', action=StoreBool, + "--color", + action=StoreBool, default=sys.stdout.isatty(), - help='enable/disable color support', + help="enable/disable color support", docs=""" Toggle colored output support. This can be used to forcibly enable color support when piping output or other sitations where stdout is not a tty. - """) + """, + ) # register existing subcommands if subcmds: - prefix = f'{prog}.scripts.{prog}_' + prefix = f"{prog}.scripts.{prog}_" if subcmd_modules := [ - name[len(prefix):] for _, name, _ in - pkgutil.walk_packages([os.path.dirname(script_path)], f'{prog}.scripts.') - if name.startswith(prefix)]: + name[len(prefix) :] + for _, name, _ in pkgutil.walk_packages( + [os.path.dirname(script_path)], f"{prog}.scripts." + ) + if name.startswith(prefix) + ]: subparsers = self.add_subparsers() for subcmd in subcmd_modules: subparsers.add_command(subcmd) @@ -1080,7 +1165,7 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): """ description_lines = [] if description is not None: - description_lines = description.strip().split('\n', 1) + description_lines = description.strip().split("\n", 1) description = description_lines[0] if _generate_docs: if docs is None and len(description_lines) == 2: @@ -1156,7 +1241,9 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): try: # run registered early parse functions from all parsers - for functor, parser in chain.from_iterable(x.__early_parse for x in self.parsers): + for functor, parser in chain.from_iterable( + x.__early_parse for x in self.parsers + ): namespace, args = functor(parser, namespace, args) # parse the arguments and exit if there are any errors @@ -1176,7 +1263,7 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): args, unknown_args = self.parse_known_args(args, namespace) # make sure the correct function and prog are set if running a subcommand - subcmd_parser = self.subparsers.get(getattr(args, 'subcommand', None), None) + subcmd_parser = self.subparsers.get(getattr(args, "subcommand", None), None) if subcmd_parser is not None: # override the running program with full subcommand self.prog = subcmd_parser.prog @@ -1186,7 +1273,7 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): namespace.main_func = subcmd_parser.__main_func if unknown_args: - self.error('unrecognized arguments: %s' % ' '.join(unknown_args)) + self.error("unrecognized arguments: %s" % " ".join(unknown_args)) # Two runs are required; first, handle any suppression defaults # introduced. Subparsers defaults cannot override the parent parser, as @@ -1198,14 +1285,20 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): # intentionally no protection of suppression code; this should # just work. - i = ((attr, val) for attr, val in args.__dict__.items() - if isinstance(val, DelayedDefault)) + i = ( + (attr, val) + for attr, val in args.__dict__.items() + if isinstance(val, DelayedDefault) + ) for attr, functor in sorted(i, key=lambda val: val[1].priority): functor(args, attr) # now run the delays - i = ((attr, val) for attr, val in args.__dict__.items() - if isinstance(val, DelayedValue)) + i = ( + (attr, val) + for attr, val in args.__dict__.items() + if isinstance(val, DelayedValue) + ) try: for attr, delayed in sorted(i, key=lambda val: val[1].priority): delayed(args, attr) @@ -1216,7 +1309,9 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): self.error(str(e)) # run final arg validation - final_checks = [k for k in args.__dict__.keys() if k.startswith('__final_check__')] + final_checks = [ + k for k in args.__dict__.keys() if k.startswith("__final_check__") + ] for check in final_checks: functor = args.pop(check) functor(self, args) @@ -1232,7 +1327,7 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): if self.debug and sys.exc_info() != (None, None, None): # output traceback if any exception is on the stack traceback.print_exc() - self.exit(status, '%s: error: %s\n' % (self.prog, message)) + self.exit(status, "%s: error: %s\n" % (self.prog, message)) def bind_main_func(self, functor): """Decorator to set a main function for the parser.""" @@ -1245,8 +1340,8 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): def bind_class(self, obj): if not isinstance(obj, ArgparseCommand): raise ValueError( - "expected obj to be an instance of " - "ArgparseCommand; got %r" % (obj,)) + "expected obj to be an instance of " "ArgparseCommand; got %r" % (obj,) + ) obj.bind_to_parser(self) return self @@ -1261,10 +1356,12 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): """Only run delayed default functor if the attribute isn't set.""" if isinstance(object.__getattribute__(namespace, attr), DelayedValue): functor(namespace, attr) + if name is None: name = functor.__name__ self.set_defaults(**{name: DelayedValue(default, priority)}) return functor + return f def bind_parse_priority(self, priority): @@ -1272,6 +1369,7 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): name = functor.__name__ self.set_defaults(**{name: OrderedParse(functor, priority)}) return functor + return f def add_subparsers(self, **kwargs): @@ -1280,9 +1378,9 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): if self.__subparsers is not None: return self.__subparsers - kwargs.setdefault('title', 'subcommands') - kwargs.setdefault('dest', 'subcommand') - kwargs.setdefault('prog', self.prog) + kwargs.setdefault("title", "subcommands") + kwargs.setdefault("dest", "subcommand") + kwargs.setdefault("prog", self.prog) subparsers = argparse.ArgumentParser.add_subparsers(self, **kwargs) subparsers.required = True self.__subparsers = subparsers @@ -1300,18 +1398,17 @@ class ArgumentParser(OptionalsParser, CsvActionsParser): def bind_final_check(self, functor): """Decorator to bind a function for argument validation.""" - name = f'__final_check__{functor.__name__}' + name = f"__final_check__{functor.__name__}" self.set_defaults(**{name: functor}) return functor class ArgparseCommand: - def bind_to_parser(self, parser): parser.bind_main_func(self) def __call__(self, namespace, out, err): - raise NotImplementedError(self, '__call__') + raise NotImplementedError(self, "__call__") class FileType(argparse.FileType): @@ -1322,11 +1419,11 @@ class FileType(argparse.FileType): def __call__(self, string): # the special argument "-" means sys.std{in,out} - if string == '-': - if 'r' in self._mode: - return sys.stdin.buffer if 'b' in self._mode else sys.stdin - elif any(c in self._mode for c in 'wax'): - return sys.stdout.buffer if 'b' in self._mode else sys.stdout + if string == "-": + if "r" in self._mode: + return sys.stdin.buffer if "b" in self._mode else sys.stdin + elif any(c in self._mode for c in "wax"): + return sys.stdout.buffer if "b" in self._mode else sys.stdout else: msg = _('argument "-" with mode %r') % self._mode raise ValueError(msg) @@ -1342,23 +1439,27 @@ class FileType(argparse.FileType): def existent_path(value): """Check if file argument path exists.""" if not os.path.exists(value): - raise argparse.ArgumentTypeError(f'nonexistent path: {value!r}') + raise argparse.ArgumentTypeError(f"nonexistent path: {value!r}") try: return os.path.realpath(value) except EnvironmentError as e: - raise ValueError(f'while resolving path {value!r}, encountered error: {e}') from e + raise ValueError( + f"while resolving path {value!r}, encountered error: {e}" + ) from e def existent_dir(value): """Check if argument path exists and is a directory.""" if not os.path.exists(value): - raise argparse.ArgumentTypeError(f'nonexistent dir: {value!r}') + raise argparse.ArgumentTypeError(f"nonexistent dir: {value!r}") elif not os.path.isdir(value): - raise argparse.ArgumentTypeError(f'file already exists: {value!r}') + raise argparse.ArgumentTypeError(f"file already exists: {value!r}") try: return os.path.realpath(value) except EnvironmentError as e: - raise ValueError(f'while resolving path {value!r}, encountered error: {e}') from e + raise ValueError( + f"while resolving path {value!r}, encountered error: {e}" + ) from e def create_dir(value): @@ -1367,9 +1468,9 @@ def create_dir(value): try: os.makedirs(path, exist_ok=True) except FileExistsError: - raise argparse.ArgumentTypeError(f'file already exists: {value!r}') + raise argparse.ArgumentTypeError(f"file already exists: {value!r}") except IOError as e: - raise argparse.ArgumentTypeError(f'failed creating dir: {e}') + raise argparse.ArgumentTypeError(f"failed creating dir: {e}") return path @@ -1378,12 +1479,12 @@ def bounded_int(func, desc, x): try: n = int(x) except ValueError: - raise argparse.ArgumentTypeError('invalid integer value') + raise argparse.ArgumentTypeError("invalid integer value") if not func(n): - raise argparse.ArgumentTypeError(f'must be {desc}') + raise argparse.ArgumentTypeError(f"must be {desc}") return n def positive_int(x): """Check if argument is a positive integer.""" - return bounded_int(lambda n: n >= 1, '>= 1', x) + return bounded_int(lambda n: n >= 1, ">= 1", x) |