Skip to content

Commit e6334f1

Browse files
Merge branch 'main' into fix-help-format-color
2 parents 9b1975b + f193c8f commit e6334f1

5 files changed

Lines changed: 140 additions & 151 deletions

File tree

Lib/argparse.py

Lines changed: 92 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -336,31 +336,15 @@ def _format_usage(self, usage, actions, groups, prefix):
336336
elif usage is None:
337337
prog = '%(prog)s' % dict(prog=self._prog)
338338

339-
# split optionals from positionals
340-
optionals = []
341-
positionals = []
342-
for action in actions:
343-
if action.option_strings:
344-
optionals.append(action)
345-
else:
346-
positionals.append(action)
347-
339+
parts, pos_start = self._get_actions_usage_parts(actions, groups)
348340
# build full usage string
349-
format = self._format_actions_usage
350-
action_usage = format(optionals + positionals, groups)
351-
usage = ' '.join([s for s in [prog, action_usage] if s])
341+
usage = ' '.join(filter(None, [prog, *parts]))
352342

353343
# wrap the usage parts if it's too long
354344
text_width = self._width - self._current_indent
355345
if len(prefix) + len(self._decolor(usage)) > text_width:
356346

357347
# break usage into wrappable parts
358-
# keep optionals and positionals together to preserve
359-
# mutually exclusive group formatting (gh-75949)
360-
all_actions = optionals + positionals
361-
parts, pos_start = self._get_actions_usage_parts_with_split(
362-
all_actions, groups, len(optionals)
363-
)
364348
opt_parts = parts[:pos_start]
365349
pos_parts = parts[pos_start:]
366350

@@ -419,125 +403,114 @@ def get_lines(parts, indent, prefix=None):
419403
# prefix with 'usage:'
420404
return f'{t.usage}{prefix}{t.reset}{usage}\n\n'
421405

422-
def _format_actions_usage(self, actions, groups):
423-
return ' '.join(self._get_actions_usage_parts(actions, groups))
424-
425406
def _is_long_option(self, string):
426407
return len(string) > 2
427408

428409
def _get_actions_usage_parts(self, actions, groups):
429-
parts, _ = self._get_actions_usage_parts_with_split(actions, groups)
430-
return parts
431-
432-
def _get_actions_usage_parts_with_split(self, actions, groups, opt_count=None):
433410
"""Get usage parts with split index for optionals/positionals.
434411
435412
Returns (parts, pos_start) where pos_start is the index in parts
436-
where positionals begin. When opt_count is None, pos_start is None.
413+
where positionals begin.
437414
This preserves mutually exclusive group formatting across the
438415
optionals/positionals boundary (gh-75949).
439416
"""
440-
# find group indices and identify actions in groups
441-
group_actions = set()
442-
inserts = {}
417+
actions = [action for action in actions if action.help is not SUPPRESS]
418+
# group actions by mutually exclusive groups
419+
action_groups = dict.fromkeys(actions)
443420
for group in groups:
444-
if not group._group_actions:
445-
raise ValueError(f'empty group {group}')
446-
447-
if all(action.help is SUPPRESS for action in group._group_actions):
448-
continue
449-
450-
try:
451-
start = min(actions.index(item) for item in group._group_actions)
452-
except ValueError:
453-
continue
454-
else:
455-
end = start + len(group._group_actions)
456-
if set(actions[start:end]) == set(group._group_actions):
457-
group_actions.update(group._group_actions)
458-
inserts[start, end] = group
421+
for action in group._group_actions:
422+
if action in action_groups:
423+
action_groups[action] = group
424+
# positional arguments keep their position
425+
positionals = []
426+
for action in actions:
427+
if not action.option_strings:
428+
group = action_groups.pop(action)
429+
if group:
430+
group_actions = [
431+
action2 for action2 in group._group_actions
432+
if action2.option_strings and
433+
action_groups.pop(action2, None)
434+
] + [action]
435+
positionals.append((group.required, group_actions))
436+
else:
437+
positionals.append((None, [action]))
438+
# the remaining optional arguments are sorted by the position of
439+
# the first option in the group
440+
optionals = []
441+
for action in actions:
442+
if action.option_strings and action in action_groups:
443+
group = action_groups.pop(action)
444+
if group:
445+
group_actions = [action] + [
446+
action2 for action2 in group._group_actions
447+
if action2.option_strings and
448+
action_groups.pop(action2, None)
449+
]
450+
optionals.append((group.required, group_actions))
451+
else:
452+
optionals.append((None, [action]))
459453

460454
# collect all actions format strings
461455
parts = []
462456
t = self._theme
463-
for action in actions:
464-
465-
# suppressed arguments are marked with None
466-
if action.help is SUPPRESS:
467-
part = None
468-
469-
# produce all arg strings
470-
elif not action.option_strings:
471-
default = self._get_default_metavar_for_positional(action)
472-
part = self._format_args(action, default)
473-
# if it's in a group, strip the outer []
474-
if action in group_actions:
475-
if part[0] == '[' and part[-1] == ']':
476-
part = part[1:-1]
477-
part = t.summary_action + part + t.reset
478-
479-
# produce the first way to invoke the option in brackets
480-
else:
481-
option_string = action.option_strings[0]
482-
if self._is_long_option(option_string):
483-
option_color = t.summary_long_option
457+
pos_start = None
458+
for i, (required, group) in enumerate(optionals + positionals):
459+
start = len(parts)
460+
if i == len(optionals):
461+
pos_start = start
462+
in_group = len(group) > 1
463+
for action in group:
464+
# produce all arg strings
465+
if not action.option_strings:
466+
default = self._get_default_metavar_for_positional(action)
467+
part = self._format_args(action, default)
468+
# if it's in a group, strip the outer []
469+
if in_group:
470+
if part[0] == '[' and part[-1] == ']':
471+
part = part[1:-1]
472+
part = t.summary_action + part + t.reset
473+
474+
# produce the first way to invoke the option in brackets
484475
else:
485-
option_color = t.summary_short_option
486-
487-
# if the Optional doesn't take a value, format is:
488-
# -s or --long
489-
if action.nargs == 0:
490-
part = action.format_usage()
491-
part = f"{option_color}{part}{t.reset}"
492-
493-
# if the Optional takes a value, format is:
494-
# -s ARGS or --long ARGS
495-
else:
496-
default = self._get_default_metavar_for_optional(action)
497-
args_string = self._format_args(action, default)
498-
part = (
499-
f"{option_color}{option_string} "
500-
f"{t.summary_label}{args_string}{t.reset}"
501-
)
502-
503-
# make it look optional if it's not required or in a group
504-
if not action.required and action not in group_actions:
505-
part = '[%s]' % part
476+
option_string = action.option_strings[0]
477+
if self._is_long_option(option_string):
478+
option_color = t.summary_long_option
479+
else:
480+
option_color = t.summary_short_option
506481

507-
# add the action string to the list
508-
parts.append(part)
482+
# if the Optional doesn't take a value, format is:
483+
# -s or --long
484+
if action.nargs == 0:
485+
part = action.format_usage()
486+
part = f"{option_color}{part}{t.reset}"
509487

510-
# group mutually exclusive actions
511-
inserted_separators_indices = set()
512-
for start, end in sorted(inserts, reverse=True):
513-
group = inserts[start, end]
514-
group_parts = [item for item in parts[start:end] if item is not None]
515-
group_size = len(group_parts)
516-
if group.required:
517-
open, close = "()" if group_size > 1 else ("", "")
518-
else:
519-
open, close = "[]"
520-
group_parts[0] = open + group_parts[0]
521-
group_parts[-1] = group_parts[-1] + close
522-
for i, part in enumerate(group_parts[:-1], start=start):
523-
# insert a separator if not already done in a nested group
524-
if i not in inserted_separators_indices:
525-
parts[i] = part + ' |'
526-
inserted_separators_indices.add(i)
527-
parts[start + group_size - 1] = group_parts[-1]
528-
for i in range(start + group_size, end):
529-
parts[i] = None
530-
531-
# if opt_count is provided, calculate where positionals start in
532-
# the final parts list (for wrapping onto separate lines).
533-
# Count before filtering None entries since indices shift after.
534-
if opt_count is not None:
535-
pos_start = sum(1 for p in parts[:opt_count] if p is not None)
536-
else:
537-
pos_start = None
538-
539-
# return the usage parts and split point (gh-75949)
540-
return [item for item in parts if item is not None], pos_start
488+
# if the Optional takes a value, format is:
489+
# -s ARGS or --long ARGS
490+
else:
491+
default = self._get_default_metavar_for_optional(action)
492+
args_string = self._format_args(action, default)
493+
part = (
494+
f"{option_color}{option_string} "
495+
f"{t.summary_label}{args_string}{t.reset}"
496+
)
497+
498+
# make it look optional if it's not required or in a group
499+
if not (action.required or required or in_group):
500+
part = '[%s]' % part
501+
502+
# add the action string to the list
503+
parts.append(part)
504+
505+
if in_group:
506+
parts[start] = ('(' if required else '[') + parts[start]
507+
for i in range(start, len(parts) - 1):
508+
parts[i] += ' |'
509+
parts[-1] += ')' if required else ']'
510+
511+
if pos_start is None:
512+
pos_start = len(parts)
513+
return parts, pos_start
541514

542515
def _format_text(self, text):
543516
if '%(prog)' in text:

Lib/test/test_argparse.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3398,12 +3398,11 @@ def test_help_subparser_all_mutually_exclusive_group_members_suppressed(self):
33983398
'''
33993399
self.assertEqual(cmd_foo.format_help(), textwrap.dedent(expected))
34003400

3401-
def test_empty_group(self):
3401+
def test_usage_empty_group(self):
34023402
# See issue 26952
3403-
parser = argparse.ArgumentParser()
3403+
parser = ErrorRaisingArgumentParser(prog='PROG')
34043404
group = parser.add_mutually_exclusive_group()
3405-
with self.assertRaises(ValueError):
3406-
parser.parse_args(['-h'])
3405+
self.assertEqual(parser.format_usage(), 'usage: PROG [-h]\n')
34073406

34083407
def test_nested_mutex_groups(self):
34093408
parser = argparse.ArgumentParser(prog='PROG')
@@ -3671,25 +3670,29 @@ def get_parser(self, required):
36713670
group.add_argument('-b', action='store_true', help='b help')
36723671
parser.add_argument('-y', action='store_true', help='y help')
36733672
group.add_argument('-c', action='store_true', help='c help')
3673+
parser.add_argument('-z', action='store_true', help='z help')
36743674
return parser
36753675

36763676
failures = ['-a -b', '-b -c', '-a -c', '-a -b -c']
36773677
successes = [
3678-
('-a', NS(a=True, b=False, c=False, x=False, y=False)),
3679-
('-b', NS(a=False, b=True, c=False, x=False, y=False)),
3680-
('-c', NS(a=False, b=False, c=True, x=False, y=False)),
3681-
('-a -x', NS(a=True, b=False, c=False, x=True, y=False)),
3682-
('-y -b', NS(a=False, b=True, c=False, x=False, y=True)),
3683-
('-x -y -c', NS(a=False, b=False, c=True, x=True, y=True)),
3678+
('-a', NS(a=True, b=False, c=False, x=False, y=False, z=False)),
3679+
('-b', NS(a=False, b=True, c=False, x=False, y=False, z=False)),
3680+
('-c', NS(a=False, b=False, c=True, x=False, y=False, z=False)),
3681+
('-a -x', NS(a=True, b=False, c=False, x=True, y=False, z=False)),
3682+
('-y -b', NS(a=False, b=True, c=False, x=False, y=True, z=False)),
3683+
('-x -y -c', NS(a=False, b=False, c=True, x=True, y=True, z=False)),
36843684
]
36853685
successes_when_not_required = [
3686-
('', NS(a=False, b=False, c=False, x=False, y=False)),
3687-
('-x', NS(a=False, b=False, c=False, x=True, y=False)),
3688-
('-y', NS(a=False, b=False, c=False, x=False, y=True)),
3686+
('', NS(a=False, b=False, c=False, x=False, y=False, z=False)),
3687+
('-x', NS(a=False, b=False, c=False, x=True, y=False, z=False)),
3688+
('-y', NS(a=False, b=False, c=False, x=False, y=True, z=False)),
36893689
]
36903690

3691-
usage_when_required = usage_when_not_required = '''\
3692-
usage: PROG [-h] [-x] [-a] [-b] [-y] [-c]
3691+
usage_when_not_required = '''\
3692+
usage: PROG [-h] [-x] [-a | -b | -c] [-y] [-z]
3693+
'''
3694+
usage_when_required = '''\
3695+
usage: PROG [-h] [-x] (-a | -b | -c) [-y] [-z]
36933696
'''
36943697
help = '''\
36953698
@@ -3700,6 +3703,7 @@ def get_parser(self, required):
37003703
-b b help
37013704
-y y help
37023705
-c c help
3706+
-z z help
37033707
'''
37043708

37053709

@@ -3753,23 +3757,27 @@ def get_parser(self, required):
37533757
group.add_argument('a', nargs='?', help='a help')
37543758
group.add_argument('-b', action='store_true', help='b help')
37553759
group.add_argument('-c', action='store_true', help='c help')
3760+
parser.add_argument('-z', action='store_true', help='z help')
37563761
return parser
37573762

37583763
failures = ['X A -b', '-b -c', '-c X A']
37593764
successes = [
3760-
('X A', NS(a='A', b=False, c=False, x='X', y=False)),
3761-
('X -b', NS(a=None, b=True, c=False, x='X', y=False)),
3762-
('X -c', NS(a=None, b=False, c=True, x='X', y=False)),
3763-
('X A -y', NS(a='A', b=False, c=False, x='X', y=True)),
3764-
('X -y -b', NS(a=None, b=True, c=False, x='X', y=True)),
3765+
('X A', NS(a='A', b=False, c=False, x='X', y=False, z=False)),
3766+
('X -b', NS(a=None, b=True, c=False, x='X', y=False, z=False)),
3767+
('X -c', NS(a=None, b=False, c=True, x='X', y=False, z=False)),
3768+
('X A -y', NS(a='A', b=False, c=False, x='X', y=True, z=False)),
3769+
('X -y -b', NS(a=None, b=True, c=False, x='X', y=True, z=False)),
37653770
]
37663771
successes_when_not_required = [
3767-
('X', NS(a=None, b=False, c=False, x='X', y=False)),
3768-
('X -y', NS(a=None, b=False, c=False, x='X', y=True)),
3772+
('X', NS(a=None, b=False, c=False, x='X', y=False, z=False)),
3773+
('X -y', NS(a=None, b=False, c=False, x='X', y=True, z=False)),
37693774
]
37703775

3771-
usage_when_required = usage_when_not_required = '''\
3772-
usage: PROG [-h] [-y] [-b] [-c] x [a]
3776+
usage_when_not_required = '''\
3777+
usage: PROG [-h] [-y] [-z] x [-b | -c | a]
3778+
'''
3779+
usage_when_required = '''\
3780+
usage: PROG [-h] [-y] [-z] x (-b | -c | a)
37733781
'''
37743782
help = '''\
37753783
@@ -3782,6 +3790,7 @@ def get_parser(self, required):
37823790
-y y help
37833791
-b b help
37843792
-c c help
3793+
-z z help
37853794
'''
37863795

37873796

@@ -4989,9 +4998,9 @@ def test_mutex_groups_with_mixed_optionals_positionals_wrap(self):
49894998
g.add_argument('positional', nargs='?')
49904999

49915000
usage = textwrap.dedent('''\
4992-
usage: PROG [-h] [-v | -q | -x [EXTRA_LONG_OPTION_NAME] |
4993-
-y [YET_ANOTHER_LONG_OPTION] |
4994-
positional]
5001+
usage: PROG [-h]
5002+
[-v | -q | -x [EXTRA_LONG_OPTION_NAME] |
5003+
-y [YET_ANOTHER_LONG_OPTION] | positional]
49955004
''')
49965005
self.assertEqual(parser.format_usage(), usage)
49975006

Lib/test/test_ast/test_ast.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,8 @@ def next(self):
992992
@skip_wasi_stack_overflow()
993993
@skip_emscripten_stack_overflow()
994994
def test_ast_recursion_limit(self):
995-
crash_depth = 500_000
995+
# Android test devices have less memory.
996+
crash_depth = 100_000 if sys.platform == "android" else 500_000
996997
success_depth = 200
997998
if _testinternalcapi is not None:
998999
remaining = _testinternalcapi.get_c_recursion_remaining()

0 commit comments

Comments
 (0)