From 795337e1ee03a762e7638e23985c04818f1c9754 Mon Sep 17 00:00:00 2001 From: Jiri Kucera Date: Apr 04 2018 21:27:03 +0000 Subject: Added custom ArgumentParser (supports allow_abbrev) Python 2's argparse.ArgumentParser do not support allow_abbrev option that is implemented in Python 3's argparse.ArgumentParser; the custom ArgumentParser class implements the allow_abbrev option functionality for Python 2. The abbreviation of passed arguments is switched off (allow_abbrev=False). Discussion: https://pagure.io/fedpkg/pull-request/186 Signed-off-by: Jiri Kucera --- diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py index 1bf94e2..cf2ac60 100644 --- a/pyrpkg/cli.py +++ b/pyrpkg/cli.py @@ -22,6 +22,8 @@ import string import sys import time import re +# For `_ArgumentParser' +from gettext import gettext as _ import koji_cli.lib import pyrpkg.utils as utils @@ -38,6 +40,92 @@ def warning_deprecated_dist(value): return value +# Adds `allow_abbrev' feature +class _ArgumentParser(argparse.ArgumentParser): + def __init__(self, *args, **kwargs): + sallow_abbrev = 'allow_abbrev' + self.allow_abbrev = kwargs.get(sallow_abbrev, True) + if sallow_abbrev in kwargs: + del kwargs[sallow_abbrev] + super(_ArgumentParser, self).__init__(*args, **kwargs) + + # We take `argparse.ArgumentParser._parse_optional' from the Python 2.7 + # standard library + # https://github.com/python/cpython/blob/2.7/Lib/argparse.py#L2055 + # and combine it with `argparse.ArgumentParser._parse_optional' from the + # Python 3.6 + # https://github.com/python/cpython/blob/3.6/Lib/argparse.py#L2083 + def _parse_optional(self, arg_string): + # if it's an empty string, it was meant to be a positional + if not arg_string: + return None + + # if it doesn't start with a prefix, it was meant to be positional + if not arg_string[0] in self.prefix_chars: + return None + + # if the option string is present in the parser, return the action + if arg_string in self._option_string_actions: + action = self._option_string_actions[arg_string] + return action, arg_string, None + + # if it's just a single character, it was meant to be positional + if len(arg_string) == 1: + return None + + # if the option string before the "=" is present, return the action + if '=' in arg_string: + option_string, explicit_arg = arg_string.split('=', 1) + if option_string in self._option_string_actions: + action = self._option_string_actions[option_string] + return action, option_string, explicit_arg + + # This was added from Python 3's argparse library; the rest of + # _parse_optional remains same as in Python 2's argparse + # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + if self.allow_abbrev: + # search through all possible prefixes of the option string + # and all actions in the parser for possible interpretations + option_tuples = self._get_option_tuples(arg_string) + + # if multiple actions match, the option string was ambiguous + if len(option_tuples) > 1: + options = ', '.join([ + option_string_ + for action_, option_string_, explicit_arg_ in option_tuples + ]) + tup = arg_string, options + self.error(_('ambiguous option: %s could match %s') % tup) + + # if exactly one action matched, this segmentation is good, + # so return the parsed action + elif len(option_tuples) == 1: + option_tuple, = option_tuples + return option_tuple + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + # if it was not found as an option, but it looks like a negative + # number, it was meant to be positional + # unless there are negative-number-like options + if self._negative_number_matcher.match(arg_string): + if not self._has_negative_number_optionals: + return None + + # if it contains a space, it was meant to be a positional + if ' ' in arg_string: + return None + + # it was meant to be an optional but there is no such option + # in this parser (though it might be a valid option in a subparser) + return None, arg_string, None + + +if six.PY2: + ArgumentParser = _ArgumentParser +else: + ArgumentParser = argparse.ArgumentParser + + class cliClient(object): """This is a client class for rpkg clients.""" @@ -206,9 +294,10 @@ class cliClient(object): def setup_argparser(self): """Setup the argument parser and register some basic commands.""" - self.parser = argparse.ArgumentParser( + self.parser = ArgumentParser( prog=self.name, - epilog='For detailed help pass --help to a target') + epilog='For detailed help pass --help to a target', + allow_abbrev=False) # Add some basic arguments that should be used by all. # Add a config file self.parser.add_argument('--config', '-C', @@ -329,8 +418,8 @@ class cliClient(object): def register_build_common(self): """Create a common build parser to use in other commands""" - self.build_parser_common = argparse.ArgumentParser( - 'build_common', add_help=False) + self.build_parser_common = ArgumentParser( + 'build_common', add_help=False, allow_abbrev=False) self.build_parser_common.add_argument( '--arches', nargs='*', help='Build for specific arches') self.build_parser_common.add_argument( @@ -349,8 +438,8 @@ class cliClient(object): def register_rpm_common(self): """Create a common parser for rpm commands""" - self.rpm_parser_common = argparse.ArgumentParser( - 'rpm_common', add_help=False) + self.rpm_parser_common = ArgumentParser( + 'rpm_common', add_help=False, allow_abbrev=False) self.rpm_parser_common.add_argument( '--builddir', default=None, help='Define an alternate builddir') self.rpm_parser_common.add_argument(