summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-09-24 07:23:07 (GMT)
committerGitHub <noreply@github.com>2024-09-24 07:23:07 (GMT)
commit3094cd17b0e5ba69309c54964744c797a70aa11b (patch)
treeabca8f5f42108d59eb7f1a2156ca274818568993 /Lib
parentfaef3fa653f2901cc905f98eae0ddcd8dc334d33 (diff)
downloadcpython-3094cd17b0e5ba69309c54964744c797a70aa11b.zip
cpython-3094cd17b0e5ba69309c54964744c797a70aa11b.tar.gz
cpython-3094cd17b0e5ba69309c54964744c797a70aa11b.tar.bz2
gh-63143: Fix parsing mutually exclusive arguments in argparse (GH-124307)
Arguments with the value identical to the default value (e.g. booleans, small integers, empty or 1-character strings) are no longer considered "not present".
Diffstat (limited to 'Lib')
-rw-r--r--Lib/argparse.py5
-rw-r--r--Lib/test/test_argparse.py121
2 files changed, 117 insertions, 9 deletions
diff --git a/Lib/argparse.py b/Lib/argparse.py
index 98d6531..7f6b31b 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -1949,9 +1949,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
argument_values = self._get_values(action, argument_strings)
# error if this argument is not allowed with other previously
- # seen arguments, assuming that actions that use the default
- # value don't really count as "present"
- if argument_values is not action.default:
+ # seen arguments
+ if action.option_strings or argument_strings:
seen_non_default_actions.add(action)
for conflict_action in action_conflicts.get(action, []):
if conflict_action in seen_non_default_actions:
diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py
index f51a690..2d8905e 100644
--- a/Lib/test/test_argparse.py
+++ b/Lib/test/test_argparse.py
@@ -2879,26 +2879,30 @@ class MEMixin(object):
parse_args = self.get_parser(required=False).parse_args
error = ArgumentParserError
for args_string in self.failures:
- self.assertRaises(error, parse_args, args_string.split())
+ with self.subTest(args=args_string):
+ self.assertRaises(error, parse_args, args_string.split())
def test_failures_when_required(self):
parse_args = self.get_parser(required=True).parse_args
error = ArgumentParserError
for args_string in self.failures + ['']:
- self.assertRaises(error, parse_args, args_string.split())
+ with self.subTest(args=args_string):
+ self.assertRaises(error, parse_args, args_string.split())
def test_successes_when_not_required(self):
parse_args = self.get_parser(required=False).parse_args
successes = self.successes + self.successes_when_not_required
for args_string, expected_ns in successes:
- actual_ns = parse_args(args_string.split())
- self.assertEqual(actual_ns, expected_ns)
+ with self.subTest(args=args_string):
+ actual_ns = parse_args(args_string.split())
+ self.assertEqual(actual_ns, expected_ns)
def test_successes_when_required(self):
parse_args = self.get_parser(required=True).parse_args
for args_string, expected_ns in self.successes:
- actual_ns = parse_args(args_string.split())
- self.assertEqual(actual_ns, expected_ns)
+ with self.subTest(args=args_string):
+ actual_ns = parse_args(args_string.split())
+ self.assertEqual(actual_ns, expected_ns)
def test_usage_when_not_required(self):
format_usage = self.get_parser(required=False).format_usage
@@ -3285,6 +3289,111 @@ class TestMutuallyExclusiveNested(MEMixin, TestCase):
test_successes_when_not_required = None
test_successes_when_required = None
+
+class TestMutuallyExclusiveOptionalOptional(MEMixin, TestCase):
+ def get_parser(self, required=None):
+ parser = ErrorRaisingArgumentParser(prog='PROG')
+ group = parser.add_mutually_exclusive_group(required=required)
+ group.add_argument('--foo')
+ group.add_argument('--bar', nargs='?')
+ return parser
+
+ failures = [
+ '--foo X --bar Y',
+ '--foo X --bar',
+ ]
+ successes = [
+ ('--foo X', NS(foo='X', bar=None)),
+ ('--bar X', NS(foo=None, bar='X')),
+ ('--bar', NS(foo=None, bar=None)),
+ ]
+ successes_when_not_required = [
+ ('', NS(foo=None, bar=None)),
+ ]
+ usage_when_required = '''\
+ usage: PROG [-h] (--foo FOO | --bar [BAR])
+ '''
+ usage_when_not_required = '''\
+ usage: PROG [-h] [--foo FOO | --bar [BAR]]
+ '''
+ help = '''\
+
+ options:
+ -h, --help show this help message and exit
+ --foo FOO
+ --bar [BAR]
+ '''
+
+
+class TestMutuallyExclusiveOptionalWithDefault(MEMixin, TestCase):
+ def get_parser(self, required=None):
+ parser = ErrorRaisingArgumentParser(prog='PROG')
+ group = parser.add_mutually_exclusive_group(required=required)
+ group.add_argument('--foo')
+ group.add_argument('--bar', type=bool, default=True)
+ return parser
+
+ failures = [
+ '--foo X --bar Y',
+ '--foo X --bar=',
+ ]
+ successes = [
+ ('--foo X', NS(foo='X', bar=True)),
+ ('--bar X', NS(foo=None, bar=True)),
+ ('--bar=', NS(foo=None, bar=False)),
+ ]
+ successes_when_not_required = [
+ ('', NS(foo=None, bar=True)),
+ ]
+ usage_when_required = '''\
+ usage: PROG [-h] (--foo FOO | --bar BAR)
+ '''
+ usage_when_not_required = '''\
+ usage: PROG [-h] [--foo FOO | --bar BAR]
+ '''
+ help = '''\
+
+ options:
+ -h, --help show this help message and exit
+ --foo FOO
+ --bar BAR
+ '''
+
+
+class TestMutuallyExclusivePositionalWithDefault(MEMixin, TestCase):
+ def get_parser(self, required=None):
+ parser = ErrorRaisingArgumentParser(prog='PROG')
+ group = parser.add_mutually_exclusive_group(required=required)
+ group.add_argument('--foo')
+ group.add_argument('bar', nargs='?', type=bool, default=True)
+ return parser
+
+ failures = [
+ '--foo X Y',
+ ]
+ successes = [
+ ('--foo X', NS(foo='X', bar=True)),
+ ('X', NS(foo=None, bar=True)),
+ ]
+ successes_when_not_required = [
+ ('', NS(foo=None, bar=True)),
+ ]
+ usage_when_required = '''\
+ usage: PROG [-h] (--foo FOO | bar)
+ '''
+ usage_when_not_required = '''\
+ usage: PROG [-h] [--foo FOO | bar]
+ '''
+ help = '''\
+
+ positional arguments:
+ bar
+
+ options:
+ -h, --help show this help message and exit
+ --foo FOO
+ '''
+
# =================================================
# Mutually exclusive group in parent parser tests
# =================================================