Skip to content

Commit 167d8d2

Browse files
[3.13] gh-63143: Fix parsing mutually exclusive arguments in argparse (GH-124307) (GH-124418)
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 6387016 commit 167d8d2

File tree

3 files changed

+120
-9
lines changed

3 files changed

+120
-9
lines changed

Lib/argparse.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,9 +1977,8 @@ def take_action(action, argument_strings, option_string=None):
19771977
argument_values = self._get_values(action, argument_strings)
19781978

19791979
# error if this argument is not allowed with other previously
1980-
# seen arguments, assuming that actions that use the default
1981-
# value don't really count as "present"
1982-
if argument_values is not action.default:
1980+
# seen arguments
1981+
if action.option_strings or argument_strings:
19831982
seen_non_default_actions.add(action)
19841983
for conflict_action in action_conflicts.get(action, []):
19851984
if conflict_action in seen_non_default_actions:

Lib/test/test_argparse.py

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2900,26 +2900,30 @@ def test_failures_when_not_required(self):
29002900
parse_args = self.get_parser(required=False).parse_args
29012901
error = ArgumentParserError
29022902
for args_string in self.failures:
2903-
self.assertRaises(error, parse_args, args_string.split())
2903+
with self.subTest(args=args_string):
2904+
self.assertRaises(error, parse_args, args_string.split())
29042905

29052906
def test_failures_when_required(self):
29062907
parse_args = self.get_parser(required=True).parse_args
29072908
error = ArgumentParserError
29082909
for args_string in self.failures + ['']:
2909-
self.assertRaises(error, parse_args, args_string.split())
2910+
with self.subTest(args=args_string):
2911+
self.assertRaises(error, parse_args, args_string.split())
29102912

29112913
def test_successes_when_not_required(self):
29122914
parse_args = self.get_parser(required=False).parse_args
29132915
successes = self.successes + self.successes_when_not_required
29142916
for args_string, expected_ns in successes:
2915-
actual_ns = parse_args(args_string.split())
2916-
self.assertEqual(actual_ns, expected_ns)
2917+
with self.subTest(args=args_string):
2918+
actual_ns = parse_args(args_string.split())
2919+
self.assertEqual(actual_ns, expected_ns)
29172920

29182921
def test_successes_when_required(self):
29192922
parse_args = self.get_parser(required=True).parse_args
29202923
for args_string, expected_ns in self.successes:
2921-
actual_ns = parse_args(args_string.split())
2922-
self.assertEqual(actual_ns, expected_ns)
2924+
with self.subTest(args=args_string):
2925+
actual_ns = parse_args(args_string.split())
2926+
self.assertEqual(actual_ns, expected_ns)
29232927

29242928
def test_usage_when_not_required(self):
29252929
format_usage = self.get_parser(required=False).format_usage
@@ -3306,6 +3310,111 @@ def get_parser(self, required):
33063310
test_successes_when_not_required = None
33073311
test_successes_when_required = None
33083312

3313+
3314+
class TestMutuallyExclusiveOptionalOptional(MEMixin, TestCase):
3315+
def get_parser(self, required=None):
3316+
parser = ErrorRaisingArgumentParser(prog='PROG')
3317+
group = parser.add_mutually_exclusive_group(required=required)
3318+
group.add_argument('--foo')
3319+
group.add_argument('--bar', nargs='?')
3320+
return parser
3321+
3322+
failures = [
3323+
'--foo X --bar Y',
3324+
'--foo X --bar',
3325+
]
3326+
successes = [
3327+
('--foo X', NS(foo='X', bar=None)),
3328+
('--bar X', NS(foo=None, bar='X')),
3329+
('--bar', NS(foo=None, bar=None)),
3330+
]
3331+
successes_when_not_required = [
3332+
('', NS(foo=None, bar=None)),
3333+
]
3334+
usage_when_required = '''\
3335+
usage: PROG [-h] (--foo FOO | --bar [BAR])
3336+
'''
3337+
usage_when_not_required = '''\
3338+
usage: PROG [-h] [--foo FOO | --bar [BAR]]
3339+
'''
3340+
help = '''\
3341+
3342+
options:
3343+
-h, --help show this help message and exit
3344+
--foo FOO
3345+
--bar [BAR]
3346+
'''
3347+
3348+
3349+
class TestMutuallyExclusiveOptionalWithDefault(MEMixin, TestCase):
3350+
def get_parser(self, required=None):
3351+
parser = ErrorRaisingArgumentParser(prog='PROG')
3352+
group = parser.add_mutually_exclusive_group(required=required)
3353+
group.add_argument('--foo')
3354+
group.add_argument('--bar', type=bool, default=True)
3355+
return parser
3356+
3357+
failures = [
3358+
'--foo X --bar Y',
3359+
'--foo X --bar=',
3360+
]
3361+
successes = [
3362+
('--foo X', NS(foo='X', bar=True)),
3363+
('--bar X', NS(foo=None, bar=True)),
3364+
('--bar=', NS(foo=None, bar=False)),
3365+
]
3366+
successes_when_not_required = [
3367+
('', NS(foo=None, bar=True)),
3368+
]
3369+
usage_when_required = '''\
3370+
usage: PROG [-h] (--foo FOO | --bar BAR)
3371+
'''
3372+
usage_when_not_required = '''\
3373+
usage: PROG [-h] [--foo FOO | --bar BAR]
3374+
'''
3375+
help = '''\
3376+
3377+
options:
3378+
-h, --help show this help message and exit
3379+
--foo FOO
3380+
--bar BAR
3381+
'''
3382+
3383+
3384+
class TestMutuallyExclusivePositionalWithDefault(MEMixin, TestCase):
3385+
def get_parser(self, required=None):
3386+
parser = ErrorRaisingArgumentParser(prog='PROG')
3387+
group = parser.add_mutually_exclusive_group(required=required)
3388+
group.add_argument('--foo')
3389+
group.add_argument('bar', nargs='?', type=bool, default=True)
3390+
return parser
3391+
3392+
failures = [
3393+
'--foo X Y',
3394+
]
3395+
successes = [
3396+
('--foo X', NS(foo='X', bar=True)),
3397+
('X', NS(foo=None, bar=True)),
3398+
]
3399+
successes_when_not_required = [
3400+
('', NS(foo=None, bar=True)),
3401+
]
3402+
usage_when_required = '''\
3403+
usage: PROG [-h] (--foo FOO | bar)
3404+
'''
3405+
usage_when_not_required = '''\
3406+
usage: PROG [-h] [--foo FOO | bar]
3407+
'''
3408+
help = '''\
3409+
3410+
positional arguments:
3411+
bar
3412+
3413+
options:
3414+
-h, --help show this help message and exit
3415+
--foo FOO
3416+
'''
3417+
33093418
# =================================================
33103419
# Mutually exclusive group in parent parser tests
33113420
# =================================================
Lines changed: 3 additions & 0 deletions
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)