diff --git a/cmd2/__init__.py b/cmd2/__init__.py index b6b56682f..618c04723 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -6,7 +6,10 @@ with contextlib.suppress(importlib_metadata.PackageNotFoundError): __version__ = importlib_metadata.version(__name__) -from . import plugin +from . import ( + plugin, + rich_utils, +) from .ansi import ( Bg, Cursor, @@ -96,6 +99,7 @@ 'SkipPostcommandHooks', # modules 'plugin', + 'rich_utils', # Utilities 'categorize', 'CompletionMode', diff --git a/cmd2/ansi.py b/cmd2/ansi.py index cca020188..929d77bd7 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -19,6 +19,8 @@ wcswidth, ) +from . import rich_utils + ####################################################### # Common ANSI escape sequence constants ####################################################### @@ -28,38 +30,6 @@ BEL = '\a' -class AllowStyle(Enum): - """Values for ``cmd2.ansi.allow_style``.""" - - ALWAYS = 'Always' # Always output ANSI style sequences - NEVER = 'Never' # Remove ANSI style sequences from all output - TERMINAL = 'Terminal' # Remove ANSI style sequences if the output is not going to the terminal - - def __str__(self) -> str: - """Return value instead of enum name for printing in cmd2's set command.""" - return str(self.value) - - def __repr__(self) -> str: - """Return quoted value instead of enum description for printing in cmd2's set command.""" - return repr(self.value) - - -# Controls when ANSI style sequences are allowed in output -allow_style = AllowStyle.TERMINAL -"""When using outside of a cmd2 app, set this variable to one of: - -- ``AllowStyle.ALWAYS`` - always output ANSI style sequences -- ``AllowStyle.NEVER`` - remove ANSI style sequences from all output -- ``AllowStyle.TERMINAL`` - remove ANSI style sequences if the output is not going to the terminal - -to control how ANSI style sequences are handled by ``style_aware_write()``. - -``style_aware_write()`` is called by cmd2 methods like ``poutput()``, ``perror()``, -``pwarning()``, etc. - -The default is ``AllowStyle.TERMINAL``. -""" - # Regular expression to match ANSI style sequence ANSI_STYLE_RE = re.compile(rf'{ESC}\[[^m]*m') @@ -133,8 +103,11 @@ def style_aware_write(fileobj: IO[str], msg: str) -> None: :param fileobj: the file object being written to :param msg: the string being written """ - if allow_style == AllowStyle.NEVER or (allow_style == AllowStyle.TERMINAL and not fileobj.isatty()): + if rich_utils.allow_style == rich_utils.AllowStyle.NEVER or ( + rich_utils.allow_style == rich_utils.AllowStyle.TERMINAL and not fileobj.isatty() + ): msg = strip_style(msg) + fileobj.write(msg) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 79d17a8ce..41f063549 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -236,7 +236,6 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) ) from gettext import gettext from typing import ( - IO, TYPE_CHECKING, Any, ClassVar, @@ -248,11 +247,23 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) runtime_checkable, ) -from rich_argparse import RawTextRichHelpFormatter +from rich.console import ( + Group, + RenderableType, +) +from rich.table import Column, Table +from rich.text import Text +from rich_argparse import ( + ArgumentDefaultsRichHelpFormatter, + MetavarTypeRichHelpFormatter, + RawDescriptionRichHelpFormatter, + RawTextRichHelpFormatter, + RichHelpFormatter, +) from . import ( - ansi, constants, + rich_utils, ) if TYPE_CHECKING: # pragma: no cover @@ -759,11 +770,11 @@ def _add_argument_wrapper( # Validate nargs tuple if ( len(nargs) != 2 - or not isinstance(nargs[0], int) # type: ignore[unreachable] - or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) # type: ignore[misc] + or not isinstance(nargs[0], int) + or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) ): raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers') - if nargs[0] >= nargs[1]: # type: ignore[misc] + if nargs[0] >= nargs[1]: raise ValueError('Invalid nargs range. The first value must be less than the second') if nargs[0] < 0: raise ValueError('Negative numbers are invalid for nargs range') @@ -771,7 +782,7 @@ def _add_argument_wrapper( # Save the nargs tuple as our range setting nargs_range = nargs range_min = nargs_range[0] - range_max = nargs_range[1] # type: ignore[misc] + range_max = nargs_range[1] # Convert nargs into a format argparse recognizes if range_min == 0: @@ -807,7 +818,7 @@ def _add_argument_wrapper( new_arg = orig_actions_container_add_argument(self, *args, **kwargs) # Set the custom attributes - new_arg.set_nargs_range(nargs_range) # type: ignore[arg-type, attr-defined] + new_arg.set_nargs_range(nargs_range) # type: ignore[attr-defined] if choices_provider: new_arg.set_choices_provider(choices_provider) # type: ignore[attr-defined] @@ -996,13 +1007,9 @@ def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) ############################################################################################################ -class Cmd2HelpFormatter(RawTextRichHelpFormatter): +class Cmd2HelpFormatter(RichHelpFormatter): """Custom help formatter to configure ordering of help text.""" - # rich-argparse formats all group names with str.title(). - # Override their formatter to do nothing. - group_name_formatter: ClassVar[Callable[[str], str]] = str - # Disable automatic highlighting in the help text. highlights: ClassVar[list[str]] = [] @@ -1015,6 +1022,22 @@ class Cmd2HelpFormatter(RawTextRichHelpFormatter): help_markup: ClassVar[bool] = False text_markup: ClassVar[bool] = False + def __init__( + self, + prog: str, + indent_increment: int = 2, + max_help_position: int = 24, + width: Optional[int] = None, + *, + console: Optional[rich_utils.Cmd2Console] = None, + **kwargs: Any, + ) -> None: + """Initialize Cmd2HelpFormatter.""" + if console is None: + console = rich_utils.Cmd2Console(sys.stdout) + + super().__init__(prog, indent_increment, max_help_position, width, console=console, **kwargs) + def _format_usage( self, usage: Optional[str], @@ -1207,6 +1230,82 @@ def _format_args(self, action: argparse.Action, default_metavar: Union[str, tupl return super()._format_args(action, default_metavar) # type: ignore[arg-type] +class RawDescriptionCmd2HelpFormatter( + RawDescriptionRichHelpFormatter, + Cmd2HelpFormatter, +): + """Cmd2 help message formatter which retains any formatting in descriptions and epilogs.""" + + +class RawTextCmd2HelpFormatter( + RawTextRichHelpFormatter, + Cmd2HelpFormatter, +): + """Cmd2 help message formatter which retains formatting of all help text.""" + + +class ArgumentDefaultsCmd2HelpFormatter( + ArgumentDefaultsRichHelpFormatter, + Cmd2HelpFormatter, +): + """Cmd2 help message formatter which adds default values to argument help.""" + + +class MetavarTypeCmd2HelpFormatter( + MetavarTypeRichHelpFormatter, + Cmd2HelpFormatter, +): + """Cmd2 help message formatter which uses the argument 'type' as the default + metavar value (instead of the argument 'dest'). + """ # noqa: D205 + + +class TextGroup: + """A block of text which is formatted like an argparse argument group, including a title. + + Title: + Here is the first row of text. + Here is yet another row of text. + """ + + def __init__( + self, + title: str, + text: RenderableType, + formatter_creator: Callable[[], Cmd2HelpFormatter], + ) -> None: + """TextGroup initializer. + + :param title: the group's title + :param text: the group's text (string or object that may be rendered by Rich) + :param formatter_creator: callable which returns a Cmd2HelpFormatter instance + """ + self.title = title + self.text = text + self.formatter_creator = formatter_creator + + def __rich__(self) -> Group: + """Perform custom rendering.""" + formatter = self.formatter_creator() + + styled_title = Text( + type(formatter).group_name_formatter(f"{self.title}:"), + style=formatter.styles["argparse.groups"], + ) + + # Left pad the text like an argparse argument group does + left_padding = formatter._indent_increment + text_table = Table( + Column(overflow="fold"), + box=None, + show_header=False, + padding=(0, 0, 0, left_padding), + ) + text_table.add_row(self.text) + + return Group(styled_title, text_table) + + class Cmd2ArgumentParser(argparse.ArgumentParser): """Custom ArgumentParser class that improves error and help output.""" @@ -1214,10 +1313,10 @@ def __init__( self, prog: Optional[str] = None, usage: Optional[str] = None, - description: Optional[str] = None, - epilog: Optional[str] = None, + description: Optional[RenderableType] = None, + epilog: Optional[RenderableType] = None, parents: Sequence[argparse.ArgumentParser] = (), - formatter_class: type[argparse.HelpFormatter] = Cmd2HelpFormatter, + formatter_class: type[Cmd2HelpFormatter] = Cmd2HelpFormatter, prefix_chars: str = '-', fromfile_prefix_chars: Optional[str] = None, argument_default: Optional[str] = None, @@ -1247,8 +1346,8 @@ def __init__( super().__init__( prog=prog, usage=usage, - description=description, - epilog=epilog, + description=description, # type: ignore[arg-type] + epilog=epilog, # type: ignore[arg-type] parents=parents if parents else [], formatter_class=formatter_class, # type: ignore[arg-type] prefix_chars=prefix_chars, @@ -1261,6 +1360,10 @@ def __init__( **kwargs, # added in Python 3.14 ) + # Recast to assist type checkers since these can be Rich renderables in a Cmd2HelpFormatter. + self.description: Optional[RenderableType] = self.description # type: ignore[assignment] + self.epilog: Optional[RenderableType] = self.epilog # type: ignore[assignment] + self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined] def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: ignore[type-arg] @@ -1290,8 +1393,18 @@ def error(self, message: str) -> NoReturn: formatted_message += '\n ' + line self.print_usage(sys.stderr) - formatted_message = ansi.style_error(formatted_message) - self.exit(2, f'{formatted_message}\n\n') + + # Add error style to message + console = self._get_formatter().console + with console.capture() as capture: + console.print(formatted_message, style="cmd2.error", crop=False) + formatted_message = f"{capture.get()}" + + self.exit(2, f'{formatted_message}\n') + + def _get_formatter(self) -> Cmd2HelpFormatter: + """Override _get_formatter with customizations for Cmd2HelpFormatter.""" + return cast(Cmd2HelpFormatter, super()._get_formatter()) def format_help(self) -> str: """Return a string containing a help message, including the program usage and information about the arguments. @@ -1350,12 +1463,9 @@ def format_help(self) -> str: # determine help from format above return formatter.format_help() + '\n' - def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: # type: ignore[override] - # Override _print_message to use style_aware_write() since we use ANSI escape characters to support color - if message: - if file is None: - file = sys.stderr - ansi.style_aware_write(file, message) + def create_text_group(self, title: str, text: RenderableType) -> TextGroup: + """Create a TextGroup using this parser's formatter creator.""" + return TextGroup(title, text, self._get_formatter) class Cmd2AttributeWrapper: diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index bccfb8881..3e316d807 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -67,12 +67,15 @@ cast, ) +from rich.console import Group + from . import ( ansi, argparse_completer, argparse_custom, constants, plugin, + rich_utils, utils, ) from .argparse_custom import ( @@ -261,8 +264,8 @@ def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]: parser = self._cmd._build_parser(parent, parser_builder, command) # If the description has not been set, then use the method docstring if one exists - if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__: - parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__) + if parser.description is None and command_method.__doc__: + parser.description = strip_doc_annotations(command_method.__doc__) self._parsers[full_method_name] = parser @@ -286,10 +289,6 @@ class Cmd(cmd.Cmd): DEFAULT_EDITOR = utils.find_editor() - INTERNAL_COMMAND_EPILOG = ( - "Notes:\n This command is for internal use and is not intended to be called from the\n command line." - ) - # Sorting keys for strings ALPHABETICAL_SORT_KEY = utils.norm_fold NATURAL_SORT_KEY = utils.natural_keys @@ -1116,16 +1115,16 @@ def build_settables(self) -> None: def get_allow_style_choices(_cli_self: Cmd) -> list[str]: """Tab complete allow_style values.""" - return [val.name.lower() for val in ansi.AllowStyle] + return [val.name.lower() for val in rich_utils.AllowStyle] - def allow_style_type(value: str) -> ansi.AllowStyle: - """Convert a string value into an ansi.AllowStyle.""" + def allow_style_type(value: str) -> rich_utils.AllowStyle: + """Convert a string value into an rich_utils.AllowStyle.""" try: - return ansi.AllowStyle[value.upper()] + return rich_utils.AllowStyle[value.upper()] except KeyError as esc: raise ValueError( - f"must be {ansi.AllowStyle.ALWAYS}, {ansi.AllowStyle.NEVER}, or " - f"{ansi.AllowStyle.TERMINAL} (case-insensitive)" + f"must be {rich_utils.AllowStyle.ALWAYS}, {rich_utils.AllowStyle.NEVER}, or " + f"{rich_utils.AllowStyle.TERMINAL} (case-insensitive)" ) from esc self.add_settable( @@ -1133,7 +1132,7 @@ def allow_style_type(value: str) -> ansi.AllowStyle: 'allow_style', allow_style_type, 'Allow ANSI text style sequences in output (valid values: ' - f'{ansi.AllowStyle.ALWAYS}, {ansi.AllowStyle.NEVER}, {ansi.AllowStyle.TERMINAL})', + f'{rich_utils.AllowStyle.ALWAYS}, {rich_utils.AllowStyle.NEVER}, {rich_utils.AllowStyle.TERMINAL})', self, choices_provider=cast(ChoicesProviderFunc, get_allow_style_choices), ) @@ -1156,14 +1155,14 @@ def allow_style_type(value: str) -> ansi.AllowStyle: # ----- Methods related to presenting output to the user ----- @property - def allow_style(self) -> ansi.AllowStyle: + def allow_style(self) -> rich_utils.AllowStyle: """Read-only property needed to support do_set when it reads allow_style.""" - return ansi.allow_style + return rich_utils.allow_style @allow_style.setter - def allow_style(self, new_val: ansi.AllowStyle) -> None: + def allow_style(self, new_val: rich_utils.AllowStyle) -> None: """Setter property needed to support do_set when it updates allow_style.""" - ansi.allow_style = new_val + rich_utils.allow_style = new_val def _completion_supported(self) -> bool: """Return whether tab completion is supported.""" @@ -1312,7 +1311,7 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None: # Also only attempt to use a pager if actually running in a real fully functional terminal. if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script(): final_msg = f"{msg}{end}" - if ansi.allow_style == ansi.AllowStyle.NEVER: + if rich_utils.allow_style == rich_utils.AllowStyle.NEVER: final_msg = ansi.strip_style(final_msg) pager = self.pager @@ -3219,7 +3218,11 @@ def _cmdloop(self) -> None: # Top-level parser for alias @staticmethod def _build_alias_parser() -> Cmd2ArgumentParser: - alias_description = "Manage aliases\n\nAn alias is a command that enables replacement of a word by another string." + alias_description = Group( + "Manage aliases.", + "\n", + "An alias is a command that enables replacement of a word by another string.", + ) alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description) alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True) @@ -3234,28 +3237,38 @@ def do_alias(self, args: argparse.Namespace) -> None: handler(args) # alias -> create - alias_create_description = "Create or overwrite an alias" - @classmethod def _build_alias_create_parser(cls) -> Cmd2ArgumentParser: - alias_create_epilog = ( - "Notes:\n" - " If you want to use redirection, pipes, or terminators in the value of the\n" - " alias, then quote them.\n" - "\n" - " Since aliases are resolved during parsing, tab completion will function as\n" - " it would for the actual command the alias resolves to.\n" - "\n" - "Examples:\n" - " alias create ls !ls -lF\n" - " alias create show_log !cat \"log file.txt\"\n" - " alias create save_results print_results \">\" out.txt\n" + alias_create_description = "Create or overwrite an alias." + alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_create_description) + + # Create Notes TextGroup + alias_create_notes = Group( + "If you want to use redirection, pipes, or terminators in the value of the alias, then quote them.", + "\n", + ( + "Since aliases are resolved during parsing, tab completion will function as it would " + "for the actual command the alias resolves to." + ), + ) + notes_group = alias_create_parser.create_text_group("Notes", alias_create_notes) + + # Create Examples TextGroup + alias_create_examples = Group( + "alias create ls !ls -lF", + "alias create show_log !cat \"log file.txt\"", + "alias create save_results print_results \">\" out.txt", ) + examples_group = alias_create_parser.create_text_group("Examples", alias_create_examples) - alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description=cls.alias_create_description, - epilog=alias_create_epilog, + # Display both Notes and Examples in the epilog + alias_create_parser.epilog = Group( + notes_group, + "\n", + examples_group, ) + + # Add arguments alias_create_parser.add_argument('name', help='name of this alias') alias_create_parser.add_argument( 'command', @@ -3271,7 +3284,7 @@ def _build_alias_create_parser(cls) -> Cmd2ArgumentParser: return alias_create_parser - @as_subcommand_to('alias', 'create', _build_alias_create_parser, help=alias_create_description.lower()) + @as_subcommand_to('alias', 'create', _build_alias_create_parser, help="create or overwrite an alias") def _alias_create(self, args: argparse.Namespace) -> None: """Create or overwrite an alias.""" self.last_result = False @@ -3306,7 +3319,7 @@ def _alias_create(self, args: argparse.Namespace) -> None: # alias -> delete @classmethod def _build_alias_delete_parser(cls) -> Cmd2ArgumentParser: - alias_delete_description = "Delete specified aliases or all aliases if --all is used" + alias_delete_description = "Delete specified aliases or all aliases if --all is used." alias_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_delete_description) alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases") @@ -3342,11 +3355,13 @@ def _alias_delete(self, args: argparse.Namespace) -> None: # alias -> list @classmethod def _build_alias_list_parser(cls) -> Cmd2ArgumentParser: - alias_list_description = ( - "List specified aliases in a reusable form that can be saved to a startup\n" - "script to preserve aliases across sessions\n" - "\n" - "Without arguments, all aliases will be listed." + alias_list_description = Group( + ( + "List specified aliases in a reusable form that can be saved to a startup " + "script to preserve aliases across sessions." + ), + "\n", + "Without arguments, all aliases will be listed.", ) alias_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description) @@ -3419,7 +3434,7 @@ def complete_help_subcommands( @classmethod def _build_help_parser(cls) -> Cmd2ArgumentParser: help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description="List available commands or provide detailed help for a specific command" + description="List available commands or provide detailed help for a specific command." ) help_parser.add_argument( '-v', @@ -3639,12 +3654,8 @@ def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None: doc: Optional[str] - # If this is an argparse command, use its description. - if (cmd_parser := self._command_parsers.get(cmd_func)) is not None: - doc = cmd_parser.description - # Non-argparse commands can have help_functions for their documentation - elif command in topics: + if command in topics: help_func = getattr(self, constants.HELP_FUNC_PREFIX + command) result = io.StringIO() @@ -3675,7 +3686,7 @@ def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None: @staticmethod def _build_shortcuts_parser() -> Cmd2ArgumentParser: - return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts") + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts.") @with_argparser(_build_shortcuts_parser) def do_shortcuts(self, _: argparse.Namespace) -> None: @@ -3686,13 +3697,16 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: self.poutput(f"Shortcuts for other commands:\n{result}") self.last_result = True - @classmethod - def _build_eof_parser(cls) -> Cmd2ArgumentParser: - return argparse_custom.DEFAULT_ARGUMENT_PARSER( - description="Called when Ctrl-D is pressed", - epilog=cls.INTERNAL_COMMAND_EPILOG, + @staticmethod + def _build_eof_parser() -> Cmd2ArgumentParser: + eof_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed.") + eof_parser.epilog = eof_parser.create_text_group( + "Note", + "This command is for internal use and is not intended to be called from the command line.", ) + return eof_parser + @with_argparser(_build_eof_parser) def do_eof(self, _: argparse.Namespace) -> Optional[bool]: """Quit with no arguments, called when Ctrl-D is pressed. @@ -3706,7 +3720,7 @@ def do_eof(self, _: argparse.Namespace) -> Optional[bool]: @staticmethod def _build_quit_parser() -> Cmd2ArgumentParser: - return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application") + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application.") @with_argparser(_build_quit_parser) def do_quit(self, _: argparse.Namespace) -> Optional[bool]: @@ -3770,10 +3784,13 @@ def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], p def _build_base_set_parser(cls) -> Cmd2ArgumentParser: # When tab completing value, we recreate the set command parser with a value argument specific to # the settable being edited. To make this easier, define a base parser with all the common elements. - set_description = ( - "Set a settable parameter or show current settings of parameters\n\n" - "Call without arguments for a list of all settable parameters with their values.\n" - "Call with just param to view that parameter's value." + set_description = Group( + "Set a settable parameter or show current settings of parameters.", + "\n", + ( + "Call without arguments for a list of all settable parameters with their values. " + "Call with just param to view that parameter's value." + ), ) base_set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description) base_set_parser.add_argument( @@ -3891,7 +3908,7 @@ def do_set(self, args: argparse.Namespace) -> None: @classmethod def _build_shell_parser(cls) -> Cmd2ArgumentParser: - shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt") + shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt.") shell_parser.add_argument('command', help='the command to run', completer=cls.shell_cmd_complete) shell_parser.add_argument( 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=cls.path_complete @@ -4199,7 +4216,7 @@ def py_quit() -> None: @staticmethod def _build_py_parser() -> Cmd2ArgumentParser: - return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell") + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell.") @with_argparser(_build_py_parser) def do_py(self, _: argparse.Namespace) -> Optional[bool]: @@ -4213,7 +4230,7 @@ def do_py(self, _: argparse.Namespace) -> Optional[bool]: @classmethod def _build_run_pyscript_parser(cls) -> Cmd2ArgumentParser: run_pyscript_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description="Run a Python script file inside the console" + description="Run Python script within this application's environment." ) run_pyscript_parser.add_argument('script_path', help='path to the script file', completer=cls.path_complete) run_pyscript_parser.add_argument( @@ -4224,7 +4241,7 @@ def _build_run_pyscript_parser(cls) -> Cmd2ArgumentParser: @with_argparser(_build_run_pyscript_parser) def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: - """Run a Python script file inside the console. + """Run Python script within this application's environment. :return: True if running of commands should stop """ @@ -4258,11 +4275,11 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: @staticmethod def _build_ipython_parser() -> Cmd2ArgumentParser: - return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell") + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell.") @with_argparser(_build_ipython_parser) def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover - """Enter an interactive IPython shell. + """Run an interactive IPython shell. :return: True if running of commands should stop """ @@ -4334,9 +4351,11 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover @classmethod def _build_history_parser(cls) -> Cmd2ArgumentParser: - history_description = "View, run, edit, save, or clear previously entered commands" + history_description = "View, run, edit, save, or clear previously entered commands." - history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=history_description) + history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( + description=history_description, formatter_class=argparse_custom.RawTextCmd2HelpFormatter + ) history_action_group = history_parser.add_mutually_exclusive_group() history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items') history_action_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items') @@ -4351,7 +4370,7 @@ def _build_history_parser(cls) -> Cmd2ArgumentParser: '-t', '--transcript', metavar='TRANSCRIPT_FILE', - help='create a transcript file by re-running the commands,\nimplies both -r and -s', + help='create a transcript file by re-running the commands, implies both -r and -s', completer=cls.path_complete, ) history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history') @@ -4361,7 +4380,7 @@ def _build_history_parser(cls) -> Cmd2ArgumentParser: '-s', '--script', action='store_true', - help='output commands in script format, i.e. without command\nnumbers', + help='output commands in script format, i.e. without command numbers', ) history_format_group.add_argument( '-x', @@ -4373,13 +4392,13 @@ def _build_history_parser(cls) -> Cmd2ArgumentParser: '-v', '--verbose', action='store_true', - help='display history and include expanded commands if they\ndiffer from the typed command', + help='display history and include expanded commands if they differ from the typed command', ) history_format_group.add_argument( '-a', '--all', action='store_true', - help='display all commands, including ones persisted from\nprevious sessions', + help='display all commands, including ones persisted from previous sessions', ) history_arg_help = ( @@ -4718,15 +4737,15 @@ def _generate_transcript( @classmethod def _build_edit_parser(cls) -> Cmd2ArgumentParser: - edit_description = ( - "Run a text editor and optionally open a file with it\n" - "\n" - "The editor used is determined by a settable parameter. To set it:\n" - "\n" - " set editor (program-name)" - ) + from rich.markdown import Markdown + edit_description = "Run a text editor and optionally open a file with it." edit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=edit_description) + edit_parser.epilog = edit_parser.create_text_group( + "Note", + Markdown("To set a new editor, run: `set editor `"), + ) + edit_parser.add_argument( 'file_path', nargs=argparse.OPTIONAL, @@ -4763,19 +4782,26 @@ def _current_script_dir(self) -> Optional[str]: return self._script_dir[-1] return None - run_script_description = ( - "Run commands in script file that is encoded as either ASCII or UTF-8 text\n" - "\n" - "Script should contain one command per line, just like the command would be\n" - "typed in the console.\n" - "\n" - "If the -t/--transcript flag is used, this command instead records\n" - "the output of the script commands to a transcript for testing purposes.\n" - ) + @classmethod + def _build_base_run_script_parser(cls) -> Cmd2ArgumentParser: + run_script_description = Group( + "Run text script.", + "\n", + "Scripts should contain one command per line, entered as you would in the console.", + ) + + run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=run_script_description) + run_script_parser.add_argument( + 'script_path', + help="path to the script file", + completer=cls.path_complete, + ) + + return run_script_parser @classmethod def _build_run_script_parser(cls) -> Cmd2ArgumentParser: - run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=cls.run_script_description) + run_script_parser = cls._build_base_run_script_parser() run_script_parser.add_argument( '-t', '--transcript', @@ -4783,17 +4809,12 @@ def _build_run_script_parser(cls) -> Cmd2ArgumentParser: help='record the output of the script as a transcript file', completer=cls.path_complete, ) - run_script_parser.add_argument( - 'script_path', - help="path to the script file", - completer=cls.path_complete, - ) return run_script_parser @with_argparser(_build_run_script_parser) def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: - """Run commands in script file that is encoded as either ASCII or UTF-8 text. + """Run text script. :return: True if running of commands should stop """ @@ -4856,31 +4877,36 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: @classmethod def _build_relative_run_script_parser(cls) -> Cmd2ArgumentParser: - relative_run_script_description = cls.run_script_description - relative_run_script_description += ( - "\n\n" - "If this is called from within an already-running script, the filename will be\n" - "interpreted relative to the already-running script's directory." + relative_run_script_parser = cls._build_base_run_script_parser() + + # Append to existing description + relative_run_script_parser.description = Group( + cast(Group, relative_run_script_parser.description), + "\n", + ( + "If this is called from within an already-running script, the filename will be " + "interpreted relative to the already-running script's directory." + ), ) - relative_run_script_epilog = "Notes:\n This command is intended to only be used within text file scripts." - - relative_run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description=relative_run_script_description, epilog=relative_run_script_epilog + relative_run_script_parser.epilog = relative_run_script_parser.create_text_group( + "Note", + "This command is intended to be used from within a text script.", ) - relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script') return relative_run_script_parser @with_argparser(_build_relative_run_script_parser) def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]: - """Run commands in script file that is encoded as either ASCII or UTF-8 text. + """Run text script. + + This command is intended to be used from within a text script. :return: True if running of commands should stop """ - file_path = args.file_path + script_path = args.script_path # NOTE: Relative path is an absolute path, it is just relative to the current script directory - relative_path = os.path.join(self._current_script_dir or '', file_path) + relative_path = os.path.join(self._current_script_dir or '', script_path) # self.last_result will be set by do_run_script() return self.do_run_script(utils.quote_string(relative_path)) diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py new file mode 100644 index 000000000..c5d3264b5 --- /dev/null +++ b/cmd2/rich_utils.py @@ -0,0 +1,127 @@ +"""Provides common utilities to support Rich in cmd2 applications.""" + +from collections.abc import Mapping +from enum import Enum +from typing import ( + IO, + Any, + Optional, +) + +from rich.console import Console +from rich.style import ( + Style, + StyleType, +) +from rich.theme import Theme +from rich_argparse import RichHelpFormatter + + +class AllowStyle(Enum): + """Values for ``cmd2.rich_utils.allow_style``.""" + + ALWAYS = 'Always' # Always output ANSI style sequences + NEVER = 'Never' # Remove ANSI style sequences from all output + TERMINAL = 'Terminal' # Remove ANSI style sequences if the output is not going to the terminal + + def __str__(self) -> str: + """Return value instead of enum name for printing in cmd2's set command.""" + return str(self.value) + + def __repr__(self) -> str: + """Return quoted value instead of enum description for printing in cmd2's set command.""" + return repr(self.value) + + +# Controls when ANSI style sequences are allowed in output +allow_style = AllowStyle.TERMINAL + +# Default styles for cmd2 +DEFAULT_CMD2_STYLES: dict[str, StyleType] = { + "cmd2.success": Style(color="green"), + "cmd2.warning": Style(color="bright_yellow"), + "cmd2.error": Style(color="bright_red"), + "cmd2.help_header": Style(color="bright_green", bold=True), +} + +# Include default styles from RichHelpFormatter +DEFAULT_CMD2_STYLES.update(RichHelpFormatter.styles.copy()) + + +class Cmd2Theme(Theme): + """Rich theme class used by Cmd2Console.""" + + def __init__(self, styles: Optional[Mapping[str, StyleType]] = None, inherit: bool = True) -> None: + """Cmd2Theme initializer. + + :param styles: optional mapping of style names on to styles. + Defaults to None for a theme with no styles. + :param inherit: Inherit default styles. Defaults to True. + """ + cmd2_styles = DEFAULT_CMD2_STYLES.copy() if inherit else {} + if styles is not None: + cmd2_styles.update(styles) + + super().__init__(cmd2_styles, inherit=inherit) + + +# Current Rich theme used by Cmd2Console +THEME: Cmd2Theme = Cmd2Theme() + + +def set_theme(new_theme: Cmd2Theme) -> None: + """Set the Rich theme used by Cmd2Console and rich-argparse. + + :param new_theme: new theme to use. + """ + global THEME # noqa: PLW0603 + THEME = new_theme + + # Make sure the new theme has all style names included in a Cmd2Theme. + missing_names = Cmd2Theme().styles.keys() - THEME.styles.keys() + for name in missing_names: + THEME.styles[name] = Style() + + # Update rich-argparse styles + for name in RichHelpFormatter.styles.keys() & THEME.styles.keys(): + RichHelpFormatter.styles[name] = THEME.styles[name] + + +class Cmd2Console(Console): + """Rich console with characteristics appropriate for cmd2 applications.""" + + def __init__(self, file: IO[str]) -> None: + """Cmd2Console initializer. + + :param file: a file object where the console should write to + """ + kwargs: dict[str, Any] = {} + if allow_style == AllowStyle.ALWAYS: + kwargs["force_terminal"] = True + + # Turn off interactive mode if dest is not actually a terminal which supports it + tmp_console = Console(file=file) + kwargs["force_interactive"] = tmp_console.is_interactive + elif allow_style == AllowStyle.NEVER: + kwargs["force_terminal"] = False + + # Turn off automatic markup, emoji, and highlight rendering at the console level. + # You can still enable these in Console.print() calls. + super().__init__( + file=file, + tab_size=4, + markup=False, + emoji=False, + highlight=False, + theme=THEME, + **kwargs, + ) + + def on_broken_pipe(self) -> None: + """Override which raises BrokenPipeError instead of SystemExit.""" + import contextlib + + with contextlib.suppress(SystemExit): + super().on_broken_pipe() + + raise BrokenPipeError diff --git a/docs/features/argument_processing.md b/docs/features/argument_processing.md index a8dd62ba0..9750a596e 100644 --- a/docs/features/argument_processing.md +++ b/docs/features/argument_processing.md @@ -63,7 +63,7 @@ def do_speak(self, opts) !!! note - The `@with_argparser` decorator sets the `prog` variable in the argument parser based on the name of the method it is decorating. This will override anything you specify in `prog` variable when creating the argument parser. + `cmd2` sets the `prog` variable in the argument parser based on the name of the method it is decorating. This will override anything you specify in `prog` variable when creating the argument parser. ## Help Messages diff --git a/docs/features/help.md b/docs/features/help.md index aa2e9d709..41ce44f4c 100644 --- a/docs/features/help.md +++ b/docs/features/help.md @@ -173,17 +173,17 @@ categories with per-command Help Messages: Other ====================================================================================================== - alias Manage aliases + alias Manage aliases. config Config command. - edit Run a text editor and optionally open a file with it - help List available commands or provide detailed help for a specific command - history View, run, edit, save, or clear previously entered commands - quit Exit this application - run_pyscript Run a Python script file inside the console - run_script Run commands in script file that is encoded as either ASCII or UTF-8 text + edit Run a text editor and optionally open a file with it. + help List available commands or provide detailed help for a specific command. + history View, run, edit, save, or clear previously entered commands. + quit Exit this application. + run_pyscript Run Python script within this application's environment. + run_script Run text script. set Set a settable parameter or show current settings of parameters. - shell Execute a command as if at the OS prompt - shortcuts List available shortcuts + shell Execute a command as if at the OS prompt. + shortcuts List available shortcuts. version Version command. When called with the `-v` flag for verbose help, the one-line description for each command is diff --git a/examples/table_creation.py b/examples/table_creation.py index 00a45d292..754fe9721 100755 --- a/examples/table_creation.py +++ b/examples/table_creation.py @@ -10,6 +10,7 @@ EightBitFg, Fg, ansi, + rich_utils, ) from cmd2.table_creator import ( AlternatingTable, @@ -269,6 +270,6 @@ def nested_tables() -> None: if __name__ == '__main__': # Default to terminal mode so redirecting to a file won't include the ANSI style sequences - ansi.allow_style = ansi.AllowStyle.TERMINAL + rich_utils.allow_style = rich_utils.AllowStyle.TERMINAL basic_tables() nested_tables() diff --git a/tests/conftest.py b/tests/conftest.py index 0f7450472..19aedaac7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,39 +44,35 @@ def verify_help_text( assert verbose_string in help_text -# Help text for the history command +# Help text for the history command (Generated when terminal width is 80) HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT_FILE | -c] [-s] [-x] [-v] [-a] [arg] -View, run, edit, save, or clear previously entered commands +View, run, edit, save, or clear previously entered commands. -positional arguments: +Positional Arguments: arg empty all history items a one history item by number a..b, a:b, a:, ..b items by indices (inclusive) string items containing string /regex/ items matching regular expression -optional arguments: +Optional Arguments: -h, --help show this help message and exit -r, --run run selected history items -e, --edit edit and then run selected history items -o, --output_file FILE output commands to a script file, implies -s -t, --transcript TRANSCRIPT_FILE - create a transcript file by re-running the commands, - implies both -r and -s + create a transcript file by re-running the commands, implies both -r and -s -c, --clear clear all history -formatting: - -s, --script output commands in script format, i.e. without command - numbers +Formatting: + -s, --script output commands in script format, i.e. without command numbers -x, --expanded output fully parsed commands with aliases and shortcuts expanded - -v, --verbose display history and include expanded commands if they - differ from the typed command - -a, --all display all commands, including ones persisted from - previous sessions + -v, --verbose display history and include expanded commands if they differ from the typed command + -a, --all display all commands, including ones persisted from previous sessions """ # Output from the shortcuts command with default built-in shortcuts diff --git a/tests/test_argparse.py b/tests/test_argparse.py index ff387ecc3..0ae9e7245 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -33,8 +33,7 @@ def _say_parser_builder() -> cmd2.Cmd2ArgumentParser: @cmd2.with_argparser(_say_parser_builder) def do_say(self, args, *, keyword_arg: Optional[str] = None) -> None: - """Repeat what you - tell me to. + """Repeat what you tell me to. :param args: argparse namespace :param keyword_arg: Optional keyword arguments @@ -212,8 +211,7 @@ def test_argparse_help_docstring(argparse_app) -> None: out, err = run_cmd(argparse_app, 'help say') assert out[0].startswith('Usage: say') assert out[1] == '' - assert out[2] == 'Repeat what you' - assert out[3] == 'tell me to.' + assert out[2] == 'Repeat what you tell me to.' for line in out: assert not line.startswith(':') @@ -362,39 +360,39 @@ def test_subcommand_help(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'help base foo') assert out[0].startswith('Usage: base foo') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' # bar has aliases (usage should never show alias name) out, err = run_cmd(subcommand_app, 'help base bar') assert out[0].startswith('Usage: base bar') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base bar_1') assert out[0].startswith('Usage: base bar') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base bar_2') assert out[0].startswith('Usage: base bar') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' # helpless has aliases and no help text (usage should never show alias name) out, err = run_cmd(subcommand_app, 'help base helpless') assert out[0].startswith('Usage: base helpless') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base helpless_1') assert out[0].startswith('Usage: base helpless') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base helpless_2') assert out[0].startswith('Usage: base helpless') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' def test_subcommand_invalid_help(subcommand_app) -> None: diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index eedd0d3e5..2472ab743 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -237,7 +237,7 @@ def test_apcustom_required_options() -> None: # Make sure a 'required arguments' section shows when a flag is marked required parser = Cmd2ArgumentParser() parser.add_argument('--required_flag', required=True) - assert 'required arguments' in parser.format_help() + assert 'Required Arguments' in parser.format_help() def test_apcustom_metavar_tuple() -> None: diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index ee6667846..e641f9bda 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -24,6 +24,7 @@ constants, exceptions, plugin, + rich_utils, utils, ) from cmd2.rl_utils import ( @@ -48,12 +49,12 @@ def arg_decorator(func): @functools.wraps(func) def cmd_wrapper(*args, **kwargs): - old = ansi.allow_style - ansi.allow_style = style + old = rich_utils.allow_style + rich_utils.allow_style = style try: retval = func(*args, **kwargs) finally: - ansi.allow_style = old + rich_utils.allow_style = old return retval return cmd_wrapper @@ -222,31 +223,31 @@ def test_set_no_settables(base_app) -> None: @pytest.mark.parametrize( ('new_val', 'is_valid', 'expected'), [ - (ansi.AllowStyle.NEVER, True, ansi.AllowStyle.NEVER), - ('neVeR', True, ansi.AllowStyle.NEVER), - (ansi.AllowStyle.TERMINAL, True, ansi.AllowStyle.TERMINAL), - ('TeRMInal', True, ansi.AllowStyle.TERMINAL), - (ansi.AllowStyle.ALWAYS, True, ansi.AllowStyle.ALWAYS), - ('AlWaYs', True, ansi.AllowStyle.ALWAYS), - ('invalid', False, ansi.AllowStyle.TERMINAL), + (rich_utils.AllowStyle.NEVER, True, rich_utils.AllowStyle.NEVER), + ('neVeR', True, rich_utils.AllowStyle.NEVER), + (rich_utils.AllowStyle.TERMINAL, True, rich_utils.AllowStyle.TERMINAL), + ('TeRMInal', True, rich_utils.AllowStyle.TERMINAL), + (rich_utils.AllowStyle.ALWAYS, True, rich_utils.AllowStyle.ALWAYS), + ('AlWaYs', True, rich_utils.AllowStyle.ALWAYS), + ('invalid', False, rich_utils.AllowStyle.TERMINAL), ], ) def test_set_allow_style(base_app, new_val, is_valid, expected) -> None: # Initialize allow_style for this test - ansi.allow_style = ansi.AllowStyle.TERMINAL + rich_utils.allow_style = rich_utils.AllowStyle.TERMINAL # Use the set command to alter it out, err = run_cmd(base_app, f'set allow_style {new_val}') assert base_app.last_result is is_valid # Verify the results - assert ansi.allow_style == expected + assert rich_utils.allow_style == expected if is_valid: assert not err assert out # Reset allow_style to its default since it's an application-wide setting that can affect other unit tests - ansi.allow_style = ansi.AllowStyle.TERMINAL + rich_utils.allow_style = rich_utils.AllowStyle.TERMINAL def test_set_with_choices(base_app) -> None: @@ -1238,7 +1239,8 @@ def test_help_multiline_docstring(help_app) -> None: def test_help_verbose_uses_parser_description(help_app: HelpApp) -> None: out, err = run_cmd(help_app, 'help --verbose') - verify_help_text(help_app, out, verbose_strings=[help_app.parser_cmd_parser.description]) + expected_verbose = utils.strip_doc_annotations(help_app.do_parser_cmd.__doc__) + verify_help_text(help_app, out, verbose_strings=[expected_verbose]) class HelpCategoriesApp(cmd2.Cmd): @@ -1554,7 +1556,7 @@ def test_help_with_no_docstring(capsys) -> None: out == """Usage: greet [-h] [-s] -optional arguments: +Optional Arguments: -h, --help show this help message and exit -s, --shout N00B EMULATION MODE @@ -1980,7 +1982,7 @@ def test_ppretty_dict(outsim_app) -> None: assert out == expected.lstrip() -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_poutput_ansi_always(outsim_app) -> None: msg = 'Hello World' colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN) @@ -1991,7 +1993,7 @@ def test_poutput_ansi_always(outsim_app) -> None: assert out == expected -@with_ansi_style(ansi.AllowStyle.NEVER) +@with_ansi_style(rich_utils.AllowStyle.NEVER) def test_poutput_ansi_never(outsim_app) -> None: msg = 'Hello World' colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN) @@ -2174,7 +2176,7 @@ def test_multiple_aliases(base_app) -> None: verify_help_text(base_app, out) -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_perror_style(base_app, capsys) -> None: msg = 'testing...' end = '\n' @@ -2183,7 +2185,7 @@ def test_perror_style(base_app, capsys) -> None: assert err == ansi.style_error(msg) + end -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_perror_no_style(base_app, capsys) -> None: msg = 'testing...' end = '\n' @@ -2192,7 +2194,7 @@ def test_perror_no_style(base_app, capsys) -> None: assert err == msg + end -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_pexcept_style(base_app, capsys) -> None: msg = Exception('testing...') @@ -2201,7 +2203,7 @@ def test_pexcept_style(base_app, capsys) -> None: assert err.startswith(ansi.style_error("EXCEPTION of type 'Exception' occurred with message: testing...")) -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_pexcept_no_style(base_app, capsys) -> None: msg = Exception('testing...') @@ -2210,7 +2212,7 @@ def test_pexcept_no_style(base_app, capsys) -> None: assert err.startswith("EXCEPTION of type 'Exception' occurred with message: testing...") -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_pexcept_not_exception(base_app, capsys) -> None: # Pass in a msg that is not an Exception object msg = False @@ -2228,7 +2230,7 @@ def test_ppaged(outsim_app) -> None: assert out == msg + end -@with_ansi_style(ansi.AllowStyle.TERMINAL) +@with_ansi_style(rich_utils.AllowStyle.TERMINAL) def test_ppaged_strips_ansi_when_redirecting(outsim_app) -> None: msg = 'testing...' end = '\n' @@ -2238,7 +2240,7 @@ def test_ppaged_strips_ansi_when_redirecting(outsim_app) -> None: assert out == msg + end -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app) -> None: msg = 'testing...' end = '\n' @@ -2432,7 +2434,7 @@ def do_echo_error(self, args) -> None: self.perror(args) -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_ansi_pouterr_always_tty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=True) @@ -2455,7 +2457,7 @@ def test_ansi_pouterr_always_tty(mocker, capsys) -> None: assert 'oopsie' in err -@with_ansi_style(ansi.AllowStyle.ALWAYS) +@with_ansi_style(rich_utils.AllowStyle.ALWAYS) def test_ansi_pouterr_always_notty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=False) @@ -2478,7 +2480,7 @@ def test_ansi_pouterr_always_notty(mocker, capsys) -> None: assert 'oopsie' in err -@with_ansi_style(ansi.AllowStyle.TERMINAL) +@with_ansi_style(rich_utils.AllowStyle.TERMINAL) def test_ansi_terminal_tty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=True) @@ -2500,7 +2502,7 @@ def test_ansi_terminal_tty(mocker, capsys) -> None: assert 'oopsie' in err -@with_ansi_style(ansi.AllowStyle.TERMINAL) +@with_ansi_style(rich_utils.AllowStyle.TERMINAL) def test_ansi_terminal_notty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=False) @@ -2515,7 +2517,7 @@ def test_ansi_terminal_notty(mocker, capsys) -> None: assert out == err == 'oopsie\n' -@with_ansi_style(ansi.AllowStyle.NEVER) +@with_ansi_style(rich_utils.AllowStyle.NEVER) def test_ansi_never_tty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=True) @@ -2530,7 +2532,7 @@ def test_ansi_never_tty(mocker, capsys) -> None: assert out == err == 'oopsie\n' -@with_ansi_style(ansi.AllowStyle.NEVER) +@with_ansi_style(rich_utils.AllowStyle.NEVER) def test_ansi_never_notty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=False) diff --git a/tests/test_completion.py b/tests/test_completion.py index 2361c2227..4d9ad79d5 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -258,7 +258,7 @@ def test_set_allow_style_completion(cmd2_app) -> None: endidx = len(line) begidx = endidx - len(text) - expected = [val.name.lower() for val in cmd2.ansi.AllowStyle] + expected = [val.name.lower() for val in cmd2.rich_utils.AllowStyle] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt index f1c68d813..da5363831 100644 --- a/tests/transcripts/from_cmdloop.txt +++ b/tests/transcripts/from_cmdloop.txt @@ -6,7 +6,7 @@ Usage: speak [-h] [-p] [-s] [-r REPEAT]/ */ Repeats what you tell me to./ */ -optional arguments:/ */ +Optional Arguments:/ */ -h, --help show this help message and exit/ */ -p, --piglatin atinLay/ */ -s, --shout N00B EMULATION MODE/ */ diff --git a/tests_isolated/test_commandset/test_argparse_subcommands.py b/tests_isolated/test_commandset/test_argparse_subcommands.py index 5f4645d57..ee0b08e70 100644 --- a/tests_isolated/test_commandset/test_argparse_subcommands.py +++ b/tests_isolated/test_commandset/test_argparse_subcommands.py @@ -93,39 +93,39 @@ def test_subcommand_help(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'help base foo') assert out[0].startswith('Usage: base foo') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' # bar has aliases (usage should never show alias name) out, err = run_cmd(subcommand_app, 'help base bar') assert out[0].startswith('Usage: base bar') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base bar_1') assert out[0].startswith('Usage: base bar') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base bar_2') assert out[0].startswith('Usage: base bar') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' # helpless has aliases and no help text (usage should never show alias name) out, err = run_cmd(subcommand_app, 'help base helpless') assert out[0].startswith('Usage: base helpless') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base helpless_1') assert out[0].startswith('Usage: base helpless') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' out, err = run_cmd(subcommand_app, 'help base helpless_2') assert out[0].startswith('Usage: base helpless') assert out[1] == '' - assert out[2] == 'positional arguments:' + assert out[2] == 'Positional Arguments:' def test_subcommand_invalid_help(subcommand_app) -> None: