Skip to content

Commit f28409c

Browse files
[3.13] GH-75949: Fix argparse dropping '|' in mutually exclusive groups on line wrap (GH-142312) (#142348)
1 parent 462541c commit f28409c

File tree

3 files changed

+49
-4
lines changed

3 files changed

+49
-4
lines changed

Lib/argparse.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,14 @@ def _format_usage(self, usage, actions, groups, prefix):
330330
if len(prefix) + len(usage) > text_width:
331331

332332
# break usage into wrappable parts
333-
opt_parts = self._get_actions_usage_parts(optionals, groups)
334-
pos_parts = self._get_actions_usage_parts(positionals, groups)
333+
# keep optionals and positionals together to preserve
334+
# mutually exclusive group formatting (gh-75949)
335+
all_actions = optionals + positionals
336+
parts, pos_start = self._get_actions_usage_parts_with_split(
337+
all_actions, groups, len(optionals)
338+
)
339+
opt_parts = parts[:pos_start]
340+
pos_parts = parts[pos_start:]
335341

336342
# helper for wrapping lines
337343
def get_lines(parts, indent, prefix=None):
@@ -387,6 +393,17 @@ def _format_actions_usage(self, actions, groups):
387393
return ' '.join(self._get_actions_usage_parts(actions, groups))
388394

389395
def _get_actions_usage_parts(self, actions, groups):
396+
parts, _ = self._get_actions_usage_parts_with_split(actions, groups)
397+
return parts
398+
399+
def _get_actions_usage_parts_with_split(self, actions, groups, opt_count=None):
400+
"""Get usage parts with split index for optionals/positionals.
401+
402+
Returns (parts, pos_start) where pos_start is the index in parts
403+
where positionals begin. When opt_count is None, pos_start is None.
404+
This preserves mutually exclusive group formatting across the
405+
optionals/positionals boundary (gh-75949).
406+
"""
390407
# find group indices and identify actions in groups
391408
group_actions = set()
392409
inserts = {}
@@ -469,8 +486,16 @@ def _get_actions_usage_parts(self, actions, groups):
469486
for i in range(start + group_size, end):
470487
parts[i] = None
471488

472-
# return the usage parts
473-
return [item for item in parts if item is not None]
489+
# if opt_count is provided, calculate where positionals start in
490+
# the final parts list (for wrapping onto separate lines).
491+
# Count before filtering None entries since indices shift after.
492+
if opt_count is not None:
493+
pos_start = sum(1 for p in parts[:opt_count] if p is not None)
494+
else:
495+
pos_start = None
496+
497+
# return the usage parts and split point (gh-75949)
498+
return [item for item in parts if item is not None], pos_start
474499

475500
def _format_text(self, text):
476501
if '%(prog)' in text:

Lib/test/test_argparse.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4700,6 +4700,25 @@ def test_long_mutex_groups_wrap(self):
47004700
''')
47014701
self.assertEqual(parser.format_usage(), usage)
47024702

4703+
def test_mutex_groups_with_mixed_optionals_positionals_wrap(self):
4704+
# https://github.com/python/cpython/issues/75949
4705+
# Mutually exclusive groups containing both optionals and positionals
4706+
# should preserve pipe separators when the usage line wraps.
4707+
parser = argparse.ArgumentParser(prog='PROG')
4708+
g = parser.add_mutually_exclusive_group()
4709+
g.add_argument('-v', '--verbose', action='store_true')
4710+
g.add_argument('-q', '--quiet', action='store_true')
4711+
g.add_argument('-x', '--extra-long-option-name', nargs='?')
4712+
g.add_argument('-y', '--yet-another-long-option', nargs='?')
4713+
g.add_argument('positional', nargs='?')
4714+
4715+
usage = textwrap.dedent('''\
4716+
usage: PROG [-h] [-v | -q | -x [EXTRA_LONG_OPTION_NAME] |
4717+
-y [YET_ANOTHER_LONG_OPTION] |
4718+
positional]
4719+
''')
4720+
self.assertEqual(parser.format_usage(), usage)
4721+
47034722

47044723
class TestHelpVariableExpansion(HelpTestCase):
47054724
"""Test that variables are expanded properly in help messages"""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :mod:`argparse` to preserve ``|`` separators in mutually exclusive groups when the usage line wraps due to length.

0 commit comments

Comments
 (0)