@@ -181,6 +181,9 @@ def __init__(
181181 self ._max_help_position = min (max_help_position ,
182182 max (width - 20 , indent_increment * 2 ))
183183 self ._width = width
184+ self ._adaptive_help_start_column = min (max_help_position ,
185+ max (self ._width - 20 , indent_increment * 2 ))
186+ self ._globally_calculated_help_start_col = self ._adaptive_help_start_column
184187
185188 self ._current_indent = 0
186189 self ._level = 0
@@ -192,6 +195,37 @@ def __init__(
192195 self ._whitespace_matcher = _re .compile (r'\s+' , _re .ASCII )
193196 self ._long_break_matcher = _re .compile (r'\n\n\n+' )
194197
198+ def _get_action_details_for_pass (self , section , current_indent_for_section_items ):
199+ """
200+ Recursively collects details for actions within a given section and its subsections.
201+ These details (action object, invocation length, indent) are used for calculating
202+ the global help text alignment column.
203+ """
204+ collected_details = []
205+
206+ for func_to_call , args_for_func in section .items :
207+ if func_to_call == self ._format_action and args_for_func :
208+ action_object = args_for_func [0 ]
209+ if action_object .help is not SUPPRESS :
210+ invocation_string = self ._format_action_invocation (action_object )
211+ # Length without color codes is needed for alignment.
212+ invocation_length = len (self ._decolor (invocation_string ))
213+
214+ collected_details .append ({
215+ 'action' : action_object ,
216+ 'inv_len' : invocation_length ,
217+ 'indent' : current_indent_for_section_items ,
218+ })
219+ elif hasattr (func_to_call , '__self__' ) and isinstance (func_to_call .__self__ , self ._Section ):
220+ sub_section_object = func_to_call .__self__
221+
222+ indent_for_subsection_items = current_indent_for_section_items + self ._indent_increment
223+
224+ collected_details .extend (
225+ self ._get_action_details_for_pass (sub_section_object , indent_for_subsection_items )
226+ )
227+ return collected_details
228+
195229 def _set_color (self , color ):
196230 from _colorize import can_colorize , decolor , get_theme
197231
@@ -224,32 +258,59 @@ def __init__(self, formatter, parent, heading=None):
224258 self .items = []
225259
226260 def format_help (self ):
227- # format the indented section
228- if self .parent is not None :
261+ """
262+ Formats the help for this section, including its heading and all items.
263+ """
264+ is_subsection = self .parent is not None
265+ if is_subsection :
229266 self .formatter ._indent ()
230- join = self .formatter ._join_parts
231- item_help = join ([func (* args ) for func , args in self .items ])
232- if self .parent is not None :
267+
268+ # Generate help strings for all items (actions, text, subsections) in this section
269+ item_help_strings = [func (* args ) for func , args in self .items ]
270+ rendered_items_help = self .formatter ._join_parts (item_help_strings )
271+
272+ if is_subsection :
273+ # Restore indent level after formatting subsection items
233274 self .formatter ._dedent ()
234275
235276 # return nothing if the section was empty
236- if not item_help :
277+ if not rendered_items_help :
237278 return ''
238279
239- # add the heading if the section was non-empty
280+ # If we're here, rendered_items_help is not empty.
281+ # Now, format the heading for this section if it exists and is not suppressed.
282+ formatted_heading_output_part = ""
240283 if self .heading is not SUPPRESS and self .heading is not None :
241- current_indent = self .formatter ._current_indent
242- heading_text = _ ('%(heading)s:' ) % dict (heading = self .heading )
243- t = self .formatter ._theme
244- heading = (
245- f'{ " " * current_indent } '
246- f'{ t .heading } { heading_text } { t .reset } \n '
284+ current_section_heading_indent = ' ' * self .formatter ._current_indent
285+
286+ try :
287+ # This line checks if global `_` is defined.
288+ # If `_` is not defined in any accessible scope, it raises NameError.
289+ _
290+ except NameError :
291+ # If global `_` was not found, this line defines `_` in the global scope
292+ # as a no-op lambda function.
293+ _ = lambda text_to_translate : text_to_translate
294+
295+ # Now, call `_` directly. It is guaranteed to be defined at this point
296+ # (either as the original gettext `_` or the no-op lambda).
297+ # `xgettext` will correctly identify `_('%(heading)s:')` as translatable.
298+ heading_title_text = _ ('%(heading)s:' ) % {'heading' : self .heading }
299+
300+ theme_colors = self .formatter ._theme
301+ formatted_heading_output_part = (
302+ f'{ current_section_heading_indent } { theme_colors .heading } '
303+ f'{ heading_title_text } { theme_colors .reset } \n '
247304 )
248- else :
249- heading = ''
250-
251- # join the section-initial newline, the heading and the help
252- return join (['\n ' , heading , item_help , '\n ' ])
305+
306+ section_output_parts = [
307+ '\n ' ,
308+ formatted_heading_output_part ,
309+ rendered_items_help ,
310+ '\n '
311+ ]
312+
313+ return self .formatter ._join_parts (section_output_parts )
253314
254315 def _add_item (self , func , args ):
255316 self ._current_section .items .append ((func , args ))
@@ -302,12 +363,105 @@ def add_arguments(self, actions):
302363 # Help-formatting methods
303364 # =======================
304365
366+ def _collect_all_action_details (self ):
367+ """
368+ Helper for format_help: Traverses all sections starting from the root
369+ and collects details about each action (like its invocation string length
370+ and current indent level). This information is used to determine the
371+ optimal global alignment for help text.
372+ """
373+ all_details = []
374+ # Actions within top-level sections (direct children of _root_section, like "options:")
375+ # will have an initial indent.
376+ indent_for_actions_in_top_level_sections = self ._indent_increment # Typically 2 spaces
377+
378+ for item_func , _item_args in self ._root_section .items :
379+ section_candidate = getattr (item_func , '__self__' , None )
380+ if isinstance (section_candidate , self ._Section ):
381+ top_level_section = section_candidate
382+ details_from_this_section = self ._get_action_details_for_pass (
383+ top_level_section ,
384+ indent_for_actions_in_top_level_sections
385+ )
386+ all_details .extend (details_from_this_section )
387+ return all_details
388+
389+ def _calculate_global_help_start_column (self , all_action_details ):
390+ """
391+ Helper for format_help: Calculates the single, globally optimal starting column
392+ for all help text associated with actions. This aims to align help texts neatly.
393+ """
394+ if not all_action_details :
395+ # No actions with help were found, so use the default adaptive start column.
396+ return self ._adaptive_help_start_column
397+
398+ min_padding_between_action_and_help = 2
399+ # Track the maximum endpoint (indent + length) of action strings that can
400+ # "reasonably" have their help text start on the same line without exceeding
401+ # the _adaptive_help_start_column for the help text itself.
402+ max_endpoint_of_reasonable_actions = 0
403+
404+ for detail in all_action_details :
405+ # The column where this action's invocation string ends.
406+ action_invocation_end_column = detail ['indent' ] + detail ['inv_len' ]
407+
408+ # An action is "reasonable" if its help text can start on the same line,
409+ # aligned at or before _adaptive_help_start_column, while maintaining minimum padding.
410+ is_reasonable_to_align_with_others = \
411+ (action_invocation_end_column + min_padding_between_action_and_help <=
412+ self ._adaptive_help_start_column )
413+
414+ if is_reasonable_to_align_with_others :
415+ max_endpoint_of_reasonable_actions = max (
416+ max_endpoint_of_reasonable_actions ,
417+ action_invocation_end_column
418+ )
419+
420+ if max_endpoint_of_reasonable_actions > 0 :
421+ # At least one action fits the "reasonable" criteria.
422+ # The desired alignment column is after the longest of these "reasonable" actions, plus padding.
423+ desired_global_alignment_column = \
424+ max_endpoint_of_reasonable_actions + min_padding_between_action_and_help
425+
426+ # However, this alignment should not exceed the user's preferred _adaptive_help_start_column.
427+ return min (desired_global_alignment_column , self ._adaptive_help_start_column )
428+ else :
429+ # No action was "reasonable" (e.g., all actions are very long, or _adaptive_help_start_column is too small).
430+ # In this scenario, fall back to using _adaptive_help_start_column.
431+ # Help text for most actions will likely start on a new line, indented to this column.
432+ return self ._adaptive_help_start_column
433+
434+
305435 def format_help (self ):
306- help = self ._root_section .format_help ()
307- if help :
308- help = self ._long_break_matcher .sub ('\n \n ' , help )
309- help = help .strip ('\n ' ) + '\n '
310- return help
436+ """
437+ Formats the full help message.
438+ This orchestrates the collection of action details for alignment,
439+ calculates the global help start column, and then formats all sections.
440+ """
441+ # First Pass: Collect details of all actions to determine optimal help text alignment.
442+ # This populates a list of dictionaries, each with action details.
443+ all_action_details = self ._collect_all_action_details ()
444+
445+ # Calculate and set the global starting column for help text based on these details.
446+ # This value (self._globally_calculated_help_start_col) will be used by _format_action.
447+ self ._globally_calculated_help_start_col = \
448+ self ._calculate_global_help_start_column (all_action_details )
449+
450+ # Second Pass: Actually format the help.
451+ # This will recursively call _Section.format_help for all sections,
452+ # which in turn call item formatters like _format_action, _format_text.
453+ # These formatting methods will use the self._globally_calculated_help_start_col set above.
454+ raw_help_output = self ._root_section .format_help ()
455+
456+ # Post-process the generated help string for final presentation.
457+ if raw_help_output :
458+ # Consolidate multiple consecutive blank lines into a single blank line.
459+ processed_help_output = self ._long_break_matcher .sub ('\n \n ' , raw_help_output )
460+ # Ensure the help message ends with a single newline and strip any other leading/trailing newlines.
461+ processed_help_output = processed_help_output .strip ('\n ' ) + '\n '
462+ return processed_help_output
463+
464+ return "" # Return an empty string if no help content was generated.
311465
312466 def _join_parts (self , part_strings ):
313467 return '' .join ([part
@@ -527,59 +681,97 @@ def _format_text(self, text):
527681 return self ._fill_text (text , text_width , indent ) + '\n \n '
528682
529683 def _format_action (self , action ):
530- # determine the required width and the entry label
531- help_position = min (self ._action_max_length + 2 ,
532- self ._max_help_position )
533- help_width = max (self ._width - help_position , 11 )
534- action_width = help_position - self ._current_indent - 2
535- action_header = self ._format_action_invocation (action )
536- action_header_no_color = self ._decolor (action_header )
537-
538- # no help; start on same line and add a final newline
539- if not action .help :
540- tup = self ._current_indent , '' , action_header
541- action_header = '%*s%s\n ' % tup
542-
543- # short action name; start on the same line and pad two spaces
544- elif len (action_header_no_color ) <= action_width :
545- # calculate widths without color codes
546- action_header_color = action_header
547- tup = self ._current_indent , '' , action_width , action_header_no_color
548- action_header = '%*s%-*s ' % tup
549- # swap in the colored header
550- action_header = action_header .replace (
551- action_header_no_color , action_header_color
552- )
553- indent_first = 0
554-
555- # long action name; start on the next line
684+ """
685+ Formats the help for a single action (argument).
686+ This includes the action's invocation string and its help text,
687+ aligning the help text based on _globally_calculated_help_start_col.
688+ """
689+ action_invocation_string = self ._format_action_invocation (action )
690+ action_invocation_len_no_color = len (self ._decolor (action_invocation_string ))
691+ current_action_item_indent_str = ' ' * self ._current_indent
692+ globally_determined_help_start_col = self ._globally_calculated_help_start_col
693+ min_padding_after_action_invocation = 2
694+ output_parts = []
695+
696+ # Determine the maximum length the action_invocation_string (decolored) can be
697+ # for its help text to start on the same line, aligned at globally_determined_help_start_col,
698+ # while respecting self._current_indent and min_padding_after_action_invocation.
699+ max_action_invocation_len_for_same_line_help = \
700+ globally_determined_help_start_col - self ._current_indent - min_padding_after_action_invocation
701+
702+ action_invocation_line_part = ""
703+ help_should_start_on_new_line = True
704+
705+ # The actual column where the first line of help text (and subsequent wrapped lines) will start.
706+ actual_help_text_alignment_column = globally_determined_help_start_col
707+
708+ has_help_text = action .help and action .help .strip ()
709+
710+ if has_help_text :
711+ if action_invocation_len_no_color <= max_action_invocation_len_for_same_line_help :
712+ # Action invocation is short enough: help text can start on the same line.
713+ # Calculate the number of padding spaces needed to align the help text correctly.
714+ num_padding_spaces = globally_determined_help_start_col - \
715+ (self ._current_indent + action_invocation_len_no_color )
716+
717+ action_invocation_line_part = (
718+ f"{ current_action_item_indent_str } { action_invocation_string } "
719+ f"{ ' ' * num_padding_spaces } "
720+ )
721+ help_should_start_on_new_line = False
722+ else :
723+ action_invocation_line_part = f"{ current_action_item_indent_str } { action_invocation_string } \n "
556724 else :
557- tup = self ._current_indent , '' , action_header
558- action_header = '%*s%s\n ' % tup
559- indent_first = help_position
560-
561- # collect the pieces of the action help
562- parts = [action_header ]
563-
564- # if there was help for the action, add lines of help text
565- if action .help and action .help .strip ():
566- help_text = self ._expand_help (action )
567- if help_text :
568- help_lines = self ._split_lines (help_text , help_width )
569- parts .append ('%*s%s\n ' % (indent_first , '' , help_lines [0 ]))
570- for line in help_lines [1 :]:
571- parts .append ('%*s%s\n ' % (help_position , '' , line ))
572-
573- # or add a newline if the description doesn't end with one
574- elif not action_header .endswith ('\n ' ):
575- parts .append ('\n ' )
576-
577- # if there are any sub-actions, add their help as well
725+ action_invocation_line_part = f"{ current_action_item_indent_str } { action_invocation_string } \n "
726+
727+ output_parts .append (action_invocation_line_part )
728+
729+ if has_help_text :
730+ expanded_help_text = self ._expand_help (action )
731+
732+ # Calculate the available width for wrapping the help text.
733+ # The help text block starts at actual_help_text_alignment_column and extends to self._width.
734+ help_text_wrapping_width = max (self ._width - actual_help_text_alignment_column , 11 )
735+
736+ split_help_lines = self ._split_lines (expanded_help_text , help_text_wrapping_width )
737+
738+ if split_help_lines : # Proceed only if splitting the help text yields any lines.
739+ first_help_line_content = split_help_lines [0 ]
740+ remaining_help_lines_content = split_help_lines [1 :]
741+
742+ if help_should_start_on_new_line :
743+ # Help starts on a new line, indented to actual_help_text_alignment_column.
744+ output_parts .append (f"{ ' ' * actual_help_text_alignment_column } { first_help_line_content } \n " )
745+ else :
746+ # Help starts on the same line as action_invocation_line_part.
747+ # Append the first_help_line_content to the last part in output_parts.
748+ # (action_invocation_line_part does not end with \n in this case).
749+ output_parts [- 1 ] += f"{ first_help_line_content } \n "
750+
751+ # Add any subsequent wrapped help lines, each indented to actual_help_text_alignment_column.
752+ for line_content in remaining_help_lines_content :
753+ output_parts .append (f"{ ' ' * actual_help_text_alignment_column } { line_content } \n " )
754+
755+ elif not output_parts [- 1 ].endswith ('\n ' ):
756+ # Case: has_help_text was true (action.help existed), but it was empty after strip()
757+ # or _split_lines returned empty. If action_invocation_line_part didn't end with \n
758+ # (because help_should_start_on_new_line was false), add a newline.
759+ output_parts [- 1 ] += '\n '
760+
761+ elif output_parts and not output_parts [- 1 ].endswith ('\n ' ):
762+ # This handles the unlikely case where there's no help text, but action_invocation_line_part
763+ # (which is output_parts[-1]) somehow didn't end with a newline.
764+ # Based on the logic above, action_invocation_line_part should always end with \n if no help text.
765+ # This is mostly defensive.
766+ output_parts [- 1 ] += '\n '
767+
768+ # Recursively format any subactions associated with this action.
769+ # The _iter_indented_subactions method manages _indent() and _dedent() calls internally,
770+ # ensuring self._current_indent is correctly set for these recursive _format_action calls.
578771 for subaction in self ._iter_indented_subactions (action ):
579- parts .append (self ._format_action (subaction ))
772+ output_parts .append (self ._format_action (subaction ))
580773
581- # return a single string
582- return self ._join_parts (parts )
774+ return self ._join_parts (output_parts )
583775
584776 def _format_action_invocation (self , action ):
585777 t = self ._theme
0 commit comments