@@ -189,6 +189,8 @@ def __init__(
189189 self ._whitespace_matcher = _re .compile (r'\s+' , _re .ASCII )
190190 self ._long_break_matcher = _re .compile (r'\n\n\n+' )
191191
192+ self ._set_color (False )
193+
192194 def _set_color (self , color ):
193195 from _colorize import can_colorize , decolor , get_theme
194196
@@ -334,31 +336,15 @@ def _format_usage(self, usage, actions, groups, prefix):
334336 elif usage is None :
335337 prog = '%(prog)s' % dict (prog = self ._prog )
336338
337- # split optionals from positionals
338- optionals = []
339- positionals = []
340- for action in actions :
341- if action .option_strings :
342- optionals .append (action )
343- else :
344- positionals .append (action )
345-
339+ parts , pos_start = self ._get_actions_usage_parts (actions , groups )
346340 # build full usage string
347- format = self ._format_actions_usage
348- action_usage = format (optionals + positionals , groups )
349- usage = ' ' .join ([s for s in [prog , action_usage ] if s ])
341+ usage = ' ' .join (filter (None , [prog , * parts ]))
350342
351343 # wrap the usage parts if it's too long
352344 text_width = self ._width - self ._current_indent
353345 if len (prefix ) + len (self ._decolor (usage )) > text_width :
354346
355347 # break usage into wrappable parts
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- )
362348 opt_parts = parts [:pos_start ]
363349 pos_parts = parts [pos_start :]
364350
@@ -417,125 +403,114 @@ def get_lines(parts, indent, prefix=None):
417403 # prefix with 'usage:'
418404 return f'{ t .usage } { prefix } { t .reset } { usage } \n \n '
419405
420- def _format_actions_usage (self , actions , groups ):
421- return ' ' .join (self ._get_actions_usage_parts (actions , groups ))
422-
423406 def _is_long_option (self , string ):
424407 return len (string ) > 2
425408
426409 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 ):
431410 """Get usage parts with split index for optionals/positionals.
432411
433412 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.
413+ where positionals begin.
435414 This preserves mutually exclusive group formatting across the
436415 optionals/positionals boundary (gh-75949).
437416 """
438- # find group indices and identify actions in groups
439- group_actions = set ()
440- inserts = {}
417+ actions = [ action for action in actions if action . help is not SUPPRESS ]
418+ # group actions by mutually exclusive groups
419+ action_groups = dict . fromkeys ( actions )
441420 for group in groups :
442- if not group ._group_actions :
443- raise ValueError (f'empty group { group } ' )
444-
445- if all (action .help is SUPPRESS for action in group ._group_actions ):
446- continue
447-
448- try :
449- start = min (actions .index (item ) for item in group ._group_actions )
450- except ValueError :
451- continue
452- else :
453- end = start + len (group ._group_actions )
454- if set (actions [start :end ]) == set (group ._group_actions ):
455- group_actions .update (group ._group_actions )
456- inserts [start , end ] = group
421+ for action in group ._group_actions :
422+ if action in action_groups :
423+ action_groups [action ] = group
424+ # positional arguments keep their position
425+ positionals = []
426+ for action in actions :
427+ if not action .option_strings :
428+ group = action_groups .pop (action )
429+ if group :
430+ group_actions = [
431+ action2 for action2 in group ._group_actions
432+ if action2 .option_strings and
433+ action_groups .pop (action2 , None )
434+ ] + [action ]
435+ positionals .append ((group .required , group_actions ))
436+ else :
437+ positionals .append ((None , [action ]))
438+ # the remaining optional arguments are sorted by the position of
439+ # the first option in the group
440+ optionals = []
441+ for action in actions :
442+ if action .option_strings and action in action_groups :
443+ group = action_groups .pop (action )
444+ if group :
445+ group_actions = [action ] + [
446+ action2 for action2 in group ._group_actions
447+ if action2 .option_strings and
448+ action_groups .pop (action2 , None )
449+ ]
450+ optionals .append ((group .required , group_actions ))
451+ else :
452+ optionals .append ((None , [action ]))
457453
458454 # collect all actions format strings
459455 parts = []
460456 t = self ._theme
461- for action in actions :
462-
463- # suppressed arguments are marked with None
464- if action .help is SUPPRESS :
465- part = None
466-
467- # produce all arg strings
468- elif not action .option_strings :
469- default = self ._get_default_metavar_for_positional (action )
470- part = self ._format_args (action , default )
471- # if it's in a group, strip the outer []
472- if action in group_actions :
473- if part [0 ] == '[' and part [- 1 ] == ']' :
474- part = part [1 :- 1 ]
475- part = t .summary_action + part + t .reset
476-
477- # produce the first way to invoke the option in brackets
478- else :
479- option_string = action .option_strings [0 ]
480- if self ._is_long_option (option_string ):
481- option_color = t .summary_long_option
457+ pos_start = None
458+ for i , (required , group ) in enumerate (optionals + positionals ):
459+ start = len (parts )
460+ if i == len (optionals ):
461+ pos_start = start
462+ in_group = len (group ) > 1
463+ for action in group :
464+ # produce all arg strings
465+ if not action .option_strings :
466+ default = self ._get_default_metavar_for_positional (action )
467+ part = self ._format_args (action , default )
468+ # if it's in a group, strip the outer []
469+ if in_group :
470+ if part [0 ] == '[' and part [- 1 ] == ']' :
471+ part = part [1 :- 1 ]
472+ part = t .summary_action + part + t .reset
473+
474+ # produce the first way to invoke the option in brackets
482475 else :
483- option_color = t .summary_short_option
484-
485- # if the Optional doesn't take a value, format is:
486- # -s or --long
487- if action .nargs == 0 :
488- part = action .format_usage ()
489- part = f"{ option_color } { part } { t .reset } "
490-
491- # if the Optional takes a value, format is:
492- # -s ARGS or --long ARGS
493- else :
494- default = self ._get_default_metavar_for_optional (action )
495- args_string = self ._format_args (action , default )
496- part = (
497- f"{ option_color } { option_string } "
498- f"{ t .summary_label } { args_string } { t .reset } "
499- )
500-
501- # make it look optional if it's not required or in a group
502- if not action .required and action not in group_actions :
503- part = '[%s]' % part
476+ option_string = action .option_strings [0 ]
477+ if self ._is_long_option (option_string ):
478+ option_color = t .summary_long_option
479+ else :
480+ option_color = t .summary_short_option
504481
505- # add the action string to the list
506- parts .append (part )
482+ # if the Optional doesn't take a value, format is:
483+ # -s or --long
484+ if action .nargs == 0 :
485+ part = action .format_usage ()
486+ part = f"{ option_color } { part } { t .reset } "
507487
508- # group mutually exclusive actions
509- inserted_separators_indices = set ()
510- for start , end in sorted (inserts , reverse = True ):
511- group = inserts [start , end ]
512- group_parts = [item for item in parts [start :end ] if item is not None ]
513- group_size = len (group_parts )
514- if group .required :
515- open , close = "()" if group_size > 1 else ("" , "" )
516- else :
517- open , close = "[]"
518- group_parts [0 ] = open + group_parts [0 ]
519- group_parts [- 1 ] = group_parts [- 1 ] + close
520- for i , part in enumerate (group_parts [:- 1 ], start = start ):
521- # insert a separator if not already done in a nested group
522- if i not in inserted_separators_indices :
523- parts [i ] = part + ' |'
524- inserted_separators_indices .add (i )
525- parts [start + group_size - 1 ] = group_parts [- 1 ]
526- for i in range (start + group_size , end ):
527- parts [i ] = None
528-
529- # if opt_count is provided, calculate where positionals start in
530- # the final parts list (for wrapping onto separate lines).
531- # Count before filtering None entries since indices shift after.
532- if opt_count is not None :
533- pos_start = sum (1 for p in parts [:opt_count ] if p is not None )
534- else :
535- pos_start = None
536-
537- # return the usage parts and split point (gh-75949)
538- return [item for item in parts if item is not None ], pos_start
488+ # if the Optional takes a value, format is:
489+ # -s ARGS or --long ARGS
490+ else :
491+ default = self ._get_default_metavar_for_optional (action )
492+ args_string = self ._format_args (action , default )
493+ part = (
494+ f"{ option_color } { option_string } "
495+ f"{ t .summary_label } { args_string } { t .reset } "
496+ )
497+
498+ # make it look optional if it's not required or in a group
499+ if not (action .required or required or in_group ):
500+ part = '[%s]' % part
501+
502+ # add the action string to the list
503+ parts .append (part )
504+
505+ if in_group :
506+ parts [start ] = ('(' if required else '[' ) + parts [start ]
507+ for i in range (start , len (parts ) - 1 ):
508+ parts [i ] += ' |'
509+ parts [- 1 ] += ')' if required else ']'
510+
511+ if pos_start is None :
512+ pos_start = len (parts )
513+ return parts , pos_start
539514
540515 def _format_text (self , text ):
541516 if '%(prog)' in text :
0 commit comments