summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoug Hellmann <doug@doughellmann.com>2018-01-26 15:57:22 -0500
committerDoug Hellmann <doug@doughellmann.com>2018-02-01 09:19:39 -0500
commit854e59b7df84a88ba10877696eaa5f74a70d2bd3 (patch)
tree9e5e209166a0399e9ade4e5dbe54732c64e4e6d5
parent134ebd4a9a5d981f3bccf4f074adc63f1df72f80 (diff)
downloadcliff-854e59b7df84a88ba10877696eaa5f74a70d2bd3.tar.gz
add argparse conflict handler "ignore"
Update our version of ArgumentParser with a conflict resolution handler called "ignore" to ignore options from commands if they would conflict with options already registered. An error is reported if the action associated with the option would not be registered at all because all of its names conflict. A warning is reported for each option string that is being ignored. Change-Id: I99c62d5772017333136527f7f509c776623641a1 Signed-off-by: Doug Hellmann <doug@doughellmann.com>
-rw-r--r--cliff/_argparse.py80
-rw-r--r--cliff/command.py1
-rw-r--r--cliff/tests/test_command.py30
3 files changed, 106 insertions, 5 deletions
diff --git a/cliff/_argparse.py b/cliff/_argparse.py
index e48dc79..5358e72 100644
--- a/cliff/_argparse.py
+++ b/cliff/_argparse.py
@@ -14,11 +14,14 @@
from __future__ import absolute_import
from argparse import * # noqa
+import argparse
import sys
+import warnings
-if sys.version_info < (3, 5):
- class ArgumentParser(ArgumentParser): # noqa
+class ArgumentParser(argparse.ArgumentParser):
+
+ if sys.version_info < (3, 5):
def __init__(self, *args, **kwargs):
self.allow_abbrev = kwargs.pop("allow_abbrev", True)
super(ArgumentParser, self).__init__(*args, **kwargs)
@@ -28,3 +31,76 @@ if sys.version_info < (3, 5):
return super(ArgumentParser, self)._get_option_tuples(
option_string)
return ()
+
+ # NOTE(dhellmann): We have to override the methods for creating
+ # groups to return our objects that know how to deal with the
+ # special conflict handler.
+
+ def add_argument_group(self, *args, **kwargs):
+ group = _ArgumentGroup(self, *args, **kwargs)
+ self._action_groups.append(group)
+ return group
+
+ def add_mutually_exclusive_group(self, **kwargs):
+ group = _MutuallyExclusiveGroup(self, **kwargs)
+ self._mutually_exclusive_groups.append(group)
+ return group
+
+ def _handle_conflict_ignore(self, action, conflicting_actions):
+ _handle_conflict_ignore(
+ self,
+ self._option_string_actions,
+ action,
+ conflicting_actions,
+ )
+
+
+def _handle_conflict_ignore(container, option_string_actions,
+ new_action, conflicting_actions):
+
+ # Remember the option strings the new action starts with so we can
+ # restore them as part of error reporting if we need to.
+ original_option_strings = new_action.option_strings
+
+ # Remove all of the conflicting option strings from the new action
+ # and report an error if none are left at the end.
+ for option_string, action in conflicting_actions:
+
+ # remove the conflicting option from the new action
+ new_action.option_strings.remove(option_string)
+ warnings.warn(
+ ('Ignoring option string {} for new action '
+ 'because it conflicts with an existing option.').format(
+ option_string))
+
+ # if the option now has no option string, remove it from the
+ # container holding it
+ if not new_action.option_strings:
+ new_action.option_strings = original_option_strings
+ raise argparse.ArgumentError(
+ new_action,
+ ('Cannot resolve conflicting option string, '
+ 'all names conflict.'),
+ )
+
+
+class _ArgumentGroup(argparse._ArgumentGroup):
+
+ def _handle_conflict_ignore(self, action, conflicting_actions):
+ _handle_conflict_ignore(
+ self,
+ self._option_string_actions,
+ action,
+ conflicting_actions,
+ )
+
+
+class _MutuallyExclusiveGroup(argparse._MutuallyExclusiveGroup):
+
+ def _handle_conflict_ignore(self, action, conflicting_actions):
+ _handle_conflict_ignore(
+ self,
+ self._option_string_actions,
+ action,
+ conflicting_actions,
+ )
diff --git a/cliff/command.py b/cliff/command.py
index 859ac47..0a63f88 100644
--- a/cliff/command.py
+++ b/cliff/command.py
@@ -156,6 +156,7 @@ class Command(object):
epilog=self.get_epilog(),
prog=prog_name,
formatter_class=_SmartHelpFormatter,
+ conflict_handler='ignore',
)
for hook in self._hooks:
hook.obj.get_parser(parser)
diff --git a/cliff/tests/test_command.py b/cliff/tests/test_command.py
index 6aecff3..29c8c33 100644
--- a/cliff/tests/test_command.py
+++ b/cliff/tests/test_command.py
@@ -45,7 +45,9 @@ class TestCommand(command.Command):
)
parser.add_argument(
'-z',
- help='used in TestArgumentParser',
+ dest='zippy',
+ default='zippy-default',
+ help='defined in TestCommand and used in TestArgumentParser',
)
return parser
@@ -141,10 +143,32 @@ class TestArgumentParser(base.TestBase):
cmd = TestCommand(None, None)
parser = cmd.get_parser('NAME')
# We should have an exception registering an option with a
- # name that already exists because we do not want commands to
- # override global options.
+ # name that already exists because we configure the argument
+ # parser to ignore conflicts but this option has no other name
+ # to be used.
self.assertRaises(
argparse.ArgumentError,
parser.add_argument,
'-z',
)
+
+ def test_option_name_collision_with_alias(self):
+ cmd = TestCommand(None, None)
+ parser = cmd.get_parser('NAME')
+ # We not should have an exception registering an option with a
+ # name that already exists because we configure the argument
+ # parser to ignore conflicts and this option can be added as
+ # --zero even if the -z is ignored.
+ parser.add_argument('-z', '--zero')
+
+ def test_resolve_option_with_name_collision(self):
+ cmd = TestCommand(None, None)
+ parser = cmd.get_parser('NAME')
+ parser.add_argument(
+ '-z', '--zero',
+ dest='zero',
+ default='zero-default',
+ )
+ args = parser.parse_args(['-z', 'foo', 'a', 'b'])
+ self.assertEqual(args.zippy, 'foo')
+ self.assertEqual(args.zero, 'zero-default')