Skip to content

Commit e57fbe3

Browse files
[3.12] gh-63143: Fix parsing mutually exclusive arguments in argparse (GH-124307) (GH-124419)
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". (cherry picked from commit 3094cd1) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 1366fff commit e57fbe3

File tree

3 files changed

+120
-9
lines changed

3 files changed

+120
-9
lines changed

Lib/argparse.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1991,9 +1991,8 @@ def take_action(action, argument_strings, option_string=None):
19911991
argument_values = self._get_values(action, argument_strings)
19921992

19931993
# error if this argument is not allowed with other previously
1994-
# seen arguments, assuming that actions that use the default
1995-
# value don't really count as "present"
1996-
if argument_values is not action.default:
1994+
# seen arguments
1995+
if action.option_strings or argument_strings:
19971996
seen_non_default_actions.add(action)
19981997
for conflict_action in action_conflicts.get(action, []):
19991998
if conflict_action in seen_non_default_actions:

Lib/test/test_argparse.py

+115-6
Original file line numberDiff line numberDiff line change
@@ -2833,26 +2833,30 @@ def test_failures_when_not_required(self):
28332833
parse_args = self.get_parser(required=False).parse_args
28342834
error = ArgumentParserError
28352835
for args_string in self.failures:
2836-
self.assertRaises(error, parse_args, args_string.split())
2836+
with self.subTest(args=args_string):
2837+
self.assertRaises(error, parse_args, args_string.split())
28372838

28382839
def test_failures_when_required(self):
28392840
parse_args = self.get_parser(required=True).parse_args
28402841
error = ArgumentParserError
28412842
for args_string in self.failures + ['']:
2842-
self.assertRaises(error, parse_args, args_string.split())
2843+
with self.subTest(args=args_string):
2844+
self.assertRaises(error, parse_args, args_string.split())
28432845

28442846
def test_successes_when_not_required(self):
28452847
parse_args = self.get_parser(required=False).parse_args
28462848
successes = self.successes + self.successes_when_not_required
28472849
for args_string, expected_ns in successes:
2848-
actual_ns = parse_args(args_string.split())
2849-
self.assertEqual(actual_ns, expected_ns)
2850+
with self.subTest(args=args_string):
2851+
actual_ns = parse_args(args_string.split())
2852+
self.assertEqual(actual_ns, expected_ns)
28502853

28512854
def test_successes_when_required(self):
28522855
parse_args = self.get_parser(required=True).parse_args
28532856
for args_string, expected_ns in self.successes:
2854-
actual_ns = parse_args(args_string.split())
2855-
self.assertEqual(actual_ns, expected_ns)
2857+
with self.subTest(args=args_string):
2858+
actual_ns = parse_args(args_string.split())
2859+
self.assertEqual(actual_ns, expected_ns)
28562860

28572861
def test_usage_when_not_required(self):
28582862
format_usage = self.get_parser(required=False).format_usage
@@ -3239,6 +3243,111 @@ def get_parser(self, required):
32393243
test_successes_when_not_required = None
32403244
test_successes_when_required = None
32413245

3246+
3247+
class TestMutuallyExclusiveOptionalOptional(MEMixin, TestCase):
3248+
def get_parser(self, required=None):
3249+
parser = ErrorRaisingArgumentParser(prog='PROG')
3250+
group = parser.add_mutually_exclusive_group(required=required)
3251+
group.add_argument('--foo')
3252+
group.add_argument('--bar', nargs='?')
3253+
return parser
3254+
3255+
failures = [
3256+
'--foo X --bar Y',
3257+
'--foo X --bar',
3258+
]
3259+
successes = [
3260+
('--foo X', NS(foo='X', bar=None)),
3261+
('--bar X', NS(foo=None, bar='X')),
3262+
('--bar', NS(foo=None, bar=None)),
3263+
]
3264+
successes_when_not_required = [
3265+
('', NS(foo=None, bar=None)),
3266+
]
3267+
usage_when_required = '''\
3268+
usage: PROG [-h] (--foo FOO | --bar [BAR])
3269+
'''
3270+
usage_when_not_required = '''\
3271+
usage: PROG [-h] [--foo FOO | --bar [BAR]]
3272+
'''
3273+
help = '''\
3274+
3275+
options:
3276+
-h, --help show this help message and exit
3277+
--foo FOO
3278+
--bar [BAR]
3279+
'''
3280+
3281+
3282+
class TestMutuallyExclusiveOptionalWithDefault(MEMixin, TestCase):
3283+
def get_parser(self, required=None):
3284+
parser = ErrorRaisingArgumentParser(prog='PROG')
3285+
group = parser.add_mutually_exclusive_group(required=required)
3286+
group.add_argument('--foo')
3287+
group.add_argument('--bar', type=bool, default=True)
3288+
return parser
3289+
3290+
failures = [
3291+
'--foo X --bar Y',
3292+
'--foo X --bar=',
3293+
]
3294+
successes = [
3295+
('--foo X', NS(foo='X', bar=True)),
3296+
('--bar X', NS(foo=None, bar=True)),
3297+
('--bar=', NS(foo=None, bar=False)),
3298+
]
3299+
successes_when_not_required = [
3300+
('', NS(foo=None, bar=True)),
3301+
]
3302+
usage_when_required = '''\
3303+
usage: PROG [-h] (--foo FOO | --bar BAR)
3304+
'''
3305+
usage_when_not_required = '''\
3306+
usage: PROG [-h] [--foo FOO | --bar BAR]
3307+
'''
3308+
help = '''\
3309+
3310+
options:
3311+
-h, --help show this help message and exit
3312+
--foo FOO
3313+
--bar BAR
3314+
'''
3315+
3316+
3317+
class TestMutuallyExclusivePositionalWithDefault(MEMixin, TestCase):
3318+
def get_parser(self, required=None):
3319+
parser = ErrorRaisingArgumentParser(prog='PROG')
3320+
group = parser.add_mutually_exclusive_group(required=required)
3321+
group.add_argument('--foo')
3322+
group.add_argument('bar', nargs='?', type=bool, default=True)
3323+
return parser
3324+
3325+
failures = [
3326+
'--foo X Y',
3327+
]
3328+
successes = [
3329+
('--foo X', NS(foo='X', bar=True)),
3330+
('X', NS(foo=None, bar=True)),
3331+
]
3332+
successes_when_not_required = [
3333+
('', NS(foo=None, bar=True)),
3334+
]
3335+
usage_when_required = '''\
3336+
usage: PROG [-h] (--foo FOO | bar)
3337+
'''
3338+
usage_when_not_required = '''\
3339+
usage: PROG [-h] [--foo FOO | bar]
3340+
'''
3341+
help = '''\
3342+
3343+
positional arguments:
3344+
bar
3345+
3346+
options:
3347+
-h, --help show this help message and exit
3348+
--foo FOO
3349+
'''
3350+
32423351
# =================================================
32433352
# Mutually exclusive group in parent parser tests
32443353
# =================================================
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix parsing mutually exclusive arguments in :mod:`argparse`. Arguments with
2+
the value identical to the default value (e.g. booleans, small integers,
3+
empty or 1-character strings) are no longer considered "not present".

0 commit comments

Comments
 (0)