From f28409cb8ccf81b13bff1f2afedec0cb3a78732d Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:35:01 +0100 Subject: [PATCH] [3.13] GH-75949: Fix argparse dropping '|' in mutually exclusive groups on line wrap (GH-142312) (#142348) --- Lib/argparse.py | 33 ++++++++++++++++--- Lib/test/test_argparse.py | 19 +++++++++++ ...5-12-05-16-39-17.gh-issue-75949.pHxW98.rst | 1 + 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-05-16-39-17.gh-issue-75949.pHxW98.rst diff --git a/Lib/argparse.py b/Lib/argparse.py index bd088ea0e66a..adb35dd0180c 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -330,8 +330,14 @@ class HelpFormatter(object): if len(prefix) + len(usage) > text_width: # break usage into wrappable parts - opt_parts = self._get_actions_usage_parts(optionals, groups) - pos_parts = self._get_actions_usage_parts(positionals, groups) + # keep optionals and positionals together to preserve + # mutually exclusive group formatting (gh-75949) + all_actions = optionals + positionals + parts, pos_start = self._get_actions_usage_parts_with_split( + all_actions, groups, len(optionals) + ) + opt_parts = parts[:pos_start] + pos_parts = parts[pos_start:] # helper for wrapping lines def get_lines(parts, indent, prefix=None): @@ -387,6 +393,17 @@ class HelpFormatter(object): return ' '.join(self._get_actions_usage_parts(actions, groups)) def _get_actions_usage_parts(self, actions, groups): + parts, _ = self._get_actions_usage_parts_with_split(actions, groups) + return parts + + def _get_actions_usage_parts_with_split(self, actions, groups, opt_count=None): + """Get usage parts with split index for optionals/positionals. + + Returns (parts, pos_start) where pos_start is the index in parts + where positionals begin. When opt_count is None, pos_start is None. + This preserves mutually exclusive group formatting across the + optionals/positionals boundary (gh-75949). + """ # find group indices and identify actions in groups group_actions = set() inserts = {} @@ -469,8 +486,16 @@ class HelpFormatter(object): for i in range(start + group_size, end): parts[i] = None - # return the usage parts - return [item for item in parts if item is not None] + # if opt_count is provided, calculate where positionals start in + # the final parts list (for wrapping onto separate lines). + # Count before filtering None entries since indices shift after. + if opt_count is not None: + pos_start = sum(1 for p in parts[:opt_count] if p is not None) + else: + pos_start = None + + # return the usage parts and split point (gh-75949) + return [item for item in parts if item is not None], pos_start def _format_text(self, text): if '%(prog)' in text: diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index b7e995334fed..8974247e6e95 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -4700,6 +4700,25 @@ class TestHelpUsageNoWhitespaceCrash(TestCase): ''') self.assertEqual(parser.format_usage(), usage) + def test_mutex_groups_with_mixed_optionals_positionals_wrap(self): + # https://github.com/python/cpython/issues/75949 + # Mutually exclusive groups containing both optionals and positionals + # should preserve pipe separators when the usage line wraps. + parser = argparse.ArgumentParser(prog='PROG') + g = parser.add_mutually_exclusive_group() + g.add_argument('-v', '--verbose', action='store_true') + g.add_argument('-q', '--quiet', action='store_true') + g.add_argument('-x', '--extra-long-option-name', nargs='?') + g.add_argument('-y', '--yet-another-long-option', nargs='?') + g.add_argument('positional', nargs='?') + + usage = textwrap.dedent('''\ + usage: PROG [-h] [-v | -q | -x [EXTRA_LONG_OPTION_NAME] | + -y [YET_ANOTHER_LONG_OPTION] | + positional] + ''') + self.assertEqual(parser.format_usage(), usage) + class TestHelpVariableExpansion(HelpTestCase): """Test that variables are expanded properly in help messages""" diff --git a/Misc/NEWS.d/next/Library/2025-12-05-16-39-17.gh-issue-75949.pHxW98.rst b/Misc/NEWS.d/next/Library/2025-12-05-16-39-17.gh-issue-75949.pHxW98.rst new file mode 100644 index 000000000000..5ca3fc05b981 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-05-16-39-17.gh-issue-75949.pHxW98.rst @@ -0,0 +1 @@ +Fix :mod:`argparse` to preserve ``|`` separators in mutually exclusive groups when the usage line wraps due to length. -- 2.47.3