@@ -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 :
0 commit comments