@@ -355,8 +355,14 @@ def _format_usage(self, usage, actions, groups, prefix):
355355 if len (prefix ) + len (self ._decolor (usage )) > text_width :
356356
357357 # break usage into wrappable parts
358- opt_parts = self ._get_actions_usage_parts (optionals , groups )
359- pos_parts = self ._get_actions_usage_parts (positionals , groups )
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_split (
362+ all_actions , groups , len (optionals )
363+ )
364+ opt_parts = parts [:pos_start ]
365+ pos_parts = parts [pos_start :]
360366
361367 # helper for wrapping lines
362368 def get_lines (parts , indent , prefix = None ):
@@ -420,6 +426,17 @@ def _is_long_option(self, string):
420426 return len (string ) > 2
421427
422428 def _get_actions_usage_parts (self , actions , groups ):
429+ parts , _ = self ._get_actions_usage_parts_split (actions , groups , None )
430+ return parts
431+
432+ def _get_actions_usage_parts_split (self , actions , groups , opt_count ):
433+ """Get usage parts with split index for optionals/positionals.
434+
435+ 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.
437+ This preserves mutually exclusive group formatting across the
438+ optionals/positionals boundary (gh-75949).
439+ """
423440 # find group indices and identify actions in groups
424441 group_actions = set ()
425442 inserts = {}
@@ -515,8 +532,16 @@ def _get_actions_usage_parts(self, actions, groups):
515532 for i in range (start + group_size , end ):
516533 parts [i ] = None
517534
518- # return the usage parts
519- return [item for item in parts if item is not None ]
535+ # calculate the split point for optionals/positionals
536+ # before filtering out None entries
537+ if opt_count is not None :
538+ # Count non-None parts in the optionals section
539+ pos_start = sum (1 for p in parts [:opt_count ] if p is not None )
540+ else :
541+ pos_start = None
542+
543+ # return the usage parts and split point
544+ return [item for item in parts if item is not None ], pos_start
520545
521546 def _format_text (self , text ):
522547 if '%(prog)' in text :
0 commit comments