cmd2 2.7.0__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cmd2/argparse_custom.py CHANGED
@@ -6,7 +6,7 @@ recommended that developers of cmd2-based apps either use it or write their own
6
6
  parser that inherits from it. This will give a consistent look-and-feel between
7
7
  the help/error output of built-in cmd2 commands and the app-specific commands.
8
8
  If you wish to override the parser used by cmd2's built-in commands, see
9
- override_parser.py example.
9
+ custom_parser.py example.
10
10
 
11
11
  Since the new capabilities are added by patching at the argparse API level,
12
12
  they are available whether or not Cmd2ArgumentParser is used. However, the help
@@ -122,38 +122,25 @@ uninformative data is being tab completed. For instance, tab completing ID
122
122
  numbers isn't very helpful to a user without context. Returning a list of
123
123
  CompletionItems instead of a regular string for completion results will signal
124
124
  the ArgparseCompleter to output the completion results in a table of completion
125
- tokens with descriptions instead of just a table of tokens::
125
+ tokens with descriptive data instead of just a table of tokens::
126
126
 
127
127
  Instead of this:
128
128
  1 2 3
129
129
 
130
130
  The user sees this:
131
- ITEM_ID Item Name
132
- ============================
133
- 1 My item
134
- 2 Another item
135
- 3 Yet another item
131
+ ITEM_ID Description
132
+ ────────────────────────────
133
+ 1 My item
134
+ 2 Another item
135
+ 3 Yet another item
136
136
 
137
137
 
138
138
  The left-most column is the actual value being tab completed and its header is
139
139
  that value's name. The right column header is defined using the
140
- descriptive_header parameter of add_argument(). The right column values come
141
- from the CompletionItem.description value.
142
-
143
- Example::
144
-
145
- token = 1
146
- token_description = "My Item"
147
- completion_item = CompletionItem(token, token_description)
148
-
149
- Since descriptive_header and CompletionItem.description are just strings, you
150
- can format them in such a way to have multiple columns::
151
-
152
- ITEM_ID Item Name Checked Out Due Date
153
- ==========================================================
154
- 1 My item True 02/02/2022
155
- 2 Another item False
156
- 3 Yet another item False
140
+ ``descriptive_headers`` parameter of add_argument(), which is a list of header
141
+ names that defaults to ["Description"]. The right column values come from the
142
+ ``CompletionItem.descriptive_data`` member, which is a list with the same number
143
+ of items as columns defined in descriptive_headers.
157
144
 
158
145
  To use CompletionItems, just return them from your choices_provider or
159
146
  completer functions. They can also be used as argparse choices. When a
@@ -162,12 +149,59 @@ makes it accessible through a property called orig_value. cmd2 has patched
162
149
  argparse so that when evaluating choices, input is compared to
163
150
  CompletionItem.orig_value instead of the CompletionItem instance.
164
151
 
165
- To avoid printing a ton of information to the screen at once when a user
152
+ Example::
153
+
154
+ Add an argument and define its descriptive_headers.
155
+
156
+ parser.add_argument(
157
+ add_argument(
158
+ "item_id",
159
+ type=int,
160
+ choices_provider=get_items,
161
+ descriptive_headers=["Item Name", "Checked Out", "Due Date"],
162
+ )
163
+
164
+ Implement the choices_provider to return CompletionItems.
165
+
166
+ def get_items(self) -> list[CompletionItems]:
167
+ \"\"\"choices_provider which returns CompletionItems\"\"\"
168
+
169
+ # CompletionItem's second argument is descriptive_data.
170
+ # Its item count should match that of descriptive_headers.
171
+ return [
172
+ CompletionItem(1, ["My item", True, "02/02/2022"]),
173
+ CompletionItem(2, ["Another item", False, ""]),
174
+ CompletionItem(3, ["Yet another item", False, ""]),
175
+ ]
176
+
177
+ This is what the user will see during tab completion.
178
+
179
+ ITEM_ID Item Name Checked Out Due Date
180
+ ───────────────────────────────────────────────────────
181
+ 1 My item True 02/02/2022
182
+ 2 Another item False
183
+ 3 Yet another item False
184
+
185
+ ``descriptive_headers`` can be strings or ``Rich.table.Columns`` for more
186
+ control over things like alignment.
187
+
188
+ - If a header is a string, it will render as a left-aligned column with its
189
+ overflow behavior set to "fold". This means a long string will wrap within its
190
+ cell, creating as many new lines as required to fit.
191
+
192
+ - If a header is a ``Column``, it defaults to "ellipsis" overflow behavior.
193
+ This means a long string which exceeds the width of its column will be
194
+ truncated with an ellipsis at the end. You can override this and other settings
195
+ when you create the ``Column``.
196
+
197
+ ``descriptive_data`` items can include Rich objects, including styled Text and Tables.
198
+
199
+ To avoid printing a excessive information to the screen at once when a user
166
200
  presses tab, there is a maximum threshold for the number of CompletionItems
167
- that will be shown. Its value is defined in cmd2.Cmd.max_completion_items. It
168
- defaults to 50, but can be changed. If the number of completion suggestions
201
+ that will be shown. Its value is defined in ``cmd2.Cmd.max_completion_items``.
202
+ It defaults to 50, but can be changed. If the number of completion suggestions
169
203
  exceeds this number, they will be displayed in the typical columnized format
170
- and will not include the description value of the CompletionItems.
204
+ and will not include the descriptive_data of the CompletionItems.
171
205
 
172
206
 
173
207
  **Patched argparse functions**
@@ -200,8 +234,8 @@ for cases in which you need to manually access the cmd2-specific attributes.
200
234
  - ``argparse.Action.get_choices_callable()`` - See `action_get_choices_callable` for more details.
201
235
  - ``argparse.Action.set_choices_provider()`` - See `_action_set_choices_provider` for more details.
202
236
  - ``argparse.Action.set_completer()`` - See `_action_set_completer` for more details.
203
- - ``argparse.Action.get_descriptive_header()`` - See `_action_get_descriptive_header` for more details.
204
- - ``argparse.Action.set_descriptive_header()`` - See `_action_set_descriptive_header` for more details.
237
+ - ``argparse.Action.get_descriptive_headers()`` - See `_action_get_descriptive_headers` for more details.
238
+ - ``argparse.Action.set_descriptive_headers()`` - See `_action_set_descriptive_headers` for more details.
205
239
  - ``argparse.Action.get_nargs_range()`` - See `_action_get_nargs_range` for more details.
206
240
  - ``argparse.Action.set_nargs_range()`` - See `_action_set_nargs_range` for more details.
207
241
  - ``argparse.Action.get_suppress_tab_hint()`` - See `_action_get_suppress_tab_hint` for more details.
@@ -236,25 +270,35 @@ from collections.abc import (
236
270
  )
237
271
  from gettext import gettext
238
272
  from typing import (
239
- IO,
240
273
  TYPE_CHECKING,
241
274
  Any,
242
275
  ClassVar,
243
276
  NoReturn,
244
- Optional,
245
277
  Protocol,
246
- Union,
247
278
  cast,
248
279
  runtime_checkable,
249
280
  )
250
281
 
251
- from rich_argparse import RawTextRichHelpFormatter
252
-
253
- from . import (
254
- ansi,
255
- constants,
282
+ from rich.console import (
283
+ Group,
284
+ RenderableType,
285
+ )
286
+ from rich.protocol import is_renderable
287
+ from rich.table import Column
288
+ from rich.text import Text
289
+ from rich_argparse import (
290
+ ArgumentDefaultsRichHelpFormatter,
291
+ MetavarTypeRichHelpFormatter,
292
+ RawDescriptionRichHelpFormatter,
293
+ RawTextRichHelpFormatter,
294
+ RichHelpFormatter,
256
295
  )
257
296
 
297
+ from . import constants
298
+ from . import rich_utils as ru
299
+ from .rich_utils import Cmd2RichArgparseConsole
300
+ from .styles import Cmd2Style
301
+
258
302
  if TYPE_CHECKING: # pragma: no cover
259
303
  from .argparse_completer import (
260
304
  ArgparseCompleter,
@@ -280,6 +324,56 @@ def generate_range_error(range_min: int, range_max: float) -> str:
280
324
  return err_str
281
325
 
282
326
 
327
+ def set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
328
+ """Recursively set prog attribute of a parser and all of its subparsers.
329
+
330
+ Does so that the root command is a command name and not sys.argv[0].
331
+
332
+ :param parser: the parser being edited
333
+ :param prog: new value for the parser's prog attribute
334
+ """
335
+ # Set the prog value for this parser
336
+ parser.prog = prog
337
+ req_args: list[str] = []
338
+
339
+ # Set the prog value for the parser's subcommands
340
+ for action in parser._actions:
341
+ if isinstance(action, argparse._SubParsersAction):
342
+ # Set the _SubParsersAction's _prog_prefix value. That way if its add_parser() method is called later,
343
+ # the correct prog value will be set on the parser being added.
344
+ action._prog_prefix = parser.prog
345
+
346
+ # The keys of action.choices are subcommand names as well as subcommand aliases. The aliases point to the
347
+ # same parser as the actual subcommand. We want to avoid placing an alias into a parser's prog value.
348
+ # Unfortunately there is nothing about an action.choices entry which tells us it's an alias. In most cases
349
+ # we can filter out the aliases by checking the contents of action._choices_actions. This list only contains
350
+ # help information and names for the subcommands and not aliases. However, subcommands without help text
351
+ # won't show up in that list. Since dictionaries are ordered in Python 3.6 and above and argparse inserts the
352
+ # subcommand name into choices dictionary before aliases, we should be OK assuming the first time we see a
353
+ # parser, the dictionary key is a subcommand and not alias.
354
+ processed_parsers = []
355
+
356
+ # Set the prog value for each subcommand's parser
357
+ for subcmd_name, subcmd_parser in action.choices.items():
358
+ # Check if we've already edited this parser
359
+ if subcmd_parser in processed_parsers:
360
+ continue
361
+
362
+ subcmd_prog = parser.prog
363
+ if req_args:
364
+ subcmd_prog += " " + " ".join(req_args)
365
+ subcmd_prog += " " + subcmd_name
366
+ set_parser_prog(subcmd_parser, subcmd_prog)
367
+ processed_parsers.append(subcmd_parser)
368
+
369
+ # We can break since argparse only allows 1 group of subcommands per level
370
+ break
371
+
372
+ # Need to save required args so they can be prepended to the subcommand usage
373
+ if action.required:
374
+ req_args.append(action.dest)
375
+
376
+
283
377
  class CompletionItem(str): # noqa: SLOT000
284
378
  """Completion item with descriptive text attached.
285
379
 
@@ -290,15 +384,22 @@ class CompletionItem(str): # noqa: SLOT000
290
384
  """Responsible for creating and returning a new instance, called before __init__ when an object is instantiated."""
291
385
  return super().__new__(cls, value)
292
386
 
293
- def __init__(self, value: object, description: str = '', *args: Any) -> None:
387
+ def __init__(self, value: object, descriptive_data: Sequence[Any], *args: Any) -> None:
294
388
  """CompletionItem Initializer.
295
389
 
296
390
  :param value: the value being tab completed
297
- :param description: description text to display
391
+ :param descriptive_data: a list of descriptive data to display in the columns that follow
392
+ the completion value. The number of items in this list must equal
393
+ the number of descriptive headers defined for the argument.
298
394
  :param args: args for str __init__
299
395
  """
300
396
  super().__init__(*args)
301
- self.description = description
397
+
398
+ # Make sure all objects are renderable by a Rich table.
399
+ renderable_data = [obj if is_renderable(obj) else str(obj) for obj in descriptive_data]
400
+
401
+ # Convert strings containing ANSI style sequences to Rich Text objects for correct display width.
402
+ self.descriptive_data = ru.prepare_objects_for_rendering(*renderable_data)
302
403
 
303
404
  # Save the original value to support CompletionItems as argparse choices.
304
405
  # cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance.
@@ -331,7 +432,7 @@ class ChoicesProviderFuncWithTokens(Protocol):
331
432
  """Enable instances to be called like functions."""
332
433
 
333
434
 
334
- ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens]
435
+ ChoicesProviderFunc = ChoicesProviderFuncBase | ChoicesProviderFuncWithTokens
335
436
 
336
437
 
337
438
  @runtime_checkable
@@ -364,7 +465,7 @@ class CompleterFuncWithTokens(Protocol):
364
465
  """Enable instances to be called like functions."""
365
466
 
366
467
 
367
- CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens]
468
+ CompleterFunc = CompleterFuncBase | CompleterFuncWithTokens
368
469
 
369
470
 
370
471
  class ChoicesCallable:
@@ -376,7 +477,7 @@ class ChoicesCallable:
376
477
  def __init__(
377
478
  self,
378
479
  is_completer: bool,
379
- to_call: Union[CompleterFunc, ChoicesProviderFunc],
480
+ to_call: CompleterFunc | ChoicesProviderFunc,
380
481
  ) -> None:
381
482
  """Initialize the ChoiceCallable instance.
382
483
 
@@ -424,7 +525,7 @@ class ChoicesCallable:
424
525
  ATTR_CHOICES_CALLABLE = 'choices_callable'
425
526
 
426
527
  # Descriptive header that prints when using CompletionItems
427
- ATTR_DESCRIPTIVE_HEADER = 'descriptive_header'
528
+ ATTR_DESCRIPTIVE_HEADERS = 'descriptive_headers'
428
529
 
429
530
  # A tuple specifying nargs as a range (min, max)
430
531
  ATTR_NARGS_RANGE = 'nargs_range'
@@ -437,7 +538,7 @@ ATTR_SUPPRESS_TAB_HINT = 'suppress_tab_hint'
437
538
  ############################################################################################################
438
539
  # Patch argparse.Action with accessors for choice_callable attribute
439
540
  ############################################################################################################
440
- def _action_get_choices_callable(self: argparse.Action) -> Optional[ChoicesCallable]:
541
+ def _action_get_choices_callable(self: argparse.Action) -> ChoicesCallable | None:
441
542
  """Get the choices_callable attribute of an argparse Action.
442
543
 
443
544
  This function is added by cmd2 as a method called ``get_choices_callable()`` to ``argparse.Action`` class.
@@ -447,7 +548,7 @@ def _action_get_choices_callable(self: argparse.Action) -> Optional[ChoicesCalla
447
548
  :param self: argparse Action being queried
448
549
  :return: A ChoicesCallable instance or None if attribute does not exist
449
550
  """
450
- return cast(Optional[ChoicesCallable], getattr(self, ATTR_CHOICES_CALLABLE, None))
551
+ return cast(ChoicesCallable | None, getattr(self, ATTR_CHOICES_CALLABLE, None))
451
552
 
452
553
 
453
554
  setattr(argparse.Action, 'get_choices_callable', _action_get_choices_callable)
@@ -521,44 +622,44 @@ setattr(argparse.Action, 'set_completer', _action_set_completer)
521
622
 
522
623
 
523
624
  ############################################################################################################
524
- # Patch argparse.Action with accessors for descriptive_header attribute
625
+ # Patch argparse.Action with accessors for descriptive_headers attribute
525
626
  ############################################################################################################
526
- def _action_get_descriptive_header(self: argparse.Action) -> Optional[str]:
527
- """Get the descriptive_header attribute of an argparse Action.
627
+ def _action_get_descriptive_headers(self: argparse.Action) -> Sequence[str | Column] | None:
628
+ """Get the descriptive_headers attribute of an argparse Action.
528
629
 
529
- This function is added by cmd2 as a method called ``get_descriptive_header()`` to ``argparse.Action`` class.
630
+ This function is added by cmd2 as a method called ``get_descriptive_headers()`` to ``argparse.Action`` class.
530
631
 
531
- To call: ``action.get_descriptive_header()``
632
+ To call: ``action.get_descriptive_headers()``
532
633
 
533
634
  :param self: argparse Action being queried
534
- :return: The value of descriptive_header or None if attribute does not exist
635
+ :return: The value of descriptive_headers or None if attribute does not exist
535
636
  """
536
- return cast(Optional[str], getattr(self, ATTR_DESCRIPTIVE_HEADER, None))
637
+ return cast(Sequence[str | Column] | None, getattr(self, ATTR_DESCRIPTIVE_HEADERS, None))
537
638
 
538
639
 
539
- setattr(argparse.Action, 'get_descriptive_header', _action_get_descriptive_header)
640
+ setattr(argparse.Action, 'get_descriptive_headers', _action_get_descriptive_headers)
540
641
 
541
642
 
542
- def _action_set_descriptive_header(self: argparse.Action, descriptive_header: Optional[str]) -> None:
543
- """Set the descriptive_header attribute of an argparse Action.
643
+ def _action_set_descriptive_headers(self: argparse.Action, descriptive_headers: Sequence[str | Column] | None) -> None:
644
+ """Set the descriptive_headers attribute of an argparse Action.
544
645
 
545
- This function is added by cmd2 as a method called ``set_descriptive_header()`` to ``argparse.Action`` class.
646
+ This function is added by cmd2 as a method called ``set_descriptive_headers()`` to ``argparse.Action`` class.
546
647
 
547
- To call: ``action.set_descriptive_header(descriptive_header)``
648
+ To call: ``action.set_descriptive_headers(descriptive_headers)``
548
649
 
549
650
  :param self: argparse Action being updated
550
- :param descriptive_header: value being assigned
651
+ :param descriptive_headers: value being assigned
551
652
  """
552
- setattr(self, ATTR_DESCRIPTIVE_HEADER, descriptive_header)
653
+ setattr(self, ATTR_DESCRIPTIVE_HEADERS, descriptive_headers)
553
654
 
554
655
 
555
- setattr(argparse.Action, 'set_descriptive_header', _action_set_descriptive_header)
656
+ setattr(argparse.Action, 'set_descriptive_headers', _action_set_descriptive_headers)
556
657
 
557
658
 
558
659
  ############################################################################################################
559
660
  # Patch argparse.Action with accessors for nargs_range attribute
560
661
  ############################################################################################################
561
- def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[int, float]]]:
662
+ def _action_get_nargs_range(self: argparse.Action) -> tuple[int, int | float] | None:
562
663
  """Get the nargs_range attribute of an argparse Action.
563
664
 
564
665
  This function is added by cmd2 as a method called ``get_nargs_range()`` to ``argparse.Action`` class.
@@ -568,13 +669,13 @@ def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[
568
669
  :param self: argparse Action being queried
569
670
  :return: The value of nargs_range or None if attribute does not exist
570
671
  """
571
- return cast(Optional[tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None))
672
+ return cast(tuple[int, int | float] | None, getattr(self, ATTR_NARGS_RANGE, None))
572
673
 
573
674
 
574
675
  setattr(argparse.Action, 'get_nargs_range', _action_get_nargs_range)
575
676
 
576
677
 
577
- def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[tuple[int, Union[int, float]]]) -> None:
678
+ def _action_set_nargs_range(self: argparse.Action, nargs_range: tuple[int, int | float] | None) -> None:
578
679
  """Set the nargs_range attribute of an argparse Action.
579
680
 
580
681
  This function is added by cmd2 as a method called ``set_nargs_range()`` to ``argparse.Action`` class.
@@ -633,7 +734,7 @@ CUSTOM_ACTION_ATTRIBS: set[str] = set()
633
734
  _CUSTOM_ATTRIB_PFX = '_attr_'
634
735
 
635
736
 
636
- def register_argparse_argument_parameter(param_name: str, param_type: Optional[type[Any]]) -> None:
737
+ def register_argparse_argument_parameter(param_name: str, param_type: type[Any] | None) -> None:
637
738
  """Register a custom argparse argument parameter.
638
739
 
639
740
  The registered name will then be a recognized keyword parameter to the parser's `add_argument()` function.
@@ -699,11 +800,11 @@ orig_actions_container_add_argument = argparse._ActionsContainer.add_argument
699
800
  def _add_argument_wrapper(
700
801
  self: argparse._ActionsContainer,
701
802
  *args: Any,
702
- nargs: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None] = None,
703
- choices_provider: Optional[ChoicesProviderFunc] = None,
704
- completer: Optional[CompleterFunc] = None,
803
+ nargs: int | str | tuple[int] | tuple[int, int] | tuple[int, float] | None = None,
804
+ choices_provider: ChoicesProviderFunc | None = None,
805
+ completer: CompleterFunc | None = None,
705
806
  suppress_tab_hint: bool = False,
706
- descriptive_header: Optional[str] = None,
807
+ descriptive_headers: list[Column | str] | None = None,
707
808
  **kwargs: Any,
708
809
  ) -> argparse.Action:
709
810
  """Wrap ActionsContainer.add_argument() which supports more settings used by cmd2.
@@ -723,8 +824,8 @@ def _add_argument_wrapper(
723
824
  current argument's help text as a hint. Set this to True to suppress the hint. If this
724
825
  argument's help text is set to argparse.SUPPRESS, then tab hints will not display
725
826
  regardless of the value passed for suppress_tab_hint. Defaults to False.
726
- :param descriptive_header: if the provided choices are CompletionItems, then this header will display
727
- during tab completion. Defaults to None.
827
+ :param descriptive_headers: if the provided choices are CompletionItems, then these are the headers
828
+ of the descriptive data. Defaults to None.
728
829
 
729
830
  # Args from original function
730
831
  :param kwargs: keyword-arguments recognized by argparse._ActionsContainer.add_argument
@@ -749,7 +850,7 @@ def _add_argument_wrapper(
749
850
  nargs_range = None
750
851
 
751
852
  if nargs is not None:
752
- nargs_adjusted: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None]
853
+ nargs_adjusted: int | str | tuple[int] | tuple[int, int] | tuple[int, float] | None
753
854
  # Check if nargs was given as a range
754
855
  if isinstance(nargs, tuple):
755
856
  # Handle 1-item tuple by setting max to INFINITY
@@ -759,11 +860,11 @@ def _add_argument_wrapper(
759
860
  # Validate nargs tuple
760
861
  if (
761
862
  len(nargs) != 2
762
- or not isinstance(nargs[0], int) # type: ignore[unreachable]
763
- or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) # type: ignore[misc]
863
+ or not isinstance(nargs[0], int)
864
+ or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY)
764
865
  ):
765
866
  raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers')
766
- if nargs[0] >= nargs[1]: # type: ignore[misc]
867
+ if nargs[0] >= nargs[1]:
767
868
  raise ValueError('Invalid nargs range. The first value must be less than the second')
768
869
  if nargs[0] < 0:
769
870
  raise ValueError('Negative numbers are invalid for nargs range')
@@ -771,7 +872,7 @@ def _add_argument_wrapper(
771
872
  # Save the nargs tuple as our range setting
772
873
  nargs_range = nargs
773
874
  range_min = nargs_range[0]
774
- range_max = nargs_range[1] # type: ignore[misc]
875
+ range_max = nargs_range[1]
775
876
 
776
877
  # Convert nargs into a format argparse recognizes
777
878
  if range_min == 0:
@@ -807,7 +908,7 @@ def _add_argument_wrapper(
807
908
  new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
808
909
 
809
910
  # Set the custom attributes
810
- new_arg.set_nargs_range(nargs_range) # type: ignore[arg-type, attr-defined]
911
+ new_arg.set_nargs_range(nargs_range) # type: ignore[attr-defined]
811
912
 
812
913
  if choices_provider:
813
914
  new_arg.set_choices_provider(choices_provider) # type: ignore[attr-defined]
@@ -815,7 +916,7 @@ def _add_argument_wrapper(
815
916
  new_arg.set_completer(completer) # type: ignore[attr-defined]
816
917
 
817
918
  new_arg.set_suppress_tab_hint(suppress_tab_hint) # type: ignore[attr-defined]
818
- new_arg.set_descriptive_header(descriptive_header) # type: ignore[attr-defined]
919
+ new_arg.set_descriptive_headers(descriptive_headers) # type: ignore[attr-defined]
819
920
 
820
921
  for keyword, value in custom_attribs.items():
821
922
  attr_setter = getattr(new_arg, f'set_{keyword}', None)
@@ -890,7 +991,7 @@ setattr(argparse.ArgumentParser, '_match_argument', _match_argument_wrapper)
890
991
  ATTR_AP_COMPLETER_TYPE = 'ap_completer_type'
891
992
 
892
993
 
893
- def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Optional[type['ArgparseCompleter']]: # noqa: N802
994
+ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> type['ArgparseCompleter'] | None: # noqa: N802
894
995
  """Get the ap_completer_type attribute of an argparse ArgumentParser.
895
996
 
896
997
  This function is added by cmd2 as a method called ``get_ap_completer_type()`` to ``argparse.ArgumentParser`` class.
@@ -900,7 +1001,7 @@ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Opti
900
1001
  :param self: ArgumentParser being queried
901
1002
  :return: An ArgparseCompleter-based class or None if attribute does not exist
902
1003
  """
903
- return cast(Optional[type['ArgparseCompleter']], getattr(self, ATTR_AP_COMPLETER_TYPE, None))
1004
+ return cast(type['ArgparseCompleter'] | None, getattr(self, ATTR_AP_COMPLETER_TYPE, None))
904
1005
 
905
1006
 
906
1007
  setattr(argparse.ArgumentParser, 'get_ap_completer_type', _ArgumentParser_get_ap_completer_type)
@@ -996,13 +1097,9 @@ setattr(argparse._SubParsersAction, 'remove_parser', _SubParsersAction_remove_pa
996
1097
  ############################################################################################################
997
1098
 
998
1099
 
999
- class Cmd2HelpFormatter(RawTextRichHelpFormatter):
1100
+ class Cmd2HelpFormatter(RichHelpFormatter):
1000
1101
  """Custom help formatter to configure ordering of help text."""
1001
1102
 
1002
- # rich-argparse formats all group names with str.title().
1003
- # Override their formatter to do nothing.
1004
- group_name_formatter: ClassVar[Callable[[str], str]] = str
1005
-
1006
1103
  # Disable automatic highlighting in the help text.
1007
1104
  highlights: ClassVar[list[str]] = []
1008
1105
 
@@ -1015,12 +1112,28 @@ class Cmd2HelpFormatter(RawTextRichHelpFormatter):
1015
1112
  help_markup: ClassVar[bool] = False
1016
1113
  text_markup: ClassVar[bool] = False
1017
1114
 
1115
+ def __init__(
1116
+ self,
1117
+ prog: str,
1118
+ indent_increment: int = 2,
1119
+ max_help_position: int = 24,
1120
+ width: int | None = None,
1121
+ *,
1122
+ console: Cmd2RichArgparseConsole | None = None,
1123
+ **kwargs: Any,
1124
+ ) -> None:
1125
+ """Initialize Cmd2HelpFormatter."""
1126
+ if console is None:
1127
+ console = Cmd2RichArgparseConsole()
1128
+
1129
+ super().__init__(prog, indent_increment, max_help_position, width, console=console, **kwargs)
1130
+
1018
1131
  def _format_usage(
1019
1132
  self,
1020
- usage: Optional[str],
1133
+ usage: str | None,
1021
1134
  actions: Iterable[argparse.Action],
1022
1135
  groups: Iterable[argparse._ArgumentGroup],
1023
- prefix: Optional[str] = None,
1136
+ prefix: str | None = None,
1024
1137
  ) -> str:
1025
1138
  if prefix is None:
1026
1139
  prefix = gettext('Usage: ')
@@ -1074,7 +1187,7 @@ class Cmd2HelpFormatter(RawTextRichHelpFormatter):
1074
1187
  # End cmd2 customization
1075
1188
 
1076
1189
  # helper for wrapping lines
1077
- def get_lines(parts: list[str], indent: str, prefix: Optional[str] = None) -> list[str]:
1190
+ def get_lines(parts: list[str], indent: str, prefix: str | None = None) -> list[str]:
1078
1191
  lines: list[str] = []
1079
1192
  line: list[str] = []
1080
1193
  line_len = len(prefix) - 1 if prefix is not None else len(indent) - 1
@@ -1154,8 +1267,8 @@ class Cmd2HelpFormatter(RawTextRichHelpFormatter):
1154
1267
  def _determine_metavar(
1155
1268
  self,
1156
1269
  action: argparse.Action,
1157
- default_metavar: Union[str, tuple[str, ...]],
1158
- ) -> Union[str, tuple[str, ...]]:
1270
+ default_metavar: str,
1271
+ ) -> str | tuple[str, ...]:
1159
1272
  """Determine what to use as the metavar value of an action."""
1160
1273
  if action.metavar is not None:
1161
1274
  result = action.metavar
@@ -1171,7 +1284,7 @@ class Cmd2HelpFormatter(RawTextRichHelpFormatter):
1171
1284
  def _metavar_formatter(
1172
1285
  self,
1173
1286
  action: argparse.Action,
1174
- default_metavar: Union[str, tuple[str, ...]],
1287
+ default_metavar: str,
1175
1288
  ) -> Callable[[int], tuple[str, ...]]:
1176
1289
  metavar = self._determine_metavar(action, default_metavar)
1177
1290
 
@@ -1182,7 +1295,7 @@ class Cmd2HelpFormatter(RawTextRichHelpFormatter):
1182
1295
 
1183
1296
  return format_tuple
1184
1297
 
1185
- def _format_args(self, action: argparse.Action, default_metavar: Union[str, tuple[str, ...]]) -> str:
1298
+ def _format_args(self, action: argparse.Action, default_metavar: str) -> str:
1186
1299
  """Handle ranged nargs and make other output less verbose."""
1187
1300
  metavar = self._determine_metavar(action, default_metavar)
1188
1301
  metavar_formatter = self._metavar_formatter(action, default_metavar)
@@ -1207,20 +1320,93 @@ class Cmd2HelpFormatter(RawTextRichHelpFormatter):
1207
1320
  return super()._format_args(action, default_metavar) # type: ignore[arg-type]
1208
1321
 
1209
1322
 
1323
+ class RawDescriptionCmd2HelpFormatter(
1324
+ RawDescriptionRichHelpFormatter,
1325
+ Cmd2HelpFormatter,
1326
+ ):
1327
+ """Cmd2 help message formatter which retains any formatting in descriptions and epilogs."""
1328
+
1329
+
1330
+ class RawTextCmd2HelpFormatter(
1331
+ RawTextRichHelpFormatter,
1332
+ Cmd2HelpFormatter,
1333
+ ):
1334
+ """Cmd2 help message formatter which retains formatting of all help text."""
1335
+
1336
+
1337
+ class ArgumentDefaultsCmd2HelpFormatter(
1338
+ ArgumentDefaultsRichHelpFormatter,
1339
+ Cmd2HelpFormatter,
1340
+ ):
1341
+ """Cmd2 help message formatter which adds default values to argument help."""
1342
+
1343
+
1344
+ class MetavarTypeCmd2HelpFormatter(
1345
+ MetavarTypeRichHelpFormatter,
1346
+ Cmd2HelpFormatter,
1347
+ ):
1348
+ """Cmd2 help message formatter which uses the argument 'type' as the default
1349
+ metavar value (instead of the argument 'dest').
1350
+ """ # noqa: D205
1351
+
1352
+
1353
+ class TextGroup:
1354
+ """A block of text which is formatted like an argparse argument group, including a title.
1355
+
1356
+ Title:
1357
+ Here is the first row of text.
1358
+ Here is yet another row of text.
1359
+ """
1360
+
1361
+ def __init__(
1362
+ self,
1363
+ title: str,
1364
+ text: RenderableType,
1365
+ formatter_creator: Callable[[], Cmd2HelpFormatter],
1366
+ ) -> None:
1367
+ """TextGroup initializer.
1368
+
1369
+ :param title: the group's title
1370
+ :param text: the group's text (string or object that may be rendered by Rich)
1371
+ :param formatter_creator: callable which returns a Cmd2HelpFormatter instance
1372
+ """
1373
+ self.title = title
1374
+ self.text = text
1375
+ self.formatter_creator = formatter_creator
1376
+
1377
+ def __rich__(self) -> Group:
1378
+ """Return a renderable Rich Group object for the class instance.
1379
+
1380
+ This method formats the title and indents the text to match argparse
1381
+ group styling, making the object displayable by a Rich console.
1382
+ """
1383
+ formatter = self.formatter_creator()
1384
+
1385
+ styled_title = Text(
1386
+ type(formatter).group_name_formatter(f"{self.title}:"),
1387
+ style=formatter.styles["argparse.groups"],
1388
+ )
1389
+
1390
+ # Indent text like an argparse argument group does
1391
+ indented_text = ru.indent(self.text, formatter._indent_increment)
1392
+
1393
+ return Group(styled_title, indented_text)
1394
+
1395
+
1210
1396
  class Cmd2ArgumentParser(argparse.ArgumentParser):
1211
1397
  """Custom ArgumentParser class that improves error and help output."""
1212
1398
 
1213
1399
  def __init__(
1214
1400
  self,
1215
- prog: Optional[str] = None,
1216
- usage: Optional[str] = None,
1217
- description: Optional[str] = None,
1218
- epilog: Optional[str] = None,
1401
+ prog: str | None = None,
1402
+ usage: str | None = None,
1403
+ description: RenderableType | None = None,
1404
+ epilog: RenderableType | None = None,
1219
1405
  parents: Sequence[argparse.ArgumentParser] = (),
1220
- formatter_class: type[argparse.HelpFormatter] = Cmd2HelpFormatter,
1406
+ formatter_class: type[Cmd2HelpFormatter] = Cmd2HelpFormatter,
1221
1407
  prefix_chars: str = '-',
1222
- fromfile_prefix_chars: Optional[str] = None,
1223
- argument_default: Optional[str] = None,
1408
+ fromfile_prefix_chars: str | None = None,
1409
+ argument_default: str | None = None,
1224
1410
  conflict_handler: str = 'error',
1225
1411
  add_help: bool = True,
1226
1412
  allow_abbrev: bool = True,
@@ -1228,7 +1414,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1228
1414
  suggest_on_error: bool = False,
1229
1415
  color: bool = False,
1230
1416
  *,
1231
- ap_completer_type: Optional[type['ArgparseCompleter']] = None,
1417
+ ap_completer_type: type['ArgparseCompleter'] | None = None,
1232
1418
  ) -> None:
1233
1419
  """Initialize the Cmd2ArgumentParser instance, a custom ArgumentParser added by cmd2.
1234
1420
 
@@ -1247,10 +1433,10 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1247
1433
  super().__init__(
1248
1434
  prog=prog,
1249
1435
  usage=usage,
1250
- description=description,
1251
- epilog=epilog,
1436
+ description=description, # type: ignore[arg-type]
1437
+ epilog=epilog, # type: ignore[arg-type]
1252
1438
  parents=parents if parents else [],
1253
- formatter_class=formatter_class, # type: ignore[arg-type]
1439
+ formatter_class=formatter_class,
1254
1440
  prefix_chars=prefix_chars,
1255
1441
  fromfile_prefix_chars=fromfile_prefix_chars,
1256
1442
  argument_default=argument_default,
@@ -1261,6 +1447,10 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1261
1447
  **kwargs, # added in Python 3.14
1262
1448
  )
1263
1449
 
1450
+ # Recast to assist type checkers since these can be Rich renderables in a Cmd2HelpFormatter.
1451
+ self.description: RenderableType | None = self.description # type: ignore[assignment]
1452
+ self.epilog: RenderableType | None = self.epilog # type: ignore[assignment]
1453
+
1264
1454
  self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined]
1265
1455
 
1266
1456
  def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: ignore[type-arg]
@@ -1290,8 +1480,18 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1290
1480
  formatted_message += '\n ' + line
1291
1481
 
1292
1482
  self.print_usage(sys.stderr)
1293
- formatted_message = ansi.style_error(formatted_message)
1294
- self.exit(2, f'{formatted_message}\n\n')
1483
+
1484
+ # Add error style to message
1485
+ console = self._get_formatter().console
1486
+ with console.capture() as capture:
1487
+ console.print(formatted_message, style=Cmd2Style.ERROR, crop=False)
1488
+ formatted_message = f"{capture.get()}"
1489
+
1490
+ self.exit(2, f'{formatted_message}\n')
1491
+
1492
+ def _get_formatter(self) -> Cmd2HelpFormatter:
1493
+ """Override _get_formatter with customizations for Cmd2HelpFormatter."""
1494
+ return cast(Cmd2HelpFormatter, super()._get_formatter())
1295
1495
 
1296
1496
  def format_help(self) -> str:
1297
1497
  """Return a string containing a help message, including the program usage and information about the arguments.
@@ -1301,7 +1501,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1301
1501
  formatter = self._get_formatter()
1302
1502
 
1303
1503
  # usage
1304
- formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # type: ignore[arg-type]
1504
+ formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups)
1305
1505
 
1306
1506
  # description
1307
1507
  formatter.add_text(self.description)
@@ -1310,10 +1510,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1310
1510
 
1311
1511
  # positionals, optionals and user-defined groups
1312
1512
  for action_group in self._action_groups:
1313
- if sys.version_info >= (3, 10):
1314
- default_options_group = action_group.title == 'options'
1315
- else:
1316
- default_options_group = action_group.title == 'optional arguments'
1513
+ default_options_group = action_group.title == 'options'
1317
1514
 
1318
1515
  if default_options_group:
1319
1516
  # check if the arguments are required, group accordingly
@@ -1350,12 +1547,9 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1350
1547
  # determine help from format above
1351
1548
  return formatter.format_help() + '\n'
1352
1549
 
1353
- def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: # type: ignore[override]
1354
- # Override _print_message to use style_aware_write() since we use ANSI escape characters to support color
1355
- if message:
1356
- if file is None:
1357
- file = sys.stderr
1358
- ansi.style_aware_write(file, message)
1550
+ def create_text_group(self, title: str, text: RenderableType) -> TextGroup:
1551
+ """Create a TextGroup using this parser's formatter creator."""
1552
+ return TextGroup(title, text, self._get_formatter)
1359
1553
 
1360
1554
 
1361
1555
  class Cmd2AttributeWrapper:
@@ -1378,15 +1572,20 @@ class Cmd2AttributeWrapper:
1378
1572
  self.__attribute = new_val
1379
1573
 
1380
1574
 
1381
- # The default ArgumentParser class for a cmd2 app
1382
- DEFAULT_ARGUMENT_PARSER: type[argparse.ArgumentParser] = Cmd2ArgumentParser
1575
+ # Parser type used by cmd2's built-in commands.
1576
+ # Set it using cmd2.set_default_argument_parser_type().
1577
+ DEFAULT_ARGUMENT_PARSER: type[Cmd2ArgumentParser] = Cmd2ArgumentParser
1578
+
1579
+
1580
+ def set_default_argument_parser_type(parser_type: type[Cmd2ArgumentParser]) -> None:
1581
+ """Set the default ArgumentParser class for cmd2's built-in commands.
1383
1582
 
1583
+ Since built-in commands rely on customizations made in Cmd2ArgumentParser,
1584
+ your custom parser class should inherit from Cmd2ArgumentParser.
1384
1585
 
1385
- def set_default_argument_parser_type(parser_type: type[argparse.ArgumentParser]) -> None:
1386
- """Set the default ArgumentParser class for a cmd2 app.
1586
+ This should be called prior to instantiating your CLI object.
1387
1587
 
1388
- This must be called prior to loading cmd2.py if you want to override the parser for cmd2's built-in commands.
1389
- See examples/override_parser.py.
1588
+ See examples/custom_parser.py.
1390
1589
  """
1391
1590
  global DEFAULT_ARGUMENT_PARSER # noqa: PLW0603
1392
1591
  DEFAULT_ARGUMENT_PARSER = parser_type