@@ -356,8 +356,14 @@ def _format_usage(self, usage, actions, groups, prefix):
356356 if len (prefix ) + len (self ._decolor (usage )) > text_width :
357357
358358 # break usage into wrappable parts
359- opt_parts = self ._get_actions_usage_parts (optionals , groups )
360- pos_parts = self ._get_actions_usage_parts (positionals , groups )
359+ # keep optionals and positionals together to preserve
360+ # mutually exclusive group formatting (gh-75949)
361+ all_actions = optionals + positionals
362+ parts , pos_start = self ._get_actions_usage_parts_with_split (
363+ all_actions , groups , len (optionals )
364+ )
365+ opt_parts = parts [:pos_start ]
366+ pos_parts = parts [pos_start :]
361367
362368 # helper for wrapping lines
363369 def get_lines (parts , indent , prefix = None ):
@@ -421,6 +427,17 @@ def _is_long_option(self, string):
421427 return len (string ) > 2
422428
423429 def _get_actions_usage_parts (self , actions , groups ):
430+ parts , _ = self ._get_actions_usage_parts_with_split (actions , groups )
431+ return parts
432+
433+ def _get_actions_usage_parts_with_split (self , actions , groups , opt_count = None ):
434+ """Get usage parts with split index for optionals/positionals.
435+
436+ Returns (parts, pos_start) where pos_start is the index in parts
437+ where positionals begin. When opt_count is None, pos_start is None.
438+ This preserves mutually exclusive group formatting across the
439+ optionals/positionals boundary (gh-75949).
440+ """
424441 # find group indices and identify actions in groups
425442 group_actions = set ()
426443 inserts = {}
@@ -516,8 +533,16 @@ def _get_actions_usage_parts(self, actions, groups):
516533 for i in range (start + group_size , end ):
517534 parts [i ] = None
518535
519- # return the usage parts
520- return [item for item in parts if item is not None ]
536+ # if opt_count is provided, calculate where positionals start in
537+ # the final parts list (for wrapping onto separate lines).
538+ # Count before filtering None entries since indices shift after.
539+ if opt_count is not None :
540+ pos_start = sum (1 for p in parts [:opt_count ] if p is not None )
541+ else :
542+ pos_start = None
543+
544+ # return the usage parts and split point (gh-75949)
545+ return [item for item in parts if item is not None ], pos_start
521546
522547 def _format_text (self , text ):
523548 if '%(prog)' in text :
0 commit comments