cmd2 2.6.2__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.
@@ -229,26 +263,41 @@ from argparse import (
229
263
  ZERO_OR_MORE,
230
264
  ArgumentError,
231
265
  )
232
- from collections.abc import Callable, Iterable, Sequence
233
- from gettext import (
234
- gettext,
266
+ from collections.abc import (
267
+ Callable,
268
+ Iterable,
269
+ Sequence,
235
270
  )
271
+ from gettext import gettext
236
272
  from typing import (
237
- IO,
238
273
  TYPE_CHECKING,
239
274
  Any,
275
+ ClassVar,
240
276
  NoReturn,
241
- Optional,
242
277
  Protocol,
243
- Union,
244
278
  cast,
245
279
  runtime_checkable,
246
280
  )
247
281
 
248
- from . import (
249
- ansi,
250
- constants,
282
+ from rich.console import (
283
+ Group,
284
+ RenderableType,
251
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,
295
+ )
296
+
297
+ from . import constants
298
+ from . import rich_utils as ru
299
+ from .rich_utils import Cmd2RichArgparseConsole
300
+ from .styles import Cmd2Style
252
301
 
253
302
  if TYPE_CHECKING: # pragma: no cover
254
303
  from .argparse_completer import (
@@ -275,6 +324,56 @@ def generate_range_error(range_min: int, range_max: float) -> str:
275
324
  return err_str
276
325
 
277
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
+
278
377
  class CompletionItem(str): # noqa: SLOT000
279
378
  """Completion item with descriptive text attached.
280
379
 
@@ -285,15 +384,22 @@ class CompletionItem(str): # noqa: SLOT000
285
384
  """Responsible for creating and returning a new instance, called before __init__ when an object is instantiated."""
286
385
  return super().__new__(cls, value)
287
386
 
288
- def __init__(self, value: object, description: str = '', *args: Any) -> None:
387
+ def __init__(self, value: object, descriptive_data: Sequence[Any], *args: Any) -> None:
289
388
  """CompletionItem Initializer.
290
389
 
291
390
  :param value: the value being tab completed
292
- :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.
293
394
  :param args: args for str __init__
294
395
  """
295
396
  super().__init__(*args)
296
- 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)
297
403
 
298
404
  # Save the original value to support CompletionItems as argparse choices.
299
405
  # cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance.
@@ -326,7 +432,7 @@ class ChoicesProviderFuncWithTokens(Protocol):
326
432
  """Enable instances to be called like functions."""
327
433
 
328
434
 
329
- ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens]
435
+ ChoicesProviderFunc = ChoicesProviderFuncBase | ChoicesProviderFuncWithTokens
330
436
 
331
437
 
332
438
  @runtime_checkable
@@ -359,7 +465,7 @@ class CompleterFuncWithTokens(Protocol):
359
465
  """Enable instances to be called like functions."""
360
466
 
361
467
 
362
- CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens]
468
+ CompleterFunc = CompleterFuncBase | CompleterFuncWithTokens
363
469
 
364
470
 
365
471
  class ChoicesCallable:
@@ -371,7 +477,7 @@ class ChoicesCallable:
371
477
  def __init__(
372
478
  self,
373
479
  is_completer: bool,
374
- to_call: Union[CompleterFunc, ChoicesProviderFunc],
480
+ to_call: CompleterFunc | ChoicesProviderFunc,
375
481
  ) -> None:
376
482
  """Initialize the ChoiceCallable instance.
377
483
 
@@ -419,7 +525,7 @@ class ChoicesCallable:
419
525
  ATTR_CHOICES_CALLABLE = 'choices_callable'
420
526
 
421
527
  # Descriptive header that prints when using CompletionItems
422
- ATTR_DESCRIPTIVE_HEADER = 'descriptive_header'
528
+ ATTR_DESCRIPTIVE_HEADERS = 'descriptive_headers'
423
529
 
424
530
  # A tuple specifying nargs as a range (min, max)
425
531
  ATTR_NARGS_RANGE = 'nargs_range'
@@ -432,7 +538,7 @@ ATTR_SUPPRESS_TAB_HINT = 'suppress_tab_hint'
432
538
  ############################################################################################################
433
539
  # Patch argparse.Action with accessors for choice_callable attribute
434
540
  ############################################################################################################
435
- def _action_get_choices_callable(self: argparse.Action) -> Optional[ChoicesCallable]:
541
+ def _action_get_choices_callable(self: argparse.Action) -> ChoicesCallable | None:
436
542
  """Get the choices_callable attribute of an argparse Action.
437
543
 
438
544
  This function is added by cmd2 as a method called ``get_choices_callable()`` to ``argparse.Action`` class.
@@ -442,7 +548,7 @@ def _action_get_choices_callable(self: argparse.Action) -> Optional[ChoicesCalla
442
548
  :param self: argparse Action being queried
443
549
  :return: A ChoicesCallable instance or None if attribute does not exist
444
550
  """
445
- return cast(Optional[ChoicesCallable], getattr(self, ATTR_CHOICES_CALLABLE, None))
551
+ return cast(ChoicesCallable | None, getattr(self, ATTR_CHOICES_CALLABLE, None))
446
552
 
447
553
 
448
554
  setattr(argparse.Action, 'get_choices_callable', _action_get_choices_callable)
@@ -516,44 +622,44 @@ setattr(argparse.Action, 'set_completer', _action_set_completer)
516
622
 
517
623
 
518
624
  ############################################################################################################
519
- # Patch argparse.Action with accessors for descriptive_header attribute
625
+ # Patch argparse.Action with accessors for descriptive_headers attribute
520
626
  ############################################################################################################
521
- def _action_get_descriptive_header(self: argparse.Action) -> Optional[str]:
522
- """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.
523
629
 
524
- 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.
525
631
 
526
- To call: ``action.get_descriptive_header()``
632
+ To call: ``action.get_descriptive_headers()``
527
633
 
528
634
  :param self: argparse Action being queried
529
- :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
530
636
  """
531
- return cast(Optional[str], getattr(self, ATTR_DESCRIPTIVE_HEADER, None))
637
+ return cast(Sequence[str | Column] | None, getattr(self, ATTR_DESCRIPTIVE_HEADERS, None))
532
638
 
533
639
 
534
- setattr(argparse.Action, 'get_descriptive_header', _action_get_descriptive_header)
640
+ setattr(argparse.Action, 'get_descriptive_headers', _action_get_descriptive_headers)
535
641
 
536
642
 
537
- def _action_set_descriptive_header(self: argparse.Action, descriptive_header: Optional[str]) -> None:
538
- """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.
539
645
 
540
- 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.
541
647
 
542
- To call: ``action.set_descriptive_header(descriptive_header)``
648
+ To call: ``action.set_descriptive_headers(descriptive_headers)``
543
649
 
544
650
  :param self: argparse Action being updated
545
- :param descriptive_header: value being assigned
651
+ :param descriptive_headers: value being assigned
546
652
  """
547
- setattr(self, ATTR_DESCRIPTIVE_HEADER, descriptive_header)
653
+ setattr(self, ATTR_DESCRIPTIVE_HEADERS, descriptive_headers)
548
654
 
549
655
 
550
- setattr(argparse.Action, 'set_descriptive_header', _action_set_descriptive_header)
656
+ setattr(argparse.Action, 'set_descriptive_headers', _action_set_descriptive_headers)
551
657
 
552
658
 
553
659
  ############################################################################################################
554
660
  # Patch argparse.Action with accessors for nargs_range attribute
555
661
  ############################################################################################################
556
- 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:
557
663
  """Get the nargs_range attribute of an argparse Action.
558
664
 
559
665
  This function is added by cmd2 as a method called ``get_nargs_range()`` to ``argparse.Action`` class.
@@ -563,13 +669,13 @@ def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[
563
669
  :param self: argparse Action being queried
564
670
  :return: The value of nargs_range or None if attribute does not exist
565
671
  """
566
- 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))
567
673
 
568
674
 
569
675
  setattr(argparse.Action, 'get_nargs_range', _action_get_nargs_range)
570
676
 
571
677
 
572
- 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:
573
679
  """Set the nargs_range attribute of an argparse Action.
574
680
 
575
681
  This function is added by cmd2 as a method called ``set_nargs_range()`` to ``argparse.Action`` class.
@@ -628,7 +734,7 @@ CUSTOM_ACTION_ATTRIBS: set[str] = set()
628
734
  _CUSTOM_ATTRIB_PFX = '_attr_'
629
735
 
630
736
 
631
- 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:
632
738
  """Register a custom argparse argument parameter.
633
739
 
634
740
  The registered name will then be a recognized keyword parameter to the parser's `add_argument()` function.
@@ -694,11 +800,11 @@ orig_actions_container_add_argument = argparse._ActionsContainer.add_argument
694
800
  def _add_argument_wrapper(
695
801
  self: argparse._ActionsContainer,
696
802
  *args: Any,
697
- nargs: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None] = None,
698
- choices_provider: Optional[ChoicesProviderFunc] = None,
699
- 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,
700
806
  suppress_tab_hint: bool = False,
701
- descriptive_header: Optional[str] = None,
807
+ descriptive_headers: list[Column | str] | None = None,
702
808
  **kwargs: Any,
703
809
  ) -> argparse.Action:
704
810
  """Wrap ActionsContainer.add_argument() which supports more settings used by cmd2.
@@ -718,8 +824,8 @@ def _add_argument_wrapper(
718
824
  current argument's help text as a hint. Set this to True to suppress the hint. If this
719
825
  argument's help text is set to argparse.SUPPRESS, then tab hints will not display
720
826
  regardless of the value passed for suppress_tab_hint. Defaults to False.
721
- :param descriptive_header: if the provided choices are CompletionItems, then this header will display
722
- 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.
723
829
 
724
830
  # Args from original function
725
831
  :param kwargs: keyword-arguments recognized by argparse._ActionsContainer.add_argument
@@ -744,7 +850,7 @@ def _add_argument_wrapper(
744
850
  nargs_range = None
745
851
 
746
852
  if nargs is not None:
747
- 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
748
854
  # Check if nargs was given as a range
749
855
  if isinstance(nargs, tuple):
750
856
  # Handle 1-item tuple by setting max to INFINITY
@@ -754,11 +860,11 @@ def _add_argument_wrapper(
754
860
  # Validate nargs tuple
755
861
  if (
756
862
  len(nargs) != 2
757
- or not isinstance(nargs[0], int) # type: ignore[unreachable]
758
- 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)
759
865
  ):
760
866
  raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers')
761
- if nargs[0] >= nargs[1]: # type: ignore[misc]
867
+ if nargs[0] >= nargs[1]:
762
868
  raise ValueError('Invalid nargs range. The first value must be less than the second')
763
869
  if nargs[0] < 0:
764
870
  raise ValueError('Negative numbers are invalid for nargs range')
@@ -766,7 +872,7 @@ def _add_argument_wrapper(
766
872
  # Save the nargs tuple as our range setting
767
873
  nargs_range = nargs
768
874
  range_min = nargs_range[0]
769
- range_max = nargs_range[1] # type: ignore[misc]
875
+ range_max = nargs_range[1]
770
876
 
771
877
  # Convert nargs into a format argparse recognizes
772
878
  if range_min == 0:
@@ -802,7 +908,7 @@ def _add_argument_wrapper(
802
908
  new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
803
909
 
804
910
  # Set the custom attributes
805
- 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]
806
912
 
807
913
  if choices_provider:
808
914
  new_arg.set_choices_provider(choices_provider) # type: ignore[attr-defined]
@@ -810,7 +916,7 @@ def _add_argument_wrapper(
810
916
  new_arg.set_completer(completer) # type: ignore[attr-defined]
811
917
 
812
918
  new_arg.set_suppress_tab_hint(suppress_tab_hint) # type: ignore[attr-defined]
813
- new_arg.set_descriptive_header(descriptive_header) # type: ignore[attr-defined]
919
+ new_arg.set_descriptive_headers(descriptive_headers) # type: ignore[attr-defined]
814
920
 
815
921
  for keyword, value in custom_attribs.items():
816
922
  attr_setter = getattr(new_arg, f'set_{keyword}', None)
@@ -885,7 +991,7 @@ setattr(argparse.ArgumentParser, '_match_argument', _match_argument_wrapper)
885
991
  ATTR_AP_COMPLETER_TYPE = 'ap_completer_type'
886
992
 
887
993
 
888
- 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
889
995
  """Get the ap_completer_type attribute of an argparse ArgumentParser.
890
996
 
891
997
  This function is added by cmd2 as a method called ``get_ap_completer_type()`` to ``argparse.ArgumentParser`` class.
@@ -895,7 +1001,7 @@ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Opti
895
1001
  :param self: ArgumentParser being queried
896
1002
  :return: An ArgparseCompleter-based class or None if attribute does not exist
897
1003
  """
898
- 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))
899
1005
 
900
1006
 
901
1007
  setattr(argparse.ArgumentParser, 'get_ap_completer_type', _ArgumentParser_get_ap_completer_type)
@@ -991,15 +1097,43 @@ setattr(argparse._SubParsersAction, 'remove_parser', _SubParsersAction_remove_pa
991
1097
  ############################################################################################################
992
1098
 
993
1099
 
994
- class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
1100
+ class Cmd2HelpFormatter(RichHelpFormatter):
995
1101
  """Custom help formatter to configure ordering of help text."""
996
1102
 
1103
+ # Disable automatic highlighting in the help text.
1104
+ highlights: ClassVar[list[str]] = []
1105
+
1106
+ # Disable markup rendering in usage, help, description, and epilog text.
1107
+ # cmd2's built-in commands do not escape opening brackets in their help text
1108
+ # and therefore rely on these settings being False. If you desire to use
1109
+ # markup in your help text, inherit from Cmd2HelpFormatter and override
1110
+ # these settings in that child class.
1111
+ usage_markup: ClassVar[bool] = False
1112
+ help_markup: ClassVar[bool] = False
1113
+ text_markup: ClassVar[bool] = False
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
+
997
1131
  def _format_usage(
998
1132
  self,
999
- usage: Optional[str],
1133
+ usage: str | None,
1000
1134
  actions: Iterable[argparse.Action],
1001
1135
  groups: Iterable[argparse._ArgumentGroup],
1002
- prefix: Optional[str] = None,
1136
+ prefix: str | None = None,
1003
1137
  ) -> str:
1004
1138
  if prefix is None:
1005
1139
  prefix = gettext('Usage: ')
@@ -1053,7 +1187,7 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
1053
1187
  # End cmd2 customization
1054
1188
 
1055
1189
  # helper for wrapping lines
1056
- 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]:
1057
1191
  lines: list[str] = []
1058
1192
  line: list[str] = []
1059
1193
  line_len = len(prefix) - 1 if prefix is not None else len(indent) - 1
@@ -1133,8 +1267,8 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
1133
1267
  def _determine_metavar(
1134
1268
  self,
1135
1269
  action: argparse.Action,
1136
- default_metavar: Union[str, tuple[str, ...]],
1137
- ) -> Union[str, tuple[str, ...]]:
1270
+ default_metavar: str,
1271
+ ) -> str | tuple[str, ...]:
1138
1272
  """Determine what to use as the metavar value of an action."""
1139
1273
  if action.metavar is not None:
1140
1274
  result = action.metavar
@@ -1150,7 +1284,7 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
1150
1284
  def _metavar_formatter(
1151
1285
  self,
1152
1286
  action: argparse.Action,
1153
- default_metavar: Union[str, tuple[str, ...]],
1287
+ default_metavar: str,
1154
1288
  ) -> Callable[[int], tuple[str, ...]]:
1155
1289
  metavar = self._determine_metavar(action, default_metavar)
1156
1290
 
@@ -1161,7 +1295,7 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
1161
1295
 
1162
1296
  return format_tuple
1163
1297
 
1164
- 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:
1165
1299
  """Handle ranged nargs and make other output less verbose."""
1166
1300
  metavar = self._determine_metavar(action, default_metavar)
1167
1301
  metavar_formatter = self._metavar_formatter(action, default_metavar)
@@ -1186,20 +1320,93 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
1186
1320
  return super()._format_args(action, default_metavar) # type: ignore[arg-type]
1187
1321
 
1188
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
+
1189
1396
  class Cmd2ArgumentParser(argparse.ArgumentParser):
1190
1397
  """Custom ArgumentParser class that improves error and help output."""
1191
1398
 
1192
1399
  def __init__(
1193
1400
  self,
1194
- prog: Optional[str] = None,
1195
- usage: Optional[str] = None,
1196
- description: Optional[str] = None,
1197
- epilog: Optional[str] = None,
1401
+ prog: str | None = None,
1402
+ usage: str | None = None,
1403
+ description: RenderableType | None = None,
1404
+ epilog: RenderableType | None = None,
1198
1405
  parents: Sequence[argparse.ArgumentParser] = (),
1199
- formatter_class: type[argparse.HelpFormatter] = Cmd2HelpFormatter,
1406
+ formatter_class: type[Cmd2HelpFormatter] = Cmd2HelpFormatter,
1200
1407
  prefix_chars: str = '-',
1201
- fromfile_prefix_chars: Optional[str] = None,
1202
- argument_default: Optional[str] = None,
1408
+ fromfile_prefix_chars: str | None = None,
1409
+ argument_default: str | None = None,
1203
1410
  conflict_handler: str = 'error',
1204
1411
  add_help: bool = True,
1205
1412
  allow_abbrev: bool = True,
@@ -1207,7 +1414,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1207
1414
  suggest_on_error: bool = False,
1208
1415
  color: bool = False,
1209
1416
  *,
1210
- ap_completer_type: Optional[type['ArgparseCompleter']] = None,
1417
+ ap_completer_type: type['ArgparseCompleter'] | None = None,
1211
1418
  ) -> None:
1212
1419
  """Initialize the Cmd2ArgumentParser instance, a custom ArgumentParser added by cmd2.
1213
1420
 
@@ -1215,42 +1422,34 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1215
1422
  behavior on this parser. If this is None or not present, then cmd2 will use
1216
1423
  argparse_completer.DEFAULT_AP_COMPLETER when tab completing this parser's arguments
1217
1424
  """
1425
+ kwargs: dict[str, bool] = {}
1218
1426
  if sys.version_info >= (3, 14):
1219
1427
  # Python >= 3.14 so pass new arguments to parent argparse.ArgumentParser class
1220
- super().__init__(
1221
- prog=prog,
1222
- usage=usage,
1223
- description=description,
1224
- epilog=epilog,
1225
- parents=parents if parents else [],
1226
- formatter_class=formatter_class, # type: ignore[arg-type]
1227
- prefix_chars=prefix_chars,
1228
- fromfile_prefix_chars=fromfile_prefix_chars,
1229
- argument_default=argument_default,
1230
- conflict_handler=conflict_handler,
1231
- add_help=add_help,
1232
- allow_abbrev=allow_abbrev,
1233
- exit_on_error=exit_on_error, # added in Python 3.9
1234
- suggest_on_error=suggest_on_error, # added in Python 3.14
1235
- color=color, # added in Python 3.14
1236
- )
1237
- else:
1238
- # Python < 3.14, so don't pass new arguments to parent argparse.ArgumentParser class
1239
- super().__init__(
1240
- prog=prog,
1241
- usage=usage,
1242
- description=description,
1243
- epilog=epilog,
1244
- parents=parents if parents else [],
1245
- formatter_class=formatter_class, # type: ignore[arg-type]
1246
- prefix_chars=prefix_chars,
1247
- fromfile_prefix_chars=fromfile_prefix_chars,
1248
- argument_default=argument_default,
1249
- conflict_handler=conflict_handler,
1250
- add_help=add_help,
1251
- allow_abbrev=allow_abbrev,
1252
- exit_on_error=exit_on_error, # added in Python 3.9
1253
- )
1428
+ kwargs = {
1429
+ "suggest_on_error": suggest_on_error,
1430
+ "color": color,
1431
+ }
1432
+
1433
+ super().__init__(
1434
+ prog=prog,
1435
+ usage=usage,
1436
+ description=description, # type: ignore[arg-type]
1437
+ epilog=epilog, # type: ignore[arg-type]
1438
+ parents=parents if parents else [],
1439
+ formatter_class=formatter_class,
1440
+ prefix_chars=prefix_chars,
1441
+ fromfile_prefix_chars=fromfile_prefix_chars,
1442
+ argument_default=argument_default,
1443
+ conflict_handler=conflict_handler,
1444
+ add_help=add_help,
1445
+ allow_abbrev=allow_abbrev,
1446
+ exit_on_error=exit_on_error, # added in Python 3.9
1447
+ **kwargs, # added in Python 3.14
1448
+ )
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]
1254
1453
 
1255
1454
  self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined]
1256
1455
 
@@ -1281,8 +1480,18 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1281
1480
  formatted_message += '\n ' + line
1282
1481
 
1283
1482
  self.print_usage(sys.stderr)
1284
- formatted_message = ansi.style_error(formatted_message)
1285
- 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())
1286
1495
 
1287
1496
  def format_help(self) -> str:
1288
1497
  """Return a string containing a help message, including the program usage and information about the arguments.
@@ -1292,7 +1501,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1292
1501
  formatter = self._get_formatter()
1293
1502
 
1294
1503
  # usage
1295
- 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)
1296
1505
 
1297
1506
  # description
1298
1507
  formatter.add_text(self.description)
@@ -1301,10 +1510,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1301
1510
 
1302
1511
  # positionals, optionals and user-defined groups
1303
1512
  for action_group in self._action_groups:
1304
- if sys.version_info >= (3, 10):
1305
- default_options_group = action_group.title == 'options'
1306
- else:
1307
- default_options_group = action_group.title == 'optional arguments'
1513
+ default_options_group = action_group.title == 'options'
1308
1514
 
1309
1515
  if default_options_group:
1310
1516
  # check if the arguments are required, group accordingly
@@ -1341,12 +1547,9 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
1341
1547
  # determine help from format above
1342
1548
  return formatter.format_help() + '\n'
1343
1549
 
1344
- def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: # type: ignore[override]
1345
- # Override _print_message to use style_aware_write() since we use ANSI escape characters to support color
1346
- if message:
1347
- if file is None:
1348
- file = sys.stderr
1349
- 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)
1350
1553
 
1351
1554
 
1352
1555
  class Cmd2AttributeWrapper:
@@ -1369,15 +1572,20 @@ class Cmd2AttributeWrapper:
1369
1572
  self.__attribute = new_val
1370
1573
 
1371
1574
 
1372
- # The default ArgumentParser class for a cmd2 app
1373
- 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.
1374
1582
 
1583
+ Since built-in commands rely on customizations made in Cmd2ArgumentParser,
1584
+ your custom parser class should inherit from Cmd2ArgumentParser.
1375
1585
 
1376
- def set_default_argument_parser_type(parser_type: type[argparse.ArgumentParser]) -> None:
1377
- """Set the default ArgumentParser class for a cmd2 app.
1586
+ This should be called prior to instantiating your CLI object.
1378
1587
 
1379
- This must be called prior to loading cmd2.py if you want to override the parser for cmd2's built-in commands.
1380
- See examples/override_parser.py.
1588
+ See examples/custom_parser.py.
1381
1589
  """
1382
1590
  global DEFAULT_ARGUMENT_PARSER # noqa: PLW0603
1383
1591
  DEFAULT_ARGUMENT_PARSER = parser_type