@@ -353,8 +353,14 @@ def _format_usage(self, usage, actions, groups, prefix):
353353 if len (prefix ) + len (self ._decolor (usage )) > text_width :
354354
355355 # break usage into wrappable parts
356- opt_parts = self ._get_actions_usage_parts (optionals , groups )
357- pos_parts = self ._get_actions_usage_parts (positionals , groups )
356+ # keep optionals and positionals together to preserve
357+ # mutually exclusive group formatting (gh-75949)
358+ all_actions = optionals + positionals
359+ parts , pos_start = self ._get_actions_usage_parts_with_split (
360+ all_actions , groups , len (optionals )
361+ )
362+ opt_parts = parts [:pos_start ]
363+ pos_parts = parts [pos_start :]
358364
359365 # helper for wrapping lines
360366 def get_lines (parts , indent , prefix = None ):
@@ -418,6 +424,17 @@ def _is_long_option(self, string):
418424 return len (string ) > 2
419425
420426 def _get_actions_usage_parts (self , actions , groups ):
427+ parts , _ = self ._get_actions_usage_parts_with_split (actions , groups )
428+ return parts
429+
430+ def _get_actions_usage_parts_with_split (self , actions , groups , opt_count = None ):
431+ """Get usage parts with split index for optionals/positionals.
432+
433+ Returns (parts, pos_start) where pos_start is the index in parts
434+ where positionals begin. When opt_count is None, pos_start is None.
435+ This preserves mutually exclusive group formatting across the
436+ optionals/positionals boundary (gh-75949).
437+ """
421438 # find group indices and identify actions in groups
422439 group_actions = set ()
423440 inserts = {}
@@ -513,8 +530,16 @@ def _get_actions_usage_parts(self, actions, groups):
513530 for i in range (start + group_size , end ):
514531 parts [i ] = None
515532
516- # return the usage parts
517- return [item for item in parts if item is not None ]
533+ # if opt_count is provided, calculate where positionals start in
534+ # the final parts list (for wrapping onto separate lines).
535+ # Count before filtering None entries since indices shift after.
536+ if opt_count is not None :
537+ pos_start = sum (1 for p in parts [:opt_count ] if p is not None )
538+ else :
539+ pos_start = None
540+
541+ # return the usage parts and split point (gh-75949)
542+ return [item for item in parts if item is not None ], pos_start
518543
519544 def _format_text (self , text ):
520545 if '%(prog)' in text :
0 commit comments