From 41a49ff3789c9dec6aec52c40d3bf81c8a4ba6ca Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 24 Oct 2024 23:28:03 -0400 Subject: [PATCH 01/12] Removed macros. --- CHANGELOG.md | 4 + README.md | 4 +- cmd2/cmd2.py | 394 +----------------- cmd2/parsing.py | 58 +-- docs/api/cmd.rst | 2 +- docs/api/parsing.rst | 2 +- docs/examples/first_app.rst | 6 +- docs/features/builtin_commands.rst | 12 +- docs/features/commands.rst | 2 +- docs/features/help.rst | 88 ++-- docs/features/history.rst | 14 +- docs/features/index.rst | 2 +- docs/features/initialization.rst | 1 - docs/features/os.rst | 6 +- ...iases_macros.rst => shortcuts_aliases.rst} | 48 +-- docs/migrating/incompatibilities.rst | 2 +- docs/migrating/why.rst | 4 +- examples/help_categories.py | 3 + tests/conftest.py | 3 +- tests/test_cmd2.py | 255 +----------- tests/test_completion.py | 21 - tests/test_parsing.py | 108 ----- tests_isolated/test_commandset/conftest.py | 3 +- .../test_commandset/test_commandset.py | 8 +- 24 files changed, 107 insertions(+), 943 deletions(-) rename docs/features/{shortcuts_aliases_macros.rst => shortcuts_aliases.rst} (57%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 417d9b427..291397a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 (TBD) +* Breaking Change + * Removed macros + ## 2.5.0 (October 23, 2024) * Breaking Change * `cmd2` 2.5 supports Python 3.8+ (removed support for Python 3.6 and 3.7) diff --git a/README.md b/README.md index ac65e91c0..5ccd594a4 100755 --- a/README.md +++ b/README.md @@ -62,9 +62,9 @@ Deep extensive tab completion and help text generation based on the argparse lib -cmd2 creates the second pillar of 'ease of transition to automation' through alias/macro creation, command line argument parsing and execution of cmd2 scripting. +cmd2 creates the second pillar of 'ease of transition to automation' through alias creation, command line argument parsing and execution of cmd2 scripting. -- Flexible alias and macro creation for quick abstraction of commands. +- Flexible alias creation for quick abstraction of commands. - Text file scripting of your application with `run_script` (`@`) and `_relative_run_script` (`@@`) - Powerful and flexible built-in Python scripting of your application using the `run_pyscript` command - Transcripts for use with built-in regression can be automatically generated from `history -t` or `run_script -t` diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index dbd993495..b53ae7a20 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -37,7 +37,6 @@ import inspect import os import pydoc -import re import sys import tempfile import threading @@ -122,8 +121,6 @@ single_line_format, ) from .parsing import ( - Macro, - MacroArg, Statement, StatementParser, shlex_split, @@ -363,9 +360,6 @@ def __init__( # Commands to exclude from the history command self.exclude_from_history = ['eof', 'history'] - # Dictionary of macro names and their values - self.macros: Dict[str, Macro] = dict() - # Keeps track of typed command history in the Python shell self._py_history: List[str] = [] @@ -411,7 +405,7 @@ def __init__( self.help_error = "No help on {}" # The error that prints when a non-existent command is run - self.default_error = "{} is not a recognized command, alias, or macro." + self.default_error = "{} is not a recognized command or alias." # If non-empty, this string will be displayed if a broken pipe error occurs self.broken_pipe_warning = '' @@ -482,7 +476,7 @@ def __init__( # If natural sorting is preferred, then set this to NATURAL_SORT_KEY. # cmd2 uses this key for sorting: # command and category names - # alias, macro, settable, and shortcut names + # alias, settable, and shortcut names # tab completion results when self.matches_sorted is False self.default_sort_key = Cmd.ALPHABETICAL_SORT_KEY @@ -749,11 +743,6 @@ def _install_command_function(self, command: str, command_wrapper: Callable[..., self.pwarning(f"Deleting alias '{command}' because it shares its name with a new command") del self.aliases[command] - # Check if command shares a name with a macro - if command in self.macros: - self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command") - del self.macros[command] - self._register_command_parser(command, command_wrapper) setattr(self, cmd_func_name, command_wrapper) @@ -2077,12 +2066,8 @@ def _perform_completion( # Determine the completer function to use for the command's argument if custom_settings is None: - # Check if a macro was entered - if command in self.macros: - completer_func = self.path_complete - # Check if a command was entered - elif command in self.get_all_commands(): + if command in self.get_all_commands(): # Get the completer function for this command func_attr = getattr(self, constants.COMPLETER_FUNC_PREFIX + command, None) @@ -2108,7 +2093,7 @@ def _perform_completion( else: completer_func = self.completedefault # type: ignore[assignment] - # Not a recognized macro or command + # Not a recognized command else: # Check if this command should be run as a shell command if self.default_to_shell and command in utils.get_exes_in_path(command): @@ -2271,8 +2256,8 @@ def complete( # type: ignore[override] parser.add_argument( 'command', metavar="COMMAND", - help="command, alias, or macro name", - choices=self._get_commands_aliases_and_macros_for_completion(), + help="command or alias name", + choices=self._get_commands_and_aliases_for_completion(), ) custom_settings = utils.CustomCompletionSettings(parser) @@ -2360,19 +2345,6 @@ def _get_alias_completion_items(self) -> List[CompletionItem]: return results - # Table displayed when tab completing macros - _macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None) - - def _get_macro_completion_items(self) -> List[CompletionItem]: - """Return list of macro names and values as CompletionItems""" - results: List[CompletionItem] = [] - - for cur_key in self.macros: - row_data = [self.macros[cur_key].value] - results.append(CompletionItem(cur_key, self._macro_completion_table.generate_data_row(row_data))) - - return results - # Table displayed when tab completing Settables _settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None) @@ -2386,12 +2358,11 @@ def _get_settable_completion_items(self) -> List[CompletionItem]: return results - def _get_commands_aliases_and_macros_for_completion(self) -> List[str]: - """Return a list of visible commands, aliases, and macros for tab completion""" + def _get_commands_and_aliases_for_completion(self) -> List[str]: + """Return a list of visible commands and aliases for tab completion""" visible_commands = set(self.get_visible_commands()) alias_names = set(self.aliases) - macro_names = set(self.macros) - return list(visible_commands | alias_names | macro_names) + return list(visible_commands | alias_names) def get_help_topics(self) -> List[str]: """Return a list of help topics""" @@ -2540,7 +2511,7 @@ def onecmd_plus_hooks( try: # Convert the line into a Statement - statement = self._input_line_to_statement(line, orig_rl_history_length=orig_rl_history_length) + statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length) # call the postparsing hooks postparsing_data = plugin.PostparsingData(False, statement) @@ -2785,101 +2756,6 @@ def combine_rl_history(statement: Statement) -> None: return statement - def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement: - """ - Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved - - :param line: the line being parsed - :param orig_rl_history_length: Optional length of the readline history before the current command was typed. - This is used to assist in combining multiline readline history entries and is only - populated by cmd2. Defaults to None. - :return: parsed command line as a Statement - :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation) - :raises: EmptyStatement when the resulting Statement is blank - """ - used_macros = [] - orig_line = None - - # Continue until all macros are resolved - while True: - # Make sure all input has been read and convert it to a Statement - statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length) - - # If this is the first loop iteration, save the original line and stop - # combining multiline history entries in the remaining iterations. - if orig_line is None: - orig_line = statement.raw - orig_rl_history_length = None - - # Check if this command matches a macro and wasn't already processed to avoid an infinite loop - if statement.command in self.macros.keys() and statement.command not in used_macros: - used_macros.append(statement.command) - resolve_result = self._resolve_macro(statement) - if resolve_result is None: - raise EmptyStatement - line = resolve_result - else: - break - - # This will be true when a macro was used - if orig_line != statement.raw: - # Build a Statement that contains the resolved macro line - # but the originally typed line for its raw member. - statement = Statement( - statement.args, - raw=orig_line, - command=statement.command, - arg_list=statement.arg_list, - multiline_command=statement.multiline_command, - terminator=statement.terminator, - suffix=statement.suffix, - pipe_to=statement.pipe_to, - output=statement.output, - output_to=statement.output_to, - ) - return statement - - def _resolve_macro(self, statement: Statement) -> Optional[str]: - """ - Resolve a macro and return the resulting string - - :param statement: the parsed statement from the command line - :return: the resolved macro or None on error - """ - if statement.command not in self.macros.keys(): - raise KeyError(f"{statement.command} is not a macro") - - macro = self.macros[statement.command] - - # Make sure enough arguments were passed in - if len(statement.arg_list) < macro.minimum_arg_count: - plural = '' if macro.minimum_arg_count == 1 else 's' - self.perror(f"The macro '{statement.command}' expects at least {macro.minimum_arg_count} argument{plural}") - return None - - # Resolve the arguments in reverse and read their values from statement.argv since those - # are unquoted. Macro args should have been quoted when the macro was created. - resolved = macro.value - reverse_arg_list = sorted(macro.arg_list, key=lambda ma: ma.start_index, reverse=True) - - for macro_arg in reverse_arg_list: - if macro_arg.is_escaped: - to_replace = '{{' + macro_arg.number_str + '}}' - replacement = '{' + macro_arg.number_str + '}' - else: - to_replace = '{' + macro_arg.number_str + '}' - replacement = statement.argv[int(macro_arg.number_str)] - - parts = resolved.rsplit(to_replace, maxsplit=1) - resolved = parts[0] + replacement + parts[1] - - # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved - for stmt_arg in statement.arg_list[macro.minimum_arg_count :]: - resolved += ' ' + stmt_arg - - # Restore any terminator, suffix, redirection, etc. - return resolved + statement.post_command - def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: """Set up a command's output redirection for >, >>, and |. @@ -3063,7 +2939,7 @@ def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = Tru """ # For backwards compatibility with cmd, allow a str to be passed in if not isinstance(statement, Statement): - statement = self._input_line_to_statement(statement) + statement = self._complete_statement(statement) func = self.cmd_func(statement.command) if func: @@ -3395,8 +3271,7 @@ def _cmdloop(self) -> None: # Top-level parser for alias alias_description = "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string." - alias_epilog = "See also:\n" " macro" - alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog) + alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description) alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') alias_subparsers.required = True @@ -3430,7 +3305,7 @@ def do_alias(self, args: argparse.Namespace) -> None: ) alias_create_parser.add_argument('name', help='name of this alias') alias_create_parser.add_argument( - 'command', help='what the alias resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion + 'command', help='what the alias resolves to', choices_provider=_get_commands_and_aliases_for_completion ) alias_create_parser.add_argument( 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete @@ -3451,10 +3326,6 @@ def _alias_create(self, args: argparse.Namespace) -> None: self.perror("Alias cannot have the same name as a command") return - if args.name in self.macros: - self.perror("Alias cannot have the same name as a macro") - return - # Unquote redirection and terminator tokens tokens_to_unquote = constants.REDIRECTION_TOKENS tokens_to_unquote.extend(self.statement_parser.terminators) @@ -3558,243 +3429,6 @@ def _alias_list(self, args: argparse.Namespace) -> None: for name in not_found: self.perror(f"Alias '{name}' not found") - ############################################################# - # Parsers and functions for macro command and subcommands - ############################################################# - - # Top-level parser for macro - macro_description = "Manage macros\n" "\n" "A macro is similar to an alias, but it can contain argument placeholders." - macro_epilog = "See also:\n" " alias" - macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog) - macro_subparsers = macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') - macro_subparsers.required = True - - # Preserve quotes since we are passing strings to other commands - @with_argparser(macro_parser, preserve_quotes=True) - def do_macro(self, args: argparse.Namespace) -> None: - """Manage macros""" - # Call handler for whatever subcommand was selected - handler = args.cmd2_handler.get() - handler(args) - - # macro -> create - macro_create_help = "create or overwrite a macro" - macro_create_description = "Create or overwrite a macro" - - macro_create_epilog = ( - "A macro is similar to an alias, but it can contain argument placeholders.\n" - "Arguments are expressed when creating a macro using {#} notation where {1}\n" - "means the first argument.\n" - "\n" - "The following creates a macro called my_macro that expects two arguments:\n" - "\n" - " macro create my_macro make_dinner --meat {1} --veggie {2}\n" - "\n" - "When the macro is called, the provided arguments are resolved and the\n" - "assembled command is run. For example:\n" - "\n" - " my_macro beef broccoli ---> make_dinner --meat beef --veggie broccoli\n" - "\n" - "Notes:\n" - " To use the literal string {1} in your command, escape it this way: {{1}}.\n" - "\n" - " Extra arguments passed to a macro are appended to resolved command.\n" - "\n" - " An argument number can be repeated in a macro. In the following example the\n" - " first argument will populate both {1} instances.\n" - "\n" - " macro create ft file_taxes -p {1} -q {2} -r {1}\n" - "\n" - " To quote an argument in the resolved command, quote it during creation.\n" - "\n" - " macro create backup !cp \"{1}\" \"{1}.orig\"\n" - "\n" - " If you want to use redirection, pipes, or terminators in the value of the\n" - " macro, then quote them.\n" - "\n" - " macro create show_results print_results -type {1} \"|\" less\n" - "\n" - " Because macros do not resolve until after hitting Enter, tab completion\n" - " will only complete paths while typing a macro." - ) - - macro_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description=macro_create_description, epilog=macro_create_epilog - ) - macro_create_parser.add_argument('name', help='name of this macro') - macro_create_parser.add_argument( - 'command', help='what the macro resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion - ) - macro_create_parser.add_argument( - 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete - ) - - @as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help) - def _macro_create(self, args: argparse.Namespace) -> None: - """Create or overwrite a macro""" - self.last_result = False - - # Validate the macro name - valid, errmsg = self.statement_parser.is_valid_command(args.name) - if not valid: - self.perror(f"Invalid macro name: {errmsg}") - return - - if args.name in self.get_all_commands(): - self.perror("Macro cannot have the same name as a command") - return - - if args.name in self.aliases: - self.perror("Macro cannot have the same name as an alias") - return - - # Unquote redirection and terminator tokens - tokens_to_unquote = constants.REDIRECTION_TOKENS - tokens_to_unquote.extend(self.statement_parser.terminators) - utils.unquote_specific_tokens(args.command_args, tokens_to_unquote) - - # Build the macro value string - value = args.command - if args.command_args: - value += ' ' + ' '.join(args.command_args) - - # Find all normal arguments - arg_list = [] - normal_matches = re.finditer(MacroArg.macro_normal_arg_pattern, value) - max_arg_num = 0 - arg_nums = set() - - while True: - try: - cur_match = normal_matches.__next__() - - # Get the number string between the braces - cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0] - cur_num = int(cur_num_str) - if cur_num < 1: - self.perror("Argument numbers must be greater than 0") - return - - arg_nums.add(cur_num) - if cur_num > max_arg_num: - max_arg_num = cur_num - - arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False)) - - except StopIteration: - break - - # Make sure the argument numbers are continuous - if len(arg_nums) != max_arg_num: - self.perror(f"Not all numbers between 1 and {max_arg_num} are present in the argument placeholders") - return - - # Find all escaped arguments - escaped_matches = re.finditer(MacroArg.macro_escaped_arg_pattern, value) - - while True: - try: - cur_match = escaped_matches.__next__() - - # Get the number string between the braces - cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0] - - arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True)) - except StopIteration: - break - - # Set the macro - result = "overwritten" if args.name in self.macros else "created" - self.poutput(f"Macro '{args.name}' {result}") - - self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list) - self.last_result = True - - # macro -> delete - macro_delete_help = "delete macros" - macro_delete_description = "Delete specified macros or all macros if --all is used" - macro_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_delete_description) - macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros") - macro_delete_parser.add_argument( - 'names', - nargs=argparse.ZERO_OR_MORE, - help='macro(s) to delete', - choices_provider=_get_macro_completion_items, - descriptive_header=_macro_completion_table.generate_header(), - ) - - @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help) - def _macro_delete(self, args: argparse.Namespace) -> None: - """Delete macros""" - self.last_result = True - - if args.all: - self.macros.clear() - self.poutput("All macros deleted") - elif not args.names: - self.perror("Either --all or macro name(s) must be specified") - self.last_result = False - else: - for cur_name in utils.remove_duplicates(args.names): - if cur_name in self.macros: - del self.macros[cur_name] - self.poutput(f"Macro '{cur_name}' deleted") - else: - self.perror(f"Macro '{cur_name}' does not exist") - - # macro -> list - macro_list_help = "list macros" - macro_list_description = ( - "List specified macros in a reusable form that can be saved to a startup script\n" - "to preserve macros across sessions\n" - "\n" - "Without arguments, all macros will be listed." - ) - - macro_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_list_description) - macro_list_parser.add_argument( - 'names', - nargs=argparse.ZERO_OR_MORE, - help='macro(s) to list', - choices_provider=_get_macro_completion_items, - descriptive_header=_macro_completion_table.generate_header(), - ) - - @as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help) - def _macro_list(self, args: argparse.Namespace) -> None: - """List some or all macros as 'macro create' commands""" - self.last_result = {} # Dict[macro_name, macro_value] - - tokens_to_quote = constants.REDIRECTION_TOKENS - tokens_to_quote.extend(self.statement_parser.terminators) - - if args.names: - to_list = utils.remove_duplicates(args.names) - else: - to_list = sorted(self.macros, key=self.default_sort_key) - - not_found: List[str] = [] - for name in to_list: - if name not in self.macros: - not_found.append(name) - continue - - # Quote redirection and terminator tokens for the 'macro create' command - tokens = shlex_split(self.macros[name].value) - command = tokens[0] - command_args = tokens[1:] - utils.quote_specific_tokens(command_args, tokens_to_quote) - - val = command - if command_args: - val += ' ' + ' '.join(command_args) - - self.poutput(f"macro create {name} {val}") - self.last_result[name] = val - - for name in not_found: - self.perror(f"Macro '{name}' not found") - def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: """Completes the command argument of help""" @@ -4712,7 +4346,7 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover '-x', '--expanded', action='store_true', - help='output fully parsed commands with any aliases and\n' 'macros expanded, instead of typed commands', + help='output fully parsed commands with aliases and shortcuts expanded', ) history_format_group.add_argument( '-v', diff --git a/cmd2/parsing.py b/cmd2/parsing.py index e84f7c4fc..7ef1cc3de 100755 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -38,56 +38,6 @@ def shlex_split(str_to_split: str) -> List[str]: return shlex.split(str_to_split, comments=False, posix=False) -@dataclass(frozen=True) -class MacroArg: - """ - Information used to replace or unescape arguments in a macro value when the macro is resolved - Normal argument syntax: {5} - Escaped argument syntax: {{5}} - """ - - # The starting index of this argument in the macro value - start_index: int - - # The number string that appears between the braces - # This is a string instead of an int because we support unicode digits and must be able - # to reproduce this string later - number_str: str - - # Tells if this argument is escaped and therefore needs to be unescaped - is_escaped: bool - - # Pattern used to find normal argument - # Digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side - # Match strings like: {5}, {{{{{4}, {2}}}}} - macro_normal_arg_pattern = re.compile(r'(? str: def argv(self) -> List[str]: """a list of arguments a-la ``sys.argv``. - The first element of the list is the command after shortcut and macro - expansion. Subsequent elements of the list contain any additional - arguments, with quotes removed, just like bash would. This is very - useful if you are going to use ``argparse.parse_args()``. + The first element of the list is the command after shortcut expansion. + Subsequent elements of the list contain any additional arguments, + with quotes removed, just like bash would. This is very useful if + you are going to use ``argparse.parse_args()``. If you want to strip quotes from the input, you can use ``argv[1:]``. """ diff --git a/docs/api/cmd.rst b/docs/api/cmd.rst index 4fbb8ccd5..fef2c78c7 100644 --- a/docs/api/cmd.rst +++ b/docs/api/cmd.rst @@ -9,7 +9,7 @@ cmd2.Cmd .. attribute:: default_error The error message displayed when a non-existent command is run. - Default: ``{} is not a recognized command, alias, or macro.`` + Default: ``{} is not a recognized command or alias.`` .. attribute:: help_error diff --git a/docs/api/parsing.rst b/docs/api/parsing.rst index fa726700b..490e56559 100644 --- a/docs/api/parsing.rst +++ b/docs/api/parsing.rst @@ -15,7 +15,7 @@ Classes for parsing and storing user input. .. attribute:: command - The name of the command after shortcuts and macros have been expanded + The name of the command after shortcuts have been expanded .. attribute:: args diff --git a/docs/examples/first_app.rst b/docs/examples/first_app.rst index d90f96d86..b0164f81d 100644 --- a/docs/examples/first_app.rst +++ b/docs/examples/first_app.rst @@ -11,7 +11,7 @@ features of ``cmd2``: * :ref:`features/argument_processing:Argument Processing` * :ref:`features/generating_output:Generating Output` * :ref:`features/help:Help` -* :ref:`features/shortcuts_aliases_macros:Shortcuts` +* :ref:`features/shortcuts_aliases:Shortcuts` * :ref:`features/multiline_commands:Multiline Commands` * :ref:`features/history:History` @@ -178,8 +178,8 @@ Shortcuts --------- ``cmd2`` has several capabilities to simplify repetitive user input: -:ref:`Shortcuts, Aliases, and Macros -`. Let's add +:ref:`Shortcuts and Aliases +`. Let's add a shortcut to our application. Shortcuts are character strings that can be used instead of a command name. For example, ``cmd2`` has support for a shortcut ``!`` which runs the ``shell`` command. So instead of typing this: diff --git a/docs/features/builtin_commands.rst b/docs/features/builtin_commands.rst index 4925fc3d7..93d98a7eb 100644 --- a/docs/features/builtin_commands.rst +++ b/docs/features/builtin_commands.rst @@ -13,7 +13,7 @@ alias ~~~~~ This command manages aliases via subcommands ``create``, ``delete``, and -``list``. See :ref:`features/shortcuts_aliases_macros:Aliases` for more +``list``. See :ref:`features/shortcuts_aliases:Aliases` for more information. edit @@ -50,14 +50,6 @@ ipy This optional opt-in command enters an interactive IPython shell. See :ref:`features/embedded_python_shells:IPython (optional)` for more information. -macro -~~~~~ - -This command manages macros via subcommands ``create``, ``delete``, and -``list``. A macro is similar to an alias, but it can contain argument -placeholders. See :ref:`features/shortcuts_aliases_macros:Macros` for more -information. - py ~~ @@ -143,7 +135,7 @@ shortcuts ~~~~~~~~~ This command lists available shortcuts. See -:ref:`features/shortcuts_aliases_macros:Shortcuts` for more information. +:ref:`features/shortcuts_aliases:Shortcuts` for more information. Remove Builtin Commands diff --git a/docs/features/commands.rst b/docs/features/commands.rst index 5dd5a163b..1b3da218a 100644 --- a/docs/features/commands.rst +++ b/docs/features/commands.rst @@ -67,7 +67,7 @@ the cmd_ module. This parsing handles: - quoted arguments - output redirection and piping - multi-line commands -- shortcut, macro, and alias expansion +- shortcut and alias expansion In addition to parsing all of these elements from the user input, ``cmd2`` also has code to make all of these items work; it's almost transparent to you and to diff --git a/docs/features/help.rst b/docs/features/help.rst index b98e4164d..759a5f150 100644 --- a/docs/features/help.rst +++ b/docs/features/help.rst @@ -19,8 +19,8 @@ of the commands available: Documented commands (use 'help -v' for verbose/'help ' for details): =========================================================================== - alias help ipy py run_pyscript set shortcuts - edit history macro quit run_script shell + alias help ipy quit run_script shell + edit history py run_pyscript set shortcuts The ``help`` command can also be used to provide detailed help for a specific command: @@ -63,8 +63,8 @@ By default, the ``help`` command displays:: Documented commands (use 'help -v' for verbose/'help ' for details): =========================================================================== - alias help ipy py run_pyscript set shortcuts - edit history macro quit run_script shell + alias help ipy quit run_script shell + edit history py run_pyscript set shortcuts If you have a large number of commands, you can optionally group your commands into categories. Here's the output from the example ``help_categories.py``:: @@ -90,8 +90,8 @@ into categories. Here's the output from the example ``help_categories.py``:: Other ===== - alias edit history py run_pyscript set shortcuts - config help macro quit run_script shell version + alias edit history run_pyscript set shortcuts + config help quit run_script shell version There are 2 methods of specifying command categories, using the ``@with_category`` decorator or with the ``categorize()`` function. Once a @@ -142,51 +142,49 @@ The ``help`` command also has a verbose option (``help -v`` or ``help Documented commands (use 'help -v' for verbose/'help ' for details): Application Management - ================================================================================ - deploy Deploy command - expire Expire command - findleakers Find Leakers command - list List command - redeploy Redeploy command - restart usage: restart [-h] {now,later,sometime,whenever} - sessions Sessions command - start Start command - stop Stop command - undeploy Undeploy command + ====================================================================================================== + deploy Deploy command + expire Expire command + findleakers Find Leakers command + list List command + redeploy Redeploy command + restart Restart command + sessions Sessions command + start Start command + stop Stop command + undeploy Undeploy command Connecting - ================================================================================ - connect Connect command - which Which command + ====================================================================================================== + connect Connect command + which Which command Server Information - ================================================================================ - resources Resources command - serverinfo Server Info command - sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains - multiple lines of help information for the user. Each line of help in a - contiguous set of lines will be printed and aligned in the verbose output - provided with 'help --verbose' - status Status command - thread_dump Thread Dump command - vminfo VM Info command + ====================================================================================================== + resources Resources command + serverinfo Server Info command + sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains + multiple lines of help information for the user. Each line of help in a + contiguous set of lines will be printed and aligned in the verbose output + provided with 'help --verbose' + status Status command + thread_dump Thread Dump command + vminfo VM Info command Other - ================================================================================ - 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 - macro Manage macros - py Invoke Python command or shell - quit Exits this application - run_pyscript Runs a python script file inside the console - run_script Runs commands in script file that is encoded as either ASCII or UTF-8 text - 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 - version Version command + ====================================================================================================== + 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. + 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 + version Version command When called with the ``-v`` flag for verbose help, the one-line description for each command is provided by the first line of the docstring for that command's diff --git a/docs/features/history.rst b/docs/features/history.rst index 056e02a0b..e421588aa 100644 --- a/docs/features/history.rst +++ b/docs/features/history.rst @@ -232,9 +232,9 @@ clipboard:: (Cmd) history -s 1:3 -``cmd2`` supports both aliases and macros, which allow you to substitute a -short, more convenient input string with a longer replacement string. Say we -create an alias like this, and then use it:: +``cmd2`` supports aliases which allow you to substitute a short, more +convenient input string with a longer replacement string. Say we create +an alias like this, and then use it:: (Cmd) alias create ls shell ls -aF Alias 'ls' created @@ -248,7 +248,7 @@ By default, the ``history`` command shows exactly what we typed:: 2 ls -d h* There are two ways to modify that display so you can see what aliases and -macros were expanded to. The first is to use ``-x`` or ``--expanded``. These +shortcuts were expanded to. The first is to use ``-x`` or ``--expanded``. These options show the expanded command instead of the entered command:: (Cmd) history -x @@ -264,6 +264,6 @@ If you want to see both the entered command and the expanded command, use the 2x shell ls -aF -d h* If the entered command had no expansion, it is displayed as usual. However, if -there is some change as the result of expanding macros and aliases, then the -entered command is displayed with the number, and the expanded command is -displayed with the number followed by an ``x``. +there is some change as the result of expanding aliases, then the entered +command is displayed with the number, and the expanded command is displayed +with the number followed by an ``x``. diff --git a/docs/features/index.rst b/docs/features/index.rst index 48590b6ad..8e6fc595b 100644 --- a/docs/features/index.rst +++ b/docs/features/index.rst @@ -26,7 +26,7 @@ Features redirection scripting settings - shortcuts_aliases_macros + shortcuts_aliases startup_commands table_creation transcripts diff --git a/docs/features/initialization.rst b/docs/features/initialization.rst index 3ee96cf9e..b06f65266 100644 --- a/docs/features/initialization.rst +++ b/docs/features/initialization.rst @@ -141,7 +141,6 @@ override: of results in a Python script or interactive console. Built-in commands don't make use of this. It is purely there for user-defined commands and convenience. -- **macros**: dictionary of macro names and their values - **max_completion_items**: max number of CompletionItems to display during tab completion (Default: 50) - **pager**: sets the pager command used by the ``Cmd.ppaged()`` method for diff --git a/docs/features/os.rst b/docs/features/os.rst index 77bc6a668..722e761ad 100644 --- a/docs/features/os.rst +++ b/docs/features/os.rst @@ -14,7 +14,7 @@ operating system shell:: (Cmd) shell ls -al -If you use the default :ref:`features/shortcuts_aliases_macros:Shortcuts` +If you use the default :ref:`features/shortcuts_aliases:Shortcuts` defined in ``cmd2`` you'll get a ``!`` shortcut for ``shell``, which allows you to type:: @@ -107,8 +107,8 @@ loop:: Documented commands (use 'help -v' for verbose/'help ' for details): =========================================================================== - alias help macro orate quit run_script set shortcuts - edit history mumble py run_pyscript say shell speak + alias help ipy quit run_script shell + edit history py run_pyscript set shortcuts (Cmd) diff --git a/docs/features/shortcuts_aliases_macros.rst b/docs/features/shortcuts_aliases.rst similarity index 57% rename from docs/features/shortcuts_aliases_macros.rst rename to docs/features/shortcuts_aliases.rst index 58d6d83cd..243257feb 100644 --- a/docs/features/shortcuts_aliases_macros.rst +++ b/docs/features/shortcuts_aliases.rst @@ -1,5 +1,5 @@ -Shortcuts, Aliases, and Macros -============================== +Shortcuts and Aliases +===================== Shortcuts --------- @@ -38,7 +38,7 @@ To define more shortcuts, update the dict ``App.shortcuts`` with the updating the ``shortcuts`` attribute This warning applies in general to many other attributes which are not settable at runtime. -Note: Command, alias, and macro names cannot start with a shortcut +Note: Command and alias names cannot start with a shortcut Aliases ------- @@ -74,44 +74,4 @@ Use ``alias delete`` to remove aliases For more details run: ``help alias delete`` -Note: Aliases cannot have the same name as a command or macro - -Macros ------- - -``cmd2`` provides a feature that is similar to aliases called macros. The major -difference between macros and aliases is that macros can contain argument -placeholders. Arguments are expressed when creating a macro using {#} notation -where {1} means the first argument. - -The following creates a macro called my_macro that expects two arguments: - - macro create my_macro make_dinner -meat {1} -veggie {2} - -When the macro is called, the provided arguments are resolved and the assembled -command is run. For example: - - my_macro beef broccoli ---> make_dinner -meat beef -veggie broccoli - -Similar to aliases, pipes and redirectors need to be quoted in the definition -of a macro:: - - macro create lc !cat "{1}" "|" less - -To use the literal string ``{1}`` in your command, escape it this way: -``{{1}}``. Because macros do not resolve until after hitting ````, -tab completion will only complete paths while typing a macro. - - -For more details run: ``help macro create`` - -The macro command has ``list`` and ``delete`` subcommands that function -identically to the alias subcommands of the same name. Like aliases, macros can -be created via a ``cmd2`` startup script to preserve them across application -sessions. - -For more details on listing macros run: ``help macro list`` - -For more details on deleting macros run: ``help macro delete`` - -Note: Macros cannot have the same name as a command or alias +Note: Aliases cannot have the same name as a command diff --git a/docs/migrating/incompatibilities.rst b/docs/migrating/incompatibilities.rst index ba6f2ed10..bc922c380 100644 --- a/docs/migrating/incompatibilities.rst +++ b/docs/migrating/incompatibilities.rst @@ -38,7 +38,7 @@ characters in command names while simultaneously using ``identchars`` functionality can be somewhat painful. Requiring white space to delimit arguments also ensures reliable operation of many other useful ``cmd2`` features, including :ref:`features/completion:Completion` and -:ref:`features/shortcuts_aliases_macros:Shortcuts, Aliases, and Macros`. +:ref:`features/shortcuts_aliases:Shortcuts and Aliases`. If you really need this functionality in your app, you can add it back in by writing a :ref:`Postparsing Hook `. diff --git a/docs/migrating/why.rst b/docs/migrating/why.rst index 2bfd45f14..bbfccb650 100644 --- a/docs/migrating/why.rst +++ b/docs/migrating/why.rst @@ -55,8 +55,8 @@ new features and capabilities, without you having to do anything: - Users can load script files, which contain a series of commands to be executed. -- Users can create :ref:`features/shortcuts_aliases_macros:Shortcuts, Aliases, - and Macros` to reduce the typing required for repetitive commands. +- Users can create :ref:`features/shortcuts_aliases:Shortcuts and Aliases` + to reduce the typing required for repetitive commands. - Embedded python shell allows a user to execute python code from within your ``cmd2`` app. How meta. diff --git a/examples/help_categories.py b/examples/help_categories.py index 5c349422c..8059ca90a 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -37,6 +37,9 @@ class HelpCategories(cmd2.Cmd): def __init__(self): super().__init__() + # Set the default category for uncategorized commands + self.default_category = 'Other' + def do_connect(self, _): """Connect command""" self.poutput('Connect') diff --git a/tests/conftest.py b/tests/conftest.py index 644ae7cca..0b3a01786 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -81,8 +81,7 @@ def verify_help_text( formatting: -s, --script output commands in script format, i.e. without command numbers - -x, --expanded output fully parsed commands with any aliases and - macros expanded, instead of typed commands + -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 diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 374ba10a2..efa4868b5 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1630,8 +1630,8 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): assert statement.terminator == ';' -def test_multiline_input_line_to_statement(multiline_app): - # Verify _input_line_to_statement saves the fully entered input line for multiline commands +def test_multiline_complete_statement(multiline_app): + # Verify _complete_statement saves the fully entered input line for multiline commands # Mock out the input call so we don't actually wait for a user's response # on stdin when it looks for more input @@ -1639,7 +1639,7 @@ def test_multiline_input_line_to_statement(multiline_app): builtins.input = m line = 'orate hi' - statement = multiline_app._input_line_to_statement(line) + statement = multiline_app._complete_statement(line) assert statement.raw == 'orate hi\nperson\n' assert statement == 'hi person' assert statement.command == 'orate' @@ -1998,8 +1998,7 @@ def test_poutput_ansi_never(outsim_app): assert out == expected -# These are invalid names for aliases and macros -invalid_command_name = [ +invalid_alias_names = [ '""', # Blank name constants.COMMENT_CHAR, '!no_shortcut', @@ -2025,19 +2024,6 @@ def test_get_alias_completion_items(base_app): assert cur_res.description.rstrip() == base_app.aliases[cur_res] -def test_get_macro_completion_items(base_app): - run_cmd(base_app, 'macro create foo !echo foo') - run_cmd(base_app, 'macro create bar !echo bar') - - results = base_app._get_macro_completion_items() - assert len(results) == len(base_app.macros) - - for cur_res in results: - assert cur_res in base_app.macros - # Strip trailing spaces from table output - assert cur_res.description.rstrip() == base_app.macros[cur_res].value - - def test_get_settable_completion_items(base_app): results = base_app._get_settable_completion_items() assert len(results) == len(base_app.settables) @@ -2113,7 +2099,7 @@ def test_alias_create_with_quoted_tokens(base_app): assert base_app.last_result[alias_name] == alias_command -@pytest.mark.parametrize('alias_name', invalid_command_name) +@pytest.mark.parametrize('alias_name', invalid_alias_names) def test_alias_create_invalid_name(base_app, alias_name, capsys): out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name)) assert "Invalid alias name" in err[0] @@ -2126,14 +2112,6 @@ def test_alias_create_with_command_name(base_app): assert base_app.last_result is False -def test_alias_create_with_macro_name(base_app): - macro = "my_macro" - run_cmd(base_app, 'macro create {} help'.format(macro)) - out, err = run_cmd(base_app, 'alias create {} help'.format(macro)) - assert "Alias cannot have the same name as a macro" in err[0] - assert base_app.last_result is False - - def test_alias_that_resolves_into_comment(base_app): # Create the alias out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah') @@ -2192,228 +2170,6 @@ def test_multiple_aliases(base_app): verify_help_text(base_app, out) -def test_macro_no_subcommand(base_app): - out, err = run_cmd(base_app, 'macro') - assert "Usage: macro [-h]" in err[0] - assert "Error: the following arguments are required: SUBCOMMAND" in err[1] - - -def test_macro_create(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake run_pyscript') - assert out == normalize("Macro 'fake' created") - assert base_app.last_result is True - - # Use the macro - out, err = run_cmd(base_app, 'fake') - assert "the following arguments are required: script_path" in err[1] - - # See a list of macros - out, err = run_cmd(base_app, 'macro list') - assert out == normalize('macro create fake run_pyscript') - assert len(base_app.last_result) == len(base_app.macros) - assert base_app.last_result['fake'] == "run_pyscript" - - # Look up the new macro - out, err = run_cmd(base_app, 'macro list fake') - assert out == normalize('macro create fake run_pyscript') - assert len(base_app.last_result) == 1 - assert base_app.last_result['fake'] == "run_pyscript" - - # Overwrite macro - out, err = run_cmd(base_app, 'macro create fake help') - assert out == normalize("Macro 'fake' overwritten") - assert base_app.last_result is True - - # Look up the updated macro - out, err = run_cmd(base_app, 'macro list fake') - assert out == normalize('macro create fake help') - assert len(base_app.last_result) == 1 - assert base_app.last_result['fake'] == "help" - - -def test_macro_create_with_quoted_tokens(base_app): - """Demonstrate that quotes in macro value will be preserved""" - macro_name = "fake" - macro_command = 'help ">" "out file.txt" ";"' - create_command = f"macro create {macro_name} {macro_command}" - - # Create the macro - out, err = run_cmd(base_app, create_command) - assert out == normalize("Macro 'fake' created") - - # Look up the new macro and verify all quotes are preserved - out, err = run_cmd(base_app, 'macro list fake') - assert out == normalize(create_command) - assert len(base_app.last_result) == 1 - assert base_app.last_result[macro_name] == macro_command - - -@pytest.mark.parametrize('macro_name', invalid_command_name) -def test_macro_create_invalid_name(base_app, macro_name): - out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name)) - assert "Invalid macro name" in err[0] - assert base_app.last_result is False - - -def test_macro_create_with_command_name(base_app): - out, err = run_cmd(base_app, 'macro create help stuff') - assert "Macro cannot have the same name as a command" in err[0] - assert base_app.last_result is False - - -def test_macro_create_with_alias_name(base_app): - macro = "my_macro" - run_cmd(base_app, 'alias create {} help'.format(macro)) - out, err = run_cmd(base_app, 'macro create {} help'.format(macro)) - assert "Macro cannot have the same name as an alias" in err[0] - assert base_app.last_result is False - - -def test_macro_create_with_args(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake {1} {2}') - assert out == normalize("Macro 'fake' created") - - # Run the macro - out, err = run_cmd(base_app, 'fake help -v') - verify_help_text(base_app, out) - - -def test_macro_create_with_escaped_args(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake help {{1}}') - assert out == normalize("Macro 'fake' created") - - # Run the macro - out, err = run_cmd(base_app, 'fake') - assert err[0].startswith('No help on {1}') - - -def test_macro_usage_with_missing_args(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake help {1} {2}') - assert out == normalize("Macro 'fake' created") - - # Run the macro - out, err = run_cmd(base_app, 'fake arg1') - assert "expects at least 2 arguments" in err[0] - - -def test_macro_usage_with_exta_args(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake help {1}') - assert out == normalize("Macro 'fake' created") - - # Run the macro - out, err = run_cmd(base_app, 'fake alias create') - assert "Usage: alias create" in out[0] - - -def test_macro_create_with_missing_arg_nums(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake help {1} {3}') - assert "Not all numbers between 1 and 3" in err[0] - assert base_app.last_result is False - - -def test_macro_create_with_invalid_arg_num(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake help {1} {-1} {0}') - assert "Argument numbers must be greater than 0" in err[0] - assert base_app.last_result is False - - -def test_macro_create_with_unicode_numbered_arg(base_app): - # Create the macro expecting 1 argument - out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}') - assert out == normalize("Macro 'fake' created") - - # Run the macro - out, err = run_cmd(base_app, 'fake') - assert "expects at least 1 argument" in err[0] - - -def test_macro_create_with_missing_unicode_arg_nums(base_app): - out, err = run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}') - assert "Not all numbers between 1 and 3" in err[0] - assert base_app.last_result is False - - -def test_macro_that_resolves_into_comment(base_app): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake {1} blah blah') - assert out == normalize("Macro 'fake' created") - - # Use the macro - out, err = run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR) - assert not out - assert not err - - -def test_macro_list_invalid_macro(base_app): - # Look up invalid macro - out, err = run_cmd(base_app, 'macro list invalid') - assert "Macro 'invalid' not found" in err[0] - assert base_app.last_result == {} - - -def test_macro_delete(base_app): - # Create an macro - run_cmd(base_app, 'macro create fake run_pyscript') - - # Delete the macro - out, err = run_cmd(base_app, 'macro delete fake') - assert out == normalize("Macro 'fake' deleted") - assert base_app.last_result is True - - -def test_macro_delete_all(base_app): - out, err = run_cmd(base_app, 'macro delete --all') - assert out == normalize("All macros deleted") - assert base_app.last_result is True - - -def test_macro_delete_non_existing(base_app): - out, err = run_cmd(base_app, 'macro delete fake') - assert "Macro 'fake' does not exist" in err[0] - assert base_app.last_result is True - - -def test_macro_delete_no_name(base_app): - out, err = run_cmd(base_app, 'macro delete') - assert "Either --all or macro name(s)" in err[0] - assert base_app.last_result is False - - -def test_multiple_macros(base_app): - macro1 = 'h1' - macro2 = 'h2' - run_cmd(base_app, 'macro create {} help'.format(macro1)) - run_cmd(base_app, 'macro create {} help -v'.format(macro2)) - out, err = run_cmd(base_app, macro1) - verify_help_text(base_app, out) - - out2, err2 = run_cmd(base_app, macro2) - verify_help_text(base_app, out2) - assert len(out2) > len(out) - - -def test_nonexistent_macro(base_app): - from cmd2.parsing import ( - StatementParser, - ) - - exception = None - - try: - base_app._resolve_macro(StatementParser().parse('fake')) - except KeyError as e: - exception = e - - assert exception is not None - - @with_ansi_style(ansi.AllowStyle.ALWAYS) def test_perror_style(base_app, capsys): msg = 'testing...' @@ -2567,7 +2323,6 @@ def test_get_all_commands(base_app): 'help', 'history', 'ipy', - 'macro', 'py', 'quit', 'run_pyscript', diff --git a/tests/test_completion.py b/tests/test_completion.py index 18b7c0f27..2bb08d3a5 100755 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -26,8 +26,6 @@ from .conftest import ( complete_tester, - normalize, - run_cmd, ) # List of strings used with completion functions @@ -186,25 +184,6 @@ def test_complete_exception(cmd2_app, capsys): assert "IndexError" in err -def test_complete_macro(base_app, request): - # Create the macro - out, err = run_cmd(base_app, 'macro create fake run_pyscript {1}') - assert out == normalize("Macro 'fake' created") - - # Macros do path completion - test_dir = os.path.dirname(request.module.__file__) - - text = os.path.join(test_dir, 's') - line = 'fake {}'.format(text) - - endidx = len(line) - begidx = endidx - len(text) - - expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep] - first_match = complete_tester(text, line, begidx, endidx, base_app) - assert first_match is not None and base_app.completion_matches == expected - - def test_default_sort_key(cmd2_app): text = '' line = 'test_sort_key {}'.format(text) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index e3d42d7c7..ed5a00f99 100755 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1034,111 +1034,3 @@ def test_is_valid_command_valid(parser): valid, errmsg = parser.is_valid_command('!subcmd', is_subcommand=True) assert valid assert not errmsg - - -def test_macro_normal_arg_pattern(): - # This pattern matches digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side - from cmd2.parsing import ( - MacroArg, - ) - - pattern = MacroArg.macro_normal_arg_pattern - - # Valid strings - matches = pattern.findall('{5}') - assert matches == ['{5}'] - - matches = pattern.findall('{233}') - assert matches == ['{233}'] - - matches = pattern.findall('{{{{{4}') - assert matches == ['{4}'] - - matches = pattern.findall('{2}}}}}') - assert matches == ['{2}'] - - matches = pattern.findall('{3}{4}{5}') - assert matches == ['{3}', '{4}', '{5}'] - - matches = pattern.findall('{3} {4} {5}') - assert matches == ['{3}', '{4}', '{5}'] - - matches = pattern.findall('{3} {{{4} {5}}}}') - assert matches == ['{3}', '{4}', '{5}'] - - matches = pattern.findall('{3} text {4} stuff {5}}}}') - assert matches == ['{3}', '{4}', '{5}'] - - # Unicode digit - matches = pattern.findall('{\N{ARABIC-INDIC DIGIT ONE}}') - assert matches == ['{\N{ARABIC-INDIC DIGIT ONE}}'] - - # Invalid strings - matches = pattern.findall('5') - assert not matches - - matches = pattern.findall('{5') - assert not matches - - matches = pattern.findall('5}') - assert not matches - - matches = pattern.findall('{{5}}') - assert not matches - - matches = pattern.findall('{5text}') - assert not matches - - -def test_macro_escaped_arg_pattern(): - # This pattern matches digits surrounded by 2 or more braces on both sides - from cmd2.parsing import ( - MacroArg, - ) - - pattern = MacroArg.macro_escaped_arg_pattern - - # Valid strings - matches = pattern.findall('{{5}}') - assert matches == ['{{5}}'] - - matches = pattern.findall('{{233}}') - assert matches == ['{{233}}'] - - matches = pattern.findall('{{{{{4}}') - assert matches == ['{{4}}'] - - matches = pattern.findall('{{2}}}}}') - assert matches == ['{{2}}'] - - matches = pattern.findall('{{3}}{{4}}{{5}}') - assert matches == ['{{3}}', '{{4}}', '{{5}}'] - - matches = pattern.findall('{{3}} {{4}} {{5}}') - assert matches == ['{{3}}', '{{4}}', '{{5}}'] - - matches = pattern.findall('{{3}} {{{4}} {{5}}}}') - assert matches == ['{{3}}', '{{4}}', '{{5}}'] - - matches = pattern.findall('{{3}} text {{4}} stuff {{5}}}}') - assert matches == ['{{3}}', '{{4}}', '{{5}}'] - - # Unicode digit - matches = pattern.findall('{{\N{ARABIC-INDIC DIGIT ONE}}}') - assert matches == ['{{\N{ARABIC-INDIC DIGIT ONE}}}'] - - # Invalid strings - matches = pattern.findall('5') - assert not matches - - matches = pattern.findall('{{5') - assert not matches - - matches = pattern.findall('5}}') - assert not matches - - matches = pattern.findall('{5}') - assert not matches - - matches = pattern.findall('{{5text}}') - assert not matches diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index c8c6d34b4..e70185a89 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -83,8 +83,7 @@ def verify_help_text( formatting: -s, --script output commands in script format, i.e. without command numbers - -x, --expanded output fully parsed commands with any aliases and - macros expanded, instead of typed commands + -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 diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index c7293d411..7064b3dba 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -299,17 +299,17 @@ def test_load_commandset_errors(command_sets_manual, capsys): delattr(command_sets_manual, 'do_durian') - # pre-create intentionally conflicting macro and alias names - command_sets_manual.app_cmd('macro create apple run_pyscript') + # pre-create intentionally conflicting aliases + command_sets_manual.app_cmd('alias create apple run_pyscript') command_sets_manual.app_cmd('alias create banana run_pyscript') # now install a command set and verify the commands are now present command_sets_manual.register_command_set(cmd_set) out, err = capsys.readouterr() - # verify aliases and macros are deleted with warning if they conflict with a command + # verify aliases are deleted with warning if they conflict with a command + assert "Deleting alias 'apple'" in err assert "Deleting alias 'banana'" in err - assert "Deleting macro 'apple'" in err # verify duplicate commands are detected with pytest.raises(CommandSetRegistrationError): From 722dc9f0df0b9fd62f7f15ac95cf3db02efc3c3a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 25 Oct 2024 21:31:44 -0400 Subject: [PATCH 02/12] All built-in commands now use a function to create their argument parser. This simplifies the process for overriding cmd2's default parser class. --- CHANGELOG.md | 6 +- cmd2/__init__.py | 50 ++-- cmd2/argparse_custom.py | 17 +- cmd2/cmd2.py | 496 +++++++++++++++++++------------- cmd2/decorators.py | 69 ++--- docs/conf.py | 1 + examples/custom_parser.py | 26 +- examples/help_categories.py | 11 +- examples/override_parser.py | 28 -- plugins/ext_test/pyproject.toml | 9 - pyproject.toml | 9 - tests/test_argparse_custom.py | 23 -- tests/test_history.py | 12 +- 13 files changed, 386 insertions(+), 371 deletions(-) delete mode 100755 examples/override_parser.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 291397a47..737ab407e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## 3.0.0 (TBD) -* Breaking Change +* Breaking Changes * Removed macros +* Enhancements + * Simplified the process to set a custom parser for `cmd2's` built-in commands. + See [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/master/examples/custom_parser.py) + example for more details. ## 2.5.0 (October 23, 2024) * Breaking Change diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 8f1f030ea..abd6d59d5 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -3,8 +3,6 @@ # flake8: noqa F401 """This simply imports certain things for backwards compatibility.""" -import sys - import importlib.metadata as importlib_metadata try: @@ -15,17 +13,19 @@ from typing import List +from . import plugin from .ansi import ( - Cursor, Bg, - Fg, + Cursor, EightBitBg, EightBitFg, + Fg, RgbBg, RgbFg, TextStyle, style, ) +from .argparse_completer import set_default_ap_completer_type from .argparse_custom import ( Cmd2ArgumentParser, Cmd2AttributeWrapper, @@ -33,23 +33,21 @@ register_argparse_argument_parameter, set_default_argument_parser_type, ) - -# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER. -# Do this before loading cmd2.Cmd class so its commands use the custom parser. -import argparse - -cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None) -if cmd2_parser_module is not None: - import importlib - - importlib.import_module(cmd2_parser_module) - -from .argparse_completer import set_default_ap_completer_type - from .cmd2 import Cmd -from .command_definition import CommandSet, with_default_category -from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS -from .decorators import with_argument_list, with_argparser, with_category, as_subcommand_to +from .command_definition import ( + CommandSet, + with_default_category, +) +from .constants import ( + COMMAND_NAME, + DEFAULT_SHORTCUTS, +) +from .decorators import ( + as_subcommand_to, + with_argparser, + with_argument_list, + with_category, +) from .exceptions import ( Cmd2ArgparseError, CommandSetRegistrationError, @@ -57,11 +55,14 @@ PassThroughException, SkipPostcommandHooks, ) -from . import plugin from .parsing import Statement from .py_bridge import CommandResult -from .utils import categorize, CompletionMode, CustomCompletionSettings, Settable - +from .utils import ( + CompletionMode, + CustomCompletionSettings, + Settable, + categorize, +) __all__: List[str] = [ 'COMMAND_NAME', @@ -81,8 +82,8 @@ 'Cmd2AttributeWrapper', 'CompletionItem', 'register_argparse_argument_parameter', - 'set_default_argument_parser_type', 'set_default_ap_completer_type', + 'set_default_argument_parser_type', # Cmd2 'Cmd', 'CommandResult', @@ -98,6 +99,7 @@ 'Cmd2ArgparseError', 'CommandSetRegistrationError', 'CompletionError', + 'PassThroughException', 'SkipPostcommandHooks', # modules 'plugin', diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 8201d54b9..bed8bb475 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -1402,14 +1402,21 @@ def set(self, new_val: Any) -> None: self.__attribute = new_val -# The default ArgumentParser class for a cmd2 app -DEFAULT_ARGUMENT_PARSER: Type[argparse.ArgumentParser] = Cmd2ArgumentParser +# Parser type used by cmd2's built-in commands. +# Set it using cmd2.set_default_argument_parser_type(). +DEFAULT_ARGUMENT_PARSER: Type[Cmd2ArgumentParser] = Cmd2ArgumentParser -def set_default_argument_parser_type(parser_type: Type[argparse.ArgumentParser]) -> None: +def set_default_argument_parser_type(parser_type: Type[Cmd2ArgumentParser]) -> None: """ - Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if - you want to override the parser for cmd2's built-in commands. See examples/override_parser.py. + Set the default ArgumentParser class for cmd2's built-in commands. + + Since built-in commands rely on customizations made in Cmd2ArgumentParser, + your custom parser class should inherit from Cmd2ArgumentParser. + + This should be called prior to instantiating your CLI object. + + See examples/custom_parser.py. """ global DEFAULT_ARGUMENT_PARSER DEFAULT_ARGUMENT_PARSER = parser_type diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index b53ae7a20..25fcc735c 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -83,6 +83,7 @@ ) from .argparse_custom import ( ChoicesProviderFunc, + Cmd2ArgumentParser, CompleterFunc, CompletionItem, ) @@ -3270,13 +3271,19 @@ def _cmdloop(self) -> None: ############################################################# # Top-level parser for alias - alias_description = "Manage aliases\n" "\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_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') - alias_subparsers.required = True + @staticmethod + def _build_alias_parser() -> Cmd2ArgumentParser: + alias_description = ( + "Manage aliases\n" "\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_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') + alias_subparsers.required = True + + return alias_parser # Preserve quotes since we are passing strings to other commands - @with_argparser(alias_parser, preserve_quotes=True) + @with_argparser(_build_alias_parser, preserve_quotes=True) def do_alias(self, args: argparse.Namespace) -> None: """Manage aliases""" # Call handler for whatever subcommand was selected @@ -3284,34 +3291,44 @@ def do_alias(self, args: argparse.Namespace) -> None: handler(args) # alias -> create - alias_create_description = "Create or overwrite an alias" + @staticmethod + def _build_alias_create_parser() -> Cmd2ArgumentParser: + alias_create_description = "Create or overwrite an alias" + + 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_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_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( + description=alias_create_description, + epilog=alias_create_epilog, + ) + alias_create_parser.add_argument('name', help='name of this alias') + alias_create_parser.add_argument( + 'command', + help='what the alias resolves to', + choices_provider=Cmd._get_commands_and_aliases_for_completion, + ) + alias_create_parser.add_argument( + 'command_args', + nargs=argparse.REMAINDER, + help='arguments to pass to command', + completer=Cmd.path_complete, + ) - alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description=alias_create_description, epilog=alias_create_epilog - ) - alias_create_parser.add_argument('name', help='name of this alias') - alias_create_parser.add_argument( - 'command', help='what the alias resolves to', choices_provider=_get_commands_and_aliases_for_completion - ) - alias_create_parser.add_argument( - 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete - ) + return alias_create_parser - @as_subcommand_to('alias', 'create', 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 @@ -3344,20 +3361,23 @@ def _alias_create(self, args: argparse.Namespace) -> None: self.last_result = True # alias -> delete - alias_delete_help = "delete aliases" - 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") - alias_delete_parser.add_argument( - 'names', - nargs=argparse.ZERO_OR_MORE, - help='alias(es) to delete', - choices_provider=_get_alias_completion_items, - descriptive_header=_alias_completion_table.generate_header(), - ) + @staticmethod + def _build_alias_delete_parser() -> Cmd2ArgumentParser: + 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") + alias_delete_parser.add_argument( + 'names', + nargs=argparse.ZERO_OR_MORE, + help='alias(es) to delete', + choices_provider=Cmd._get_alias_completion_items, + descriptive_header=Cmd._alias_completion_table.generate_header(), + ) + + return alias_delete_parser - @as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help) + @as_subcommand_to('alias', 'delete', _build_alias_delete_parser, help="delete aliases") def _alias_delete(self, args: argparse.Namespace) -> None: """Delete aliases""" self.last_result = True @@ -3377,24 +3397,27 @@ def _alias_delete(self, args: argparse.Namespace) -> None: self.perror(f"Alias '{cur_name}' does not exist") # alias -> list - alias_list_help = "list aliases" - 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." - ) + @staticmethod + def _build_alias_list_parser() -> 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_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description) - alias_list_parser.add_argument( - 'names', - nargs=argparse.ZERO_OR_MORE, - help='alias(es) to list', - choices_provider=_get_alias_completion_items, - descriptive_header=_alias_completion_table.generate_header(), - ) + alias_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description) + alias_list_parser.add_argument( + 'names', + nargs=argparse.ZERO_OR_MORE, + help='alias(es) to list', + choices_provider=Cmd._get_alias_completion_items, + descriptive_header=Cmd._alias_completion_table.generate_header(), + ) - @as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help) + return alias_list_parser + + @as_subcommand_to('alias', 'list', _build_alias_list_parser, help="list aliases") def _alias_list(self, args: argparse.Namespace) -> None: """List some or all aliases as 'alias create' commands""" self.last_result = {} # Dict[alias_name, alias_value] @@ -3457,24 +3480,36 @@ def complete_help_subcommands( completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self) return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands']) - help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description="List available commands or provide " "detailed help for a specific command" - ) - help_parser.add_argument( - '-v', '--verbose', action='store_true', help="print a list of all commands with descriptions of each" - ) - help_parser.add_argument( - 'command', nargs=argparse.OPTIONAL, help="command to retrieve help for", completer=complete_help_command - ) - help_parser.add_argument( - 'subcommands', nargs=argparse.REMAINDER, help="subcommand(s) to retrieve help for", completer=complete_help_subcommands - ) + @staticmethod + def _build_help_parser() -> Cmd2ArgumentParser: + help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( + description="List available commands or provide detailed help for a specific command" + ) + help_parser.add_argument( + '-v', + '--verbose', + action='store_true', + help="print a list of all commands with descriptions of each", + ) + help_parser.add_argument( + 'command', + nargs=argparse.OPTIONAL, + help="command to retrieve help for", + completer=Cmd.complete_help_command, + ) + help_parser.add_argument( + 'subcommands', + nargs=argparse.REMAINDER, + help="subcommand(s) to retrieve help for", + completer=Cmd.complete_help_subcommands, + ) + return help_parser # Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command if getattr(cmd.Cmd, 'complete_help', None) is not None: delattr(cmd.Cmd, 'complete_help') - @with_argparser(help_parser) + @with_argparser(_build_help_parser) def do_help(self, args: argparse.Namespace) -> None: """List available commands or provide detailed help for a specific command""" self.last_result = True @@ -3697,9 +3732,11 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None: self.poutput(table_str_buf.getvalue()) - shortcuts_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts") + @staticmethod + def _build_shortcuts_parser() -> Cmd2ArgumentParser: + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts") - @with_argparser(shortcuts_parser) + @with_argparser(_build_shortcuts_parser) def do_shortcuts(self, _: argparse.Namespace) -> None: """List available shortcuts""" # Sort the shortcut tuples by name @@ -3708,11 +3745,14 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: self.poutput(f"Shortcuts for other commands:\n{result}") self.last_result = True - eof_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description="Called when Ctrl-D is pressed", epilog=INTERNAL_COMMAND_EPILOG - ) + @staticmethod + def _build_eof_parser() -> Cmd2ArgumentParser: + return argparse_custom.DEFAULT_ARGUMENT_PARSER( + description="Called when Ctrl-D is pressed", + epilog=Cmd.INTERNAL_COMMAND_EPILOG, + ) - @with_argparser(eof_parser) + @with_argparser(_build_eof_parser) def do_eof(self, _: argparse.Namespace) -> Optional[bool]: """ Called when Ctrl-D is pressed and calls quit with no arguments. @@ -3723,9 +3763,11 @@ def do_eof(self, _: argparse.Namespace) -> Optional[bool]: # self.last_result will be set by do_quit() return self.do_quit('') - quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application") + @staticmethod + def _build_quit_parser() -> Cmd2ArgumentParser: + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application") - @with_argparser(quit_parser) + @with_argparser(_build_quit_parser) def do_quit(self, _: argparse.Namespace) -> Optional[bool]: """Exit this application""" # Return True to stop the command loop @@ -3792,7 +3834,7 @@ def complete_set_value( raise CompletionError(param + " is not a settable parameter") # Create a parser with a value field based on this settable - settable_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent]) + settable_parser = Cmd._build_base_set_parser() # Settables with choices list the values of those choices instead of the arg name # in help text and this shows in tab completion hints. Set metavar to avoid this. @@ -3812,30 +3854,42 @@ def complete_set_value( _, raw_tokens = self.tokens_for_completion(line, begidx, endidx) return completer.complete(text, line, begidx, endidx, raw_tokens[1:]) - # 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 parent parser with all the common elements. - set_description = ( - "Set a settable parameter or show current settings of parameters\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_parser_parent = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False) - set_parser_parent.add_argument( - 'param', - nargs=argparse.OPTIONAL, - help='parameter to set or view', - choices_provider=_get_settable_completion_items, - descriptive_header=_settable_completion_table.generate_header(), - ) + @staticmethod + def _build_base_set_parser() -> 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." + ) + base_set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description) + base_set_parser.add_argument( + 'param', + nargs=argparse.OPTIONAL, + help='parameter to set or view', + choices_provider=Cmd._get_settable_completion_items, + descriptive_header=Cmd._settable_completion_table.generate_header(), + ) - # Create the parser for the set command - set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent]) - set_parser.add_argument( - 'value', nargs=argparse.OPTIONAL, help='new value for settable', completer=complete_set_value, suppress_tab_hint=True - ) + return base_set_parser + + @staticmethod + def _build_set_parser() -> Cmd2ArgumentParser: + # Create the parser for the set command + set_parser = Cmd._build_base_set_parser() + set_parser.add_argument( + 'value', + nargs=argparse.OPTIONAL, + help='new value for settable', + completer=Cmd.complete_set_value, + suppress_tab_hint=True, + ) + + return set_parser # Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value - @with_argparser(set_parser, preserve_quotes=True) + @with_argparser(_build_set_parser, preserve_quotes=True) def do_set(self, args: argparse.Namespace) -> None: """Set a settable parameter or show current settings of parameters""" self.last_result = False @@ -3892,14 +3946,18 @@ def do_set(self, args: argparse.Namespace) -> None: self.poutput(table.generate_data_row(row_data)) self.last_result[param] = settable.get_value() - 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=shell_cmd_complete) - shell_parser.add_argument( - 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete - ) + @staticmethod + def _build_shell_parser() -> Cmd2ArgumentParser: + 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=Cmd.shell_cmd_complete) + shell_parser.add_argument( + 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=Cmd.path_complete + ) + + return shell_parser # Preserve quotes since we are passing these strings to the shell - @with_argparser(shell_parser, preserve_quotes=True) + @with_argparser(_build_shell_parser, preserve_quotes=True) def do_shell(self, args: argparse.Namespace) -> None: """Execute a command as if at the OS prompt""" import signal @@ -4197,9 +4255,11 @@ def py_quit() -> None: return py_bridge.stop - py_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell") + @staticmethod + def _build_py_parser() -> Cmd2ArgumentParser: + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell") - @with_argparser(py_parser) + @with_argparser(_build_py_parser) def do_py(self, _: argparse.Namespace) -> Optional[bool]: """ Run an interactive Python shell @@ -4208,13 +4268,19 @@ def do_py(self, _: argparse.Namespace) -> Optional[bool]: # self.last_resort will be set by _run_python() return self._run_python() - run_pyscript_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run a Python script file inside the console") - run_pyscript_parser.add_argument('script_path', help='path to the script file', completer=path_complete) - run_pyscript_parser.add_argument( - 'script_arguments', nargs=argparse.REMAINDER, help='arguments to pass to script', completer=path_complete - ) + @staticmethod + def _build_run_pyscript_parser() -> Cmd2ArgumentParser: + run_pyscript_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( + description="Run a Python script file inside the console" + ) + run_pyscript_parser.add_argument('script_path', help='path to the script file', completer=Cmd.path_complete) + run_pyscript_parser.add_argument( + 'script_arguments', nargs=argparse.REMAINDER, help='arguments to pass to script', completer=Cmd.path_complete + ) + + return run_pyscript_parser - @with_argparser(run_pyscript_parser) + @with_argparser(_build_run_pyscript_parser) def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: """ Run a Python script file inside the console @@ -4249,9 +4315,11 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: return py_return - ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell") + @staticmethod + def _build_ipython_parser() -> Cmd2ArgumentParser: + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell") - @with_argparser(ipython_parser) + @with_argparser(_build_ipython_parser) def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover """ Enter an interactive IPython shell @@ -4320,54 +4388,68 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover finally: self._in_py = False - history_description = "View, run, edit, save, or clear previously entered commands" - - history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=history_description) - 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') - history_action_group.add_argument( - '-o', '--output_file', metavar='FILE', help='output commands to a script file, implies -s', completer=path_complete - ) - history_action_group.add_argument( - '-t', - '--transcript', - metavar='TRANSCRIPT_FILE', - help='output commands and results to a transcript file,\nimplies -s', - completer=path_complete, - ) - history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history') + @staticmethod + def _build_history_parser() -> Cmd2ArgumentParser: + history_description = "View, run, edit, save, or clear previously entered commands" + + history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=history_description) + 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') + history_action_group.add_argument( + '-o', + '--output_file', + metavar='FILE', + help='output commands to a script file, implies -s', + completer=Cmd.path_complete, + ) + history_action_group.add_argument( + '-t', + '--transcript', + metavar='TRANSCRIPT_FILE', + help='output commands and results to a transcript file,\nimplies -s', + completer=Cmd.path_complete, + ) + history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history') + + history_format_group = history_parser.add_argument_group(title='formatting') + history_format_group.add_argument( + '-s', + '--script', + action='store_true', + help='output commands in script format, i.e. without command\nnumbers', + ) + history_format_group.add_argument( + '-x', + '--expanded', + action='store_true', + help='output fully parsed commands with aliases and shortcuts expanded', + ) + history_format_group.add_argument( + '-v', + '--verbose', + action='store_true', + help='display history and include expanded commands if they\ndiffer from the typed command', + ) + history_format_group.add_argument( + '-a', + '--all', + action='store_true', + help='display all commands, including ones persisted from\nprevious sessions', + ) - history_format_group = history_parser.add_argument_group(title='formatting') - history_format_group.add_argument( - '-s', '--script', action='store_true', help='output commands in script format, i.e. without command\n' 'numbers' - ) - history_format_group.add_argument( - '-x', - '--expanded', - action='store_true', - help='output fully parsed commands with aliases and shortcuts expanded', - ) - history_format_group.add_argument( - '-v', - '--verbose', - action='store_true', - help='display history and include expanded commands if they\n' 'differ from the typed command', - ) - history_format_group.add_argument( - '-a', '--all', action='store_true', help='display all commands, including ones persisted from\n' 'previous sessions' - ) + history_arg_help = ( + "empty all history items\n" + "a one history item by number\n" + "a..b, a:b, a:, ..b items by indices (inclusive)\n" + "string items containing string\n" + "/regex/ items matching regular expression" + ) + history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help) - history_arg_help = ( - "empty all history items\n" - "a one history item by number\n" - "a..b, a:b, a:, ..b items by indices (inclusive)\n" - "string items containing string\n" - "/regex/ items matching regular expression" - ) - history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help) + return history_parser - @with_argparser(history_parser) + @with_argparser(_build_history_parser) def do_history(self, args: argparse.Namespace) -> Optional[bool]: """ View, run, edit, save, or clear previously entered commands @@ -4380,13 +4462,11 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]: if args.verbose: if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script: self.poutput("-v cannot be used with any other options") - self.poutput(self.history_parser.format_usage()) return None # -s and -x can only be used if none of these options are present: [-c -r -e -o -t] if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript): self.poutput("-s and -x cannot be used with -c, -r, -e, -o, or -t") - self.poutput(self.history_parser.format_usage()) return None if args.clear: @@ -4696,20 +4776,26 @@ def _generate_transcript( self.pfeedback(f"{commands_run} {plural} saved to transcript file '{transcript_path}'") self.last_result = True - 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)" - ) + @staticmethod + def _build_edit_parser() -> 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)" + ) - edit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=edit_description) - edit_parser.add_argument( - 'file_path', nargs=argparse.OPTIONAL, help="optional path to a file to open in editor", completer=path_complete - ) + edit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=edit_description) + edit_parser.add_argument( + 'file_path', + nargs=argparse.OPTIONAL, + help="optional path to a file to open in editor", + completer=Cmd.path_complete, + ) + return edit_parser - @with_argparser(edit_parser) + @with_argparser(_build_edit_parser) def do_edit(self, args: argparse.Namespace) -> None: """Run a text editor and optionally open a file with it""" @@ -4750,17 +4836,25 @@ def _current_script_dir(self) -> Optional[str]: "the output of the script commands to a transcript for testing purposes.\n" ) - run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=run_script_description) - run_script_parser.add_argument( - '-t', - '--transcript', - metavar='TRANSCRIPT_FILE', - help='record the output of the script as a transcript file', - completer=path_complete, - ) - run_script_parser.add_argument('script_path', help="path to the script file", completer=path_complete) + @staticmethod + def _build_run_script_parser() -> Cmd2ArgumentParser: + run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=Cmd.run_script_description) + run_script_parser.add_argument( + '-t', + '--transcript', + metavar='TRANSCRIPT_FILE', + help='record the output of the script as a transcript file', + completer=Cmd.path_complete, + ) + run_script_parser.add_argument( + 'script_path', + help="path to the script file", + completer=Cmd.path_complete, + ) + + return run_script_parser - @with_argparser(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. @@ -4823,21 +4917,25 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: self._script_dir.pop() return None - relative_run_script_description = 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." - ) + @staticmethod + def _build_relative_run_script_parser() -> Cmd2ArgumentParser: + relative_run_script_description = Cmd.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_epilog = "Notes:\n" " This command is intended to only be used within text file scripts." + 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.add_argument('file_path', help='a file path pointing to a script') + relative_run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( + description=relative_run_script_description, epilog=relative_run_script_epilog + ) + relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script') + + return relative_run_script_parser - @with_argparser(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 diff --git a/cmd2/decorators.py b/cmd2/decorators.py index cd9fd358c..d3ff80262 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -11,9 +11,9 @@ Optional, Sequence, Tuple, + Type, TypeVar, Union, - overload, ) from . import ( @@ -65,19 +65,18 @@ def cat_decorator(func: CommandFunc) -> CommandFunc: return cat_decorator -########################## -# The _parse_positionals and _arg_swap functions allow for additional positional args to be preserved -# in cmd2 command functions/callables. As long as the 2-ple of arguments we expect to be there can be -# found we can swap out the statement with each decorator's specific parameters -########################## - - CommandParent = TypeVar('CommandParent', bound=Union['cmd2.Cmd', CommandSet]) +CommandParentType = TypeVar('CommandParentType', bound=Union[Type['cmd2.Cmd'], Type[CommandSet]]) RawCommandFuncOptionalBoolReturn = Callable[[CommandParent, Union[Statement, str]], Optional[bool]] +########################## +# The _parse_positionals and _arg_swap functions allow for additional positional args to be preserved +# in cmd2 command functions/callables. As long as the 2-ple of arguments we expect to be there can be +# found we can swap out the statement with each decorator's specific parameters +########################## def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Statement, str]]: """ Helper function for cmd2 decorators to inspect the positional arguments until the cmd2.Cmd argument is found @@ -265,28 +264,12 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: ] -@overload -def with_argparser( - parser: argparse.ArgumentParser, - *, - ns_provider: Optional[Callable[..., argparse.Namespace]] = None, - preserve_quotes: bool = False, - with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover - - -@overload -def with_argparser( - parser: Callable[[], argparse.ArgumentParser], - *, - ns_provider: Optional[Callable[..., argparse.Namespace]] = None, - preserve_quotes: bool = False, - with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover - - def with_argparser( - parser: Union[argparse.ArgumentParser, Callable[[], argparse.ArgumentParser]], + parser: Union[ + argparse.ArgumentParser, # existing parser + Callable[[], argparse.ArgumentParser], # function or staticmethod + Callable[[CommandParentType], argparse.ArgumentParser], # Cmd or CommandSet classmethod + ], *, ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: bool = False, @@ -413,32 +396,14 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]: return arg_decorator -@overload -def as_subcommand_to( - command: str, - subcommand: str, - parser: argparse.ArgumentParser, - *, - help: Optional[str] = None, - aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover - - -@overload -def as_subcommand_to( - command: str, - subcommand: str, - parser: Callable[[], argparse.ArgumentParser], - *, - help: Optional[str] = None, - aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover - - def as_subcommand_to( command: str, subcommand: str, - parser: Union[argparse.ArgumentParser, Callable[[], argparse.ArgumentParser]], + parser: Union[ + argparse.ArgumentParser, # existing parser + Callable[[], argparse.ArgumentParser], # function or staticmethod + Callable[[CommandParentType], argparse.ArgumentParser], # Cmd or CommandSet classmethod + ], *, help: Optional[str] = None, aliases: Optional[List[str]] = None, diff --git a/docs/conf.py b/docs/conf.py index f4f3451be..499aa274a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -181,6 +181,7 @@ nitpick_ignore = [ ('py:class', 'cmd2.decorators.CommandParent'), ('py:obj', 'cmd2.decorators.CommandParent'), + ('py:class', 'cmd2.decorators.CommandParentType'), ('py:class', 'argparse._SubParsersAction'), ('py:class', 'cmd2.utils._T'), ('py:class', 'types.FrameType'), diff --git a/examples/custom_parser.py b/examples/custom_parser.py index 94df3b054..419c0a54a 100644 --- a/examples/custom_parser.py +++ b/examples/custom_parser.py @@ -1,25 +1,29 @@ # coding=utf-8 """ -Defines the CustomParser used with override_parser.py example +The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser. +The following code shows how to override it with your own parser class. """ import sys +from typing import NoReturn from cmd2 import ( Cmd2ArgumentParser, ansi, + cmd2, set_default_argument_parser_type, ) -# First define the parser +# Since built-in commands rely on customizations made in Cmd2ArgumentParser, +# your custom parser class should inherit from Cmd2ArgumentParser. class CustomParser(Cmd2ArgumentParser): - """Overrides error class""" + """Overrides error method""" - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - def error(self, message: str) -> None: + def error(self, message: str) -> NoReturn: """Custom override that applies custom formatting to the error message""" lines = message.split('\n') linum = 0 @@ -38,5 +42,13 @@ def error(self, message: str) -> None: self.exit(2, '{}\n\n'.format(formatted_message)) -# Now set the default parser for a cmd2 app -set_default_argument_parser_type(CustomParser) +if __name__ == '__main__': + import sys + + # Set the default parser type before instantiating app. + set_default_argument_parser_type(CustomParser) + + app = cmd2.Cmd(include_ipy=True, persistent_history_file='cmd2_history.dat') + app.self_in_py = True # Enable access to "self" within the py command + app.debug = True # Show traceback if/when an exception occurs + sys.exit(app.cmdloop()) diff --git a/examples/help_categories.py b/examples/help_categories.py index 8059ca90a..bb5a1d435 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -11,7 +11,6 @@ import cmd2 from cmd2 import ( COMMAND_NAME, - argparse_custom, ) @@ -60,8 +59,9 @@ def do_deploy(self, _): """Deploy command""" self.poutput('Deploy') - start_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description='Start', epilog='my_decorator runs even with argparse errors' + start_parser = cmd2.Cmd2ArgumentParser( + description='Start', + epilog='my_decorator runs even with argparse errors', ) start_parser.add_argument('when', choices=START_TIMES, help='Specify when to start') @@ -79,8 +79,9 @@ def do_redeploy(self, _): """Redeploy command""" self.poutput('Redeploy') - restart_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description='Restart', epilog='my_decorator does not run when argparse errors' + restart_parser = cmd2.Cmd2ArgumentParser( + description='Restart', + epilog='my_decorator does not run when argparse errors', ) restart_parser.add_argument('when', choices=START_TIMES, help='Specify when to restart') diff --git a/examples/override_parser.py b/examples/override_parser.py deleted file mode 100755 index 36f25c752..000000000 --- a/examples/override_parser.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 -# flake8: noqa F402 -""" -The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser. -The following code shows how to override it with your own parser class. -""" - -# First set a value called argparse.cmd2_parser_module with the module that defines the custom parser. -# See the code for custom_parser.py. It simply defines a parser and calls cmd2.set_default_argument_parser_type() -# with the custom parser's type. -import argparse - -argparse.cmd2_parser_module = 'examples.custom_parser' - -# Next import from cmd2. It will import your module just before the cmd2.Cmd class file is imported -# and therefore override the parser class it uses on its commands. -from cmd2 import ( - cmd2, -) - -if __name__ == '__main__': - import sys - - app = cmd2.Cmd(include_ipy=True, persistent_history_file='cmd2_history.dat') - app.self_in_py = True # Enable access to "self" within the py command - app.debug = True # Show traceback if/when an exception occurs - sys.exit(app.cmdloop()) diff --git a/plugins/ext_test/pyproject.toml b/plugins/ext_test/pyproject.toml index 715301a9b..5dbbd8268 100644 --- a/plugins/ext_test/pyproject.toml +++ b/plugins/ext_test/pyproject.toml @@ -145,19 +145,10 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" mccabe.max-complexity = 49 -per-file-ignores."cmd2/__init__.py" = [ - "E402", # Module level import not at top of file - "F401", # Unused import -] - per-file-ignores."docs/conf.py" = [ "F401", # Unused import ] -per-file-ignores."examples/override_parser.py" = [ - "E402", # Module level import not at top of file -] - per-file-ignores."examples/scripts/*.py" = [ "F821", # Undefined name `app` ] diff --git a/pyproject.toml b/pyproject.toml index 6dae9cea9..c497b8678 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,19 +186,10 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" mccabe.max-complexity = 49 -per-file-ignores."cmd2/__init__.py" = [ - "E402", # Module level import not at top of file - "F401", # Unused import -] - per-file-ignores."docs/conf.py" = [ "F401", # Unused import ] -per-file-ignores."examples/override_parser.py" = [ - "E402", # Module level import not at top of file -] - per-file-ignores."examples/scripts/*.py" = [ "F821", # Undefined name `app` ] diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index a3f85558c..f9eeec5dc 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -243,29 +243,6 @@ def test_apcustom_required_options(): assert 'required arguments' in parser.format_help() -def test_override_parser(): - """Test overriding argparse_custom.DEFAULT_ARGUMENT_PARSER""" - import importlib - - from cmd2 import ( - argparse_custom, - ) - - # The standard parser is Cmd2ArgumentParser - assert argparse_custom.DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser - - # Set our parser module and force a reload of cmd2 so it loads the module - argparse.cmd2_parser_module = 'examples.custom_parser' - importlib.reload(cmd2) - - # Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser - from examples.custom_parser import ( - CustomParser, - ) - - assert argparse_custom.DEFAULT_ARGUMENT_PARSER == CustomParser - - def test_apcustom_metavar_tuple(): # Test the case when a tuple metavar is used with nargs an integer > 1 parser = Cmd2ArgumentParser() diff --git a/tests/test_history.py b/tests/test_history.py index 1a3bd744b..f2e122777 100755 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -772,9 +772,7 @@ def test_history_verbose_with_other_options(base_app): options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x'] for opt in options_to_test: out, err = run_cmd(base_app, 'history -v ' + opt) - assert len(out) == 4 - assert out[0] == '-v cannot be used with any other options' - assert out[1].startswith('Usage:') + assert '-v cannot be used with any other options' in out assert base_app.last_result is False @@ -800,9 +798,7 @@ def test_history_script_with_invalid_options(base_app): options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] for opt in options_to_test: out, err = run_cmd(base_app, 'history -s ' + opt) - assert len(out) == 4 - assert out[0] == '-s and -x cannot be used with -c, -r, -e, -o, or -t' - assert out[1].startswith('Usage:') + assert '-s and -x cannot be used with -c, -r, -e, -o, or -t' in out assert base_app.last_result is False @@ -820,9 +816,7 @@ def test_history_expanded_with_invalid_options(base_app): options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] for opt in options_to_test: out, err = run_cmd(base_app, 'history -x ' + opt) - assert len(out) == 4 - assert out[0] == '-s and -x cannot be used with -c, -r, -e, -o, or -t' - assert out[1].startswith('Usage:') + assert '-s and -x cannot be used with -c, -r, -e, -o, or -t' in out assert base_app.last_result is False From 5ce8407c6a85c312a6a045c4ad45dacf3673c26f Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 1 Nov 2024 09:54:09 -0400 Subject: [PATCH 03/12] Integrated rich-argparse with cmd2. (#1356) --- .github/workflows/mypy.yml | 2 +- Pipfile | 2 + cmd2/argparse_custom.py | 127 ++++++++++- cmd2/cmd2.py | 203 +++++++++--------- docs/conf.py | 3 + docs/features/help.rst | 21 +- setup.py | 2 + tests/conftest.py | 17 +- tests/test_argparse.py | 20 +- tests/test_argparse_custom.py | 2 +- tests/test_cmd2.py | 2 +- tests/test_transcript.py | 4 +- tests/transcripts/from_cmdloop.txt | 2 +- .../test_argparse_subcommands.py | 14 +- 14 files changed, 266 insertions(+), 155 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 6c07f5360..b1de64890 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -19,5 +19,5 @@ jobs: # Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. # Set fetch-depth: 0 to fetch all history for all branches and tags. fetch-depth: 0 # Needed for setuptools_scm to work correctly - - run: pip install -U --user pip mypy + - run: pip install -U --user pip mypy rich rich-argparse - run: mypy . diff --git a/Pipfile b/Pipfile index f8997310a..54ee04499 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,8 @@ verify_ssl = true [packages] pyperclip = "*" +rich = "*" +rich-argparse = "*" setuptools = "*" wcwidth = "*" diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index bed8bb475..2a49d8eab 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -7,7 +7,7 @@ parser that inherits from it. This will give a consistent look-and-feel between the help/error output of built-in cmd2 commands and the app-specific commands. If you wish to override the parser used by cmd2's built-in commands, see -override_parser.py example. +custom_parser.py example. Since the new capabilities are added by patching at the argparse API level, they are available whether or not Cmd2ArgumentParser is used. However, the help @@ -265,6 +265,18 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) runtime_checkable, ) +from rich.console import ( + Group, + RenderableType, +) +from rich_argparse import ( + ArgumentDefaultsRichHelpFormatter, + MetavarTypeRichHelpFormatter, + RawDescriptionRichHelpFormatter, + RawTextRichHelpFormatter, + RichHelpFormatter, +) + from . import ( ansi, constants, @@ -1042,9 +1054,14 @@ def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) ############################################################################################################ -class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): +class Cmd2HelpFormatter(RichHelpFormatter): """Custom help formatter to configure ordering of help text""" + # Render markup in usage, help, description, and epilog text. + RichHelpFormatter.usage_markup = True + RichHelpFormatter.help_markup = True + RichHelpFormatter.text_markup = True + def _format_usage( self, usage: Optional[str], @@ -1249,6 +1266,84 @@ 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'). + """ + + +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: + """ + :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: + """Custom rendering logic.""" + import rich + + formatter = self.formatter_creator() + + styled_title = rich.text.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 = rich.table.Table( + 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""" @@ -1256,10 +1351,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, @@ -1279,10 +1374,10 @@ def __init__( super(Cmd2ArgumentParser, self).__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] + formatter_class=formatter_class, prefix_chars=prefix_chars, fromfile_prefix_chars=fromfile_prefix_chars, argument_default=argument_default, @@ -1291,6 +1386,10 @@ def __init__( allow_abbrev=allow_abbrev, ) + # Recast to assist type checkers since in a Cmd2HelpFormatter, these can be Rich renderables. + 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 @@ -1321,6 +1420,10 @@ def error(self, message: str) -> NoReturn: formatted_message = ansi.style_error(formatted_message) self.exit(2, f'{formatted_message}\n\n') + def _get_formatter(self) -> Cmd2HelpFormatter: + """Copy of _get_formatter() with a different return type to assist type checkers.""" + return cast(Cmd2HelpFormatter, super()._get_formatter()) + def format_help(self) -> str: """Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters""" formatter = self._get_formatter() @@ -1329,7 +1432,7 @@ def format_help(self) -> str: formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # type: ignore[arg-type] # description - formatter.add_text(self.description) + formatter.add_text(self.description) # type: ignore[arg-type] # Begin cmd2 customization (separate required and optional arguments) @@ -1370,7 +1473,7 @@ def format_help(self) -> str: # End cmd2 customization # epilog - formatter.add_text(self.epilog) + formatter.add_text(self.epilog) # type: ignore[arg-type] # determine help from format above return formatter.format_help() + '\n' @@ -1382,6 +1485,10 @@ def _print_message(self, message: str, file: Optional[IO[str]] = None) -> 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 25fcc735c..8711f841d 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -220,10 +220,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 @@ -3273,9 +3269,7 @@ def _cmdloop(self) -> None: # Top-level parser for alias @staticmethod def _build_alias_parser() -> Cmd2ArgumentParser: - alias_description = ( - "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string." - ) + alias_description = "Manage aliases." alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description) alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') alias_subparsers.required = True @@ -3285,7 +3279,7 @@ def _build_alias_parser() -> Cmd2ArgumentParser: # Preserve quotes since we are passing strings to other commands @with_argparser(_build_alias_parser, preserve_quotes=True) def do_alias(self, args: argparse.Namespace) -> None: - """Manage aliases""" + """Manage aliases.""" # Call handler for whatever subcommand was selected handler = args.cmd2_handler.get() handler(args) @@ -3293,26 +3287,30 @@ def do_alias(self, args: argparse.Namespace) -> None: # alias -> create @staticmethod def _build_alias_create_parser() -> Cmd2ArgumentParser: - alias_create_description = "Create or overwrite an alias" + from rich.console import Group - 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" + alias_create_description = "Create or overwrite an alias." + + alias_create_notes = ( + "If you want to use redirection, pipes, or terminators in the value of the alias, then quote them.\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" + "Since aliases are resolved during parsing, tab completion will function as it would " + "for the actual command the alias resolves to." ) - alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( - description=alias_create_description, - epilog=alias_create_epilog, + alias_create_examples = ( + "alias create ls !ls -lF\n" + "alias create show_log !cat \"log file.txt\"\n" + "alias create save_results print_results \">\" out.txt" ) + + alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_create_description) + + # Create epilog + notes_group = alias_create_parser.create_text_group("Notes", alias_create_notes) + examples_group = alias_create_parser.create_text_group("Examples", alias_create_examples) + alias_create_parser.epilog = Group(notes_group, "\n", examples_group) + alias_create_parser.add_argument('name', help='name of this alias') alias_create_parser.add_argument( 'command', @@ -3330,7 +3328,7 @@ def _build_alias_create_parser() -> Cmd2ArgumentParser: @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""" + """Create or overwrite an alias.""" self.last_result = False # Validate the alias name @@ -3363,7 +3361,7 @@ def _alias_create(self, args: argparse.Namespace) -> None: # alias -> delete @staticmethod def _build_alias_delete_parser() -> 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") @@ -3379,7 +3377,7 @@ def _build_alias_delete_parser() -> Cmd2ArgumentParser: @as_subcommand_to('alias', 'delete', _build_alias_delete_parser, help="delete aliases") def _alias_delete(self, args: argparse.Namespace) -> None: - """Delete aliases""" + """Delete aliases.""" self.last_result = True if args.all: @@ -3399,12 +3397,7 @@ def _alias_delete(self, args: argparse.Namespace) -> None: # alias -> list @staticmethod def _build_alias_list_parser() -> 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 = "List specified aliases or all aliases if no arguments are given." alias_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description) alias_list_parser.add_argument( @@ -3419,7 +3412,7 @@ def _build_alias_list_parser() -> Cmd2ArgumentParser: @as_subcommand_to('alias', 'list', _build_alias_list_parser, help="list aliases") def _alias_list(self, args: argparse.Namespace) -> None: - """List some or all aliases as 'alias create' commands""" + """List some or all aliases as 'alias create' commands.""" self.last_result = {} # Dict[alias_name, alias_value] tokens_to_quote = constants.REDIRECTION_TOKENS @@ -3483,7 +3476,7 @@ def complete_help_subcommands( @staticmethod def _build_help_parser() -> 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', @@ -3511,7 +3504,7 @@ def _build_help_parser() -> Cmd2ArgumentParser: @with_argparser(_build_help_parser) def do_help(self, args: argparse.Namespace) -> None: - """List available commands or provide detailed help for a specific command""" + """List available commands or provide detailed help for a specific command.""" self.last_result = True if not args.command or args.verbose: @@ -3734,11 +3727,11 @@ 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: - """List available shortcuts""" + """List available shortcuts.""" # Sort the shortcut tuples by name sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0])) result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts) @@ -3747,11 +3740,14 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: @staticmethod def _build_eof_parser() -> Cmd2ArgumentParser: - return argparse_custom.DEFAULT_ARGUMENT_PARSER( - description="Called when Ctrl-D is pressed", - epilog=Cmd.INTERNAL_COMMAND_EPILOG, + 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]: """ @@ -3765,11 +3761,11 @@ 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]: - """Exit this application""" + """Exit this application.""" # Return True to stop the command loop self.last_result = True return True @@ -3858,11 +3854,7 @@ def complete_set_value( def _build_base_set_parser() -> 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 = "Set a settable parameter or show current settings of parameters." base_set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description) base_set_parser.add_argument( 'param', @@ -3872,6 +3864,13 @@ def _build_base_set_parser() -> Cmd2ArgumentParser: descriptive_header=Cmd._settable_completion_table.generate_header(), ) + base_set_parser_notes = ( + "Call without arguments for a list of all settable parameters with their values.\n" + "\n" + "Call with just param to view that parameter's value." + ) + base_set_parser.epilog = base_set_parser.create_text_group("Notes", base_set_parser_notes) + return base_set_parser @staticmethod @@ -3948,7 +3947,7 @@ def do_set(self, args: argparse.Namespace) -> None: @staticmethod def _build_shell_parser() -> 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=Cmd.shell_cmd_complete) shell_parser.add_argument( 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=Cmd.path_complete @@ -3959,7 +3958,7 @@ def _build_shell_parser() -> Cmd2ArgumentParser: # Preserve quotes since we are passing these strings to the shell @with_argparser(_build_shell_parser, preserve_quotes=True) def do_shell(self, args: argparse.Namespace) -> None: - """Execute a command as if at the OS prompt""" + """Execute a command as if at the OS prompt.""" import signal import subprocess @@ -4257,12 +4256,13 @@ 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]: """ - Run an interactive Python shell + Run an interactive Python shell. + :return: True if running of commands should stop """ # self.last_resort will be set by _run_python() @@ -4271,7 +4271,7 @@ def do_py(self, _: argparse.Namespace) -> Optional[bool]: @staticmethod def _build_run_pyscript_parser() -> 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=Cmd.path_complete) run_pyscript_parser.add_argument( @@ -4283,7 +4283,7 @@ def _build_run_pyscript_parser() -> 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 """ @@ -4317,12 +4317,12 @@ 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 """ @@ -4390,9 +4390,13 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover @staticmethod def _build_history_parser() -> Cmd2ArgumentParser: - history_description = "View, run, edit, save, or clear previously entered commands" + from .argparse_custom import RawTextCmd2HelpFormatter - history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=history_description) + history_description = "View, run, edit, save, or clear previously entered commands." + + history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( + description=history_description, formatter_class=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') @@ -4400,14 +4404,14 @@ def _build_history_parser() -> Cmd2ArgumentParser: '-o', '--output_file', metavar='FILE', - help='output commands to a script file, implies -s', + help='output commands to a script file, implies --script', completer=Cmd.path_complete, ) history_action_group.add_argument( '-t', '--transcript', metavar='TRANSCRIPT_FILE', - help='output commands and results to a transcript file,\nimplies -s', + help='output commands and results to a transcript file,\nimplies --script', completer=Cmd.path_complete, ) history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history') @@ -4423,7 +4427,7 @@ def _build_history_parser() -> Cmd2ArgumentParser: '-x', '--expanded', action='store_true', - help='output fully parsed commands with aliases and shortcuts expanded', + help='output fully parsed commands with aliases and shortcuts\nexpanded', ) history_format_group.add_argument( '-v', @@ -4452,7 +4456,7 @@ def _build_history_parser() -> Cmd2ArgumentParser: @with_argparser(_build_history_parser) def do_history(self, args: argparse.Namespace) -> Optional[bool]: """ - View, run, edit, save, or clear previously entered commands + View, run, edit, save, or clear previously entered commands. :return: True if running of commands should stop """ @@ -4778,15 +4782,16 @@ def _generate_transcript( @staticmethod def _build_edit_parser() -> 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, @@ -4797,7 +4802,7 @@ def _build_edit_parser() -> Cmd2ArgumentParser: @with_argparser(_build_edit_parser) def do_edit(self, args: argparse.Namespace) -> None: - """Run a text editor and optionally open a file with it""" + """Run a text editor and optionally open a file with it.""" # self.last_result will be set by do_shell() which is called by run_editor() self.run_editor(args.file_path) @@ -4826,19 +4831,24 @@ def _current_script_dir(self) -> Optional[str]: else: 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" - ) + @staticmethod + def _build_base_run_script_parser() -> Cmd2ArgumentParser: + from rich.table import Table + + run_script_description = Table( + box=None, + show_header=False, + padding=(0, 0), + ) + run_script_description.add_row("Run text script.") + run_script_description.add_row() + run_script_description.add_row("Scripts contain one command, written as it would be typed in the console, per line.") + + return argparse_custom.DEFAULT_ARGUMENT_PARSER(description=run_script_description) @staticmethod def _build_run_script_parser() -> Cmd2ArgumentParser: - run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=Cmd.run_script_description) + run_script_parser = Cmd._build_base_run_script_parser() run_script_parser.add_argument( '-t', '--transcript', @@ -4846,17 +4856,14 @@ def _build_run_script_parser() -> Cmd2ArgumentParser: help='record the output of the script as a transcript file', completer=Cmd.path_complete, ) - run_script_parser.add_argument( - 'script_path', - help="path to the script file", - completer=Cmd.path_complete, - ) + run_script_parser.add_argument('script_path', help="path to the script file", completer=Cmd.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 """ @@ -4919,19 +4926,21 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: @staticmethod def _build_relative_run_script_parser() -> Cmd2ArgumentParser: - relative_run_script_description = Cmd.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." - ) + from rich.table import Table - relative_run_script_epilog = "Notes:\n This command is intended to only be used within text file scripts." + relative_run_script_parser = Cmd._build_base_run_script_parser() + relative_run_script_parser.description = cast(Table, relative_run_script_parser.description) + relative_run_script_parser.description.add_row() + relative_run_script_parser.description.add_row( + "If this is run from within a script, script_path will be interpreted relative to that script's directory." + ) - 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') + + relative_run_script_parser.add_argument('script_path', help="path to the script file") return relative_run_script_parser @@ -4942,9 +4951,9 @@ def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]: :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/docs/conf.py b/docs/conf.py index 499aa274a..ba7b8e995 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -185,4 +185,7 @@ ('py:class', 'argparse._SubParsersAction'), ('py:class', 'cmd2.utils._T'), ('py:class', 'types.FrameType'), + ('py:class', 'rich.console.Console'), + ('py:class', 'rich.console.ConsoleRenderable'), + ('py:class', 'rich.console.RichCast'), ] diff --git a/docs/features/help.rst b/docs/features/help.rst index 759a5f150..c4cd09483 100644 --- a/docs/features/help.rst +++ b/docs/features/help.rst @@ -173,17 +173,18 @@ The ``help`` command also has a verbose option (``help -v`` or ``help Other ====================================================================================================== - 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. + alias Manage aliases. + 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. + ipy Run an interactive IPython shell. + py Run an interactive Python shell. + 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 diff --git a/setup.py b/setup.py index 7db19ac11..6659a8d6b 100755 --- a/setup.py +++ b/setup.py @@ -46,6 +46,8 @@ INSTALL_REQUIRES = [ 'pyperclip', + 'rich', + 'rich-argparse', 'wcwidth', ] diff --git a/tests/conftest.py b/tests/conftest.py index 0b3a01786..20b9ebd31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,35 +53,36 @@ 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 + output commands to a script file, implies --script -t, --transcript TRANSCRIPT_FILE output commands and results to a transcript file, - implies -s + implies --script -c, --clear clear all history -formatting: +Formatting: -s, --script output commands in script format, i.e. without command numbers - -x, --expanded output fully parsed commands with aliases and shortcuts expanded + -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 diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 4fe884b9a..13a28b9b9 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -41,8 +41,7 @@ def _say_parser_builder() -> cmd2.Cmd2ArgumentParser: @cmd2.with_argparser(_say_parser_builder) def do_say(self, args, *, keyword_arg: Optional[str] = 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): 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(':') @@ -358,40 +356,26 @@ def test_subcommand_help(subcommand_app): # foo has no aliases out, err = run_cmd(subcommand_app, 'help base foo') assert out[0].startswith('Usage: base foo') - assert out[1] == '' - 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:' 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:' 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:' # 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:' 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:' 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:' def test_subcommand_invalid_help(subcommand_app): diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index f9eeec5dc..3a5b14aa0 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -240,7 +240,7 @@ def test_apcustom_required_options(): # 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(): diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index efa4868b5..b75980e8a 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1567,7 +1567,7 @@ def test_help_with_no_docstring(capsys): out == """Usage: greet [-h] [-s] -optional arguments: +Optional Arguments: -h, --help show this help message and exit -s, --shout N00B EMULATION MODE diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 986221ff2..96ee118bf 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -48,7 +48,9 @@ def __init__(self, *args, **kwargs): speak_parser = cmd2.Cmd2ArgumentParser() speak_parser.add_argument('-p', '--piglatin', action="store_true", help="atinLay") speak_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") - speak_parser.add_argument('-r', '--repeat', type=int, help="output [n] times") + + # Escape open bracket since help text can contain markup + speak_parser.add_argument('-r', '--repeat', type=int, help="output \[n] times") @cmd2.with_argparser(speak_parser, with_unknown_args=True) def do_speak(self, opts, arg): diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt index a615d2432..60455904d 100644 --- a/tests/transcripts/from_cmdloop.txt +++ b/tests/transcripts/from_cmdloop.txt @@ -6,7 +6,7 @@ Usage: say [-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 0fc8c47e9..2a744467d 100644 --- a/tests_isolated/test_commandset/test_argparse_subcommands.py +++ b/tests_isolated/test_commandset/test_argparse_subcommands.py @@ -99,39 +99,39 @@ def test_subcommand_help(subcommand_app): 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): From aa95d05ff2a852e730a6afeb9ad47a303894a8d3 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 2 Nov 2024 19:15:44 -0400 Subject: [PATCH 04/12] Fix a pytest warning about an invalid escape sequence (#1360) --- tests/test_transcript.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 96ee118bf..3567c55cf 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -49,8 +49,8 @@ def __init__(self, *args, **kwargs): speak_parser.add_argument('-p', '--piglatin', action="store_true", help="atinLay") speak_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") - # Escape open bracket since help text can contain markup - speak_parser.add_argument('-r', '--repeat', type=int, help="output \[n] times") + # Escape open bracket since help text can contain rich markup + speak_parser.add_argument('-r', '--repeat', type=int, help=r"output \[n] times") @cmd2.with_argparser(speak_parser, with_unknown_args=True) def do_speak(self, opts, arg): From ef6ba804c10caacb9bed17d49abf6d095dee216b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 3 Nov 2024 07:14:20 -0500 Subject: [PATCH 05/12] Remove unused mypy ignores. (#1361) --- .github/workflows/mypy.yml | 2 +- cmd2/ansi.py | 2 +- cmd2/argparse_completer.py | 6 +++--- cmd2/argparse_custom.py | 18 +++++++++--------- cmd2/clipboard.py | 2 +- cmd2/cmd2.py | 30 +++++++++++++++--------------- cmd2/decorators.py | 4 ++-- cmd2/parsing.py | 2 +- cmd2/table_creator.py | 2 +- cmd2/utils.py | 4 ++-- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 04332408c..99fa843d7 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -19,5 +19,5 @@ jobs: # Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. # Set fetch-depth: 0 to fetch all history for all branches and tags. fetch-depth: 0 # Needed for setuptools_scm to work correctly - - run: pip install -U --user pip mypy rich rich-argparse + - run: pip install -U --user pip mypy pyperclip rich rich-argparse wcwidth - run: mypy . diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 52bf382a1..c0a3a2f1c 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -17,7 +17,7 @@ cast, ) -from wcwidth import ( # type: ignore[import] +from wcwidth import ( # type: ignore[import-untyped] wcswidth, ) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 9bb86e9f1..1e4cd8b45 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -557,7 +557,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List if not self._cmd2_app.matches_sorted: # If all orig_value types are numbers, then sort by that value if all_nums: - completion_items.sort(key=lambda c: c.orig_value) # type: ignore[no-any-return] + completion_items.sort(key=lambda c: c.orig_value) # Otherwise sort as strings else: @@ -744,12 +744,12 @@ def _complete_arg( if not arg_choices.is_completer: choices_func = arg_choices.choices_provider if isinstance(choices_func, ChoicesProviderFuncWithTokens): - completion_items = choices_func(*args, **kwargs) # type: ignore[arg-type] + completion_items = choices_func(*args, **kwargs) else: # pragma: no cover # This won't hit because runtime checking doesn't check function argument types and will always # resolve true above. Mypy, however, does see the difference and gives an error that can't be # ignored. Mypy issue #5485 discusses this problem - completion_items = choices_func(*args) # type: ignore[arg-type] + completion_items = choices_func(*args) # else case is already covered above else: completion_items = arg_choices diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 2a49d8eab..010776c54 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -807,11 +807,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') @@ -819,7 +819,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: @@ -858,7 +858,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] @@ -894,7 +894,7 @@ def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.A if nargs_range[1] == constants.INFINITY: range_max = '' else: - range_max = nargs_range[1] # type: ignore[assignment] + range_max = nargs_range[1] nargs_pattern = f'(-*A{{{nargs_range[0]},{range_max}}}-*)' @@ -1429,10 +1429,10 @@ def format_help(self) -> str: formatter = self._get_formatter() # usage - formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # type: ignore[arg-type] + formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # description - formatter.add_text(self.description) # type: ignore[arg-type] + formatter.add_text(self.description) # Begin cmd2 customization (separate required and optional arguments) @@ -1473,7 +1473,7 @@ def format_help(self) -> str: # End cmd2 customization # epilog - formatter.add_text(self.epilog) # type: ignore[arg-type] + formatter.add_text(self.epilog) # determine help from format above return formatter.format_help() + '\n' diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 454e3484c..376b0015f 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -5,7 +5,7 @@ import typing -import pyperclip # type: ignore[import] +import pyperclip # type: ignore[import-untyped] def get_paste_buffer() -> str: diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 036b1419e..1dc599def 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -987,7 +987,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: for action in command_parser._actions: if isinstance(action, argparse._SubParsersAction): - action.remove_parser(subcommand_name) # type: ignore[arg-type,attr-defined] + action.remove_parser(subcommand_name) # type: ignore[attr-defined] break @property @@ -2095,7 +2095,7 @@ def _perform_completion( completer.complete, tokens=raw_tokens[1:] if preserve_quotes else tokens[1:], cmd_set=cmd_set ) else: - completer_func = self.completedefault # type: ignore[assignment] + completer_func = self.completedefault # Not a recognized command else: @@ -2103,7 +2103,7 @@ def _perform_completion( if self.default_to_shell and command in utils.get_exes_in_path(command): completer_func = self.path_complete else: - completer_func = self.completedefault # type: ignore[assignment] + completer_func = self.completedefault # Otherwise we are completing the command token or performing custom completion else: @@ -2805,11 +2805,11 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: kwargs['executable'] = shell # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen( # type: ignore[call-overload] + proc = subprocess.Popen( statement.pipe_to, stdin=subproc_stdin, stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable] - stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable] + stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, shell=True, **kwargs, ) @@ -2829,7 +2829,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: new_stdout.close() raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run') else: - redir_saved_state.redirecting = True # type: ignore[unreachable] + redir_saved_state.redirecting = True cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) sys.stdout = self.stdout = new_stdout @@ -3068,7 +3068,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover parser.add_argument( 'arg', suppress_tab_hint=True, - choices=choices, # type: ignore[arg-type] + choices=choices, choices_provider=choices_provider, completer=completer, ) @@ -3846,7 +3846,7 @@ def complete_set_value( arg_name, metavar=arg_name, help=settable.description, - choices=settable.choices, # type: ignore[arg-type] + choices=settable.choices, choices_provider=settable.choices_provider, completer=settable.completer, ) @@ -4001,15 +4001,15 @@ def do_shell(self, args: argparse.Namespace) -> None: # still receive the SIGINT since it is in the same process group as us. with self.sigint_protection: # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen( # type: ignore[call-overload] + proc = subprocess.Popen( expanded_command, stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable] - stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable] + stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, shell=True, **kwargs, ) - proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) # type: ignore[arg-type] + proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) proc_reader.wait() # Save the return code of the application for use in a pyscript @@ -4101,10 +4101,10 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env: self._reset_py_display() cmd2_env.sys_stdout = sys.stdout - sys.stdout = self.stdout # type: ignore[assignment] + sys.stdout = self.stdout cmd2_env.sys_stdin = sys.stdin - sys.stdin = self.stdin # type: ignore[assignment] + sys.stdin = self.stdin return cmd2_env @@ -4114,8 +4114,8 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None: :param cmd2_env: the environment settings to restore """ - sys.stdout = cmd2_env.sys_stdout # type: ignore[assignment] - sys.stdin = cmd2_env.sys_stdin # type: ignore[assignment] + sys.stdout = cmd2_env.sys_stdout + sys.stdin = cmd2_env.sys_stdin # Set up readline for cmd2 if rl_type != RlType.NONE: diff --git a/cmd2/decorators.py b/cmd2/decorators.py index d3ff80262..d210627e9 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -184,7 +184,7 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]: cmd2_app, statement = _parse_positionals(args) _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, statement, preserve_quotes) args_list = _arg_swap(args, statement, parsed_arglist) - return func(*args_list, **kwargs) # type: ignore[call-arg] + return func(*args_list, **kwargs) command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :] cmd_wrapper.__doc__ = func.__doc__ @@ -383,7 +383,7 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]: delattr(ns, constants.NS_ATTR_SUBCMD_HANDLER) args_list = _arg_swap(args, statement_arg, *new_args) - return func(*args_list, **kwargs) # type: ignore[call-arg] + return func(*args_list, **kwargs) command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :] diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 7ef1cc3de..7e59a558d 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -39,7 +39,7 @@ def shlex_split(str_to_split: str) -> List[str]: @dataclass(frozen=True) -class Statement(str): # type: ignore[override] +class Statement(str): """String subclass with additional attributes to store the results of parsing. The ``cmd`` module in the standard library passes commands around as a diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 409e7a994..8abe66265 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -24,7 +24,7 @@ Union, ) -from wcwidth import ( # type: ignore[import] +from wcwidth import ( # type: ignore[import-untyped] wcwidth, ) diff --git a/cmd2/utils.py b/cmd2/utils.py index f718d5f14..c55b721e9 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -1172,7 +1172,7 @@ def categorize(func: Union[Callable[..., Any], Iterable[Callable[..., Any]]], ca setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category) else: if inspect.ismethod(func): - setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined] + setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) else: setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) @@ -1192,7 +1192,7 @@ def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]: if inspect.ismethod(meth) or ( inspect.isbuiltin(meth) and getattr(meth, '__self__') is not None and getattr(meth.__self__, '__class__') ): - for cls in inspect.getmro(meth.__self__.__class__): # type: ignore[attr-defined] + for cls in inspect.getmro(meth.__self__.__class__): if meth.__name__ in cls.__dict__: return cls meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing From 6837a1bb7c17e3172e3574dfb657ea1dc0809793 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 5 Nov 2024 09:31:07 -0500 Subject: [PATCH 06/12] Update change log. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2ceab05..17a0c0cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * Simplified the process to set a custom parser for `cmd2's` built-in commands. See [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/master/examples/custom_parser.py) example for more details. + * Integrated `rich-argparse` with `Cmd2HelpFormatter`. + * Added `RawDescriptionCmd2HelpFormatter`, `RawTextCmd2HelpFormatter`, `ArgumentDefaultsCmd2HelpFormatter`, + and `MetavarTypeCmd2HelpFormatter` and they all use `rich-argparse`. ## 2.5.3 (November 5, 2024) * Enhancements From 89bee7e7a3a5df186b15cc0ac8033a0f502b7e3b Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 10 Nov 2024 10:56:05 -0500 Subject: [PATCH 07/12] Add rich-argparse to dependencies in CONTRIBUTING.md --- .github/CONTRIBUTING.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5abfbec74..886a26579 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -46,20 +46,21 @@ The tables below list all prerequisites along with the minimum required version #### Prerequisites to run cmd2 applications -| Prerequisite | Minimum Version | -|------------------------------------------------------|-----------------| -| [python](https://www.python.org/downloads/) | `3.8` | -| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8.2` | -| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.12` | +| Prerequisite | Minimum Version | Purpose | +|----------------------------------------------------------|-----------------|----------------------------------------| +| [python](https://www.python.org/downloads/) | `3.8` | Python programming language | +| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8.2` | Cross-platform clipboard functions | +| [rich-argparse](https://pypi.org/project/rich-argparse/) | `1.6.0` | Rich help formatters for argparse | +| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.12` | Measure the displayed width of unicode | #### Additional prerequisites to build and publish cmd2 -| Prerequisite | Minimum Version | -|----------------------------------------------------------|-----------------| -| [build](https://pypi.org/project/build/) | `1.2.2` | -| [setuptools](https://pypi.org/project/setuptools/) | `72.1.0` | -| [setuptools-scm](https://github.com/pypa/setuptools-scm) | `8.0.4` | -| [twine](https://github.com/pypa/twine) | `5.1.1` | +| Prerequisite | Minimum Version | Purpose | +|----------------------------------------------------------|-----------------|-------------------------------------| +| [build](https://pypi.org/project/build/) | `1.2.2` | Python build frontend | +| [setuptools](https://pypi.org/project/setuptools/) | `72.1.0` | Python package management | +| [setuptools-scm](https://github.com/pypa/setuptools-scm) | `8.0.4` | Manage your versions by scm tag s | +| [twine](https://github.com/pypa/twine) | `5.1.1` | Utilities for interacting with PyPI | #### Additional prerequisites for developing cmd2 From 410849cc81a96cb2a5e9d18a2d207bb869866e5e Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 10 Nov 2024 12:05:37 -0500 Subject: [PATCH 08/12] Remove support for Python 3.8 and make 3.9 minimum required (#1376) --- .github/CONTRIBUTING.md | 4 ++-- .github/workflows/build.yml | 6 +++--- CHANGELOG.md | 1 + README.md | 2 +- docs/conf.py | 1 + docs/overview/installation.rst | 5 ++--- plugins/ext_test/build-pyenvs.sh | 4 ++-- plugins/ext_test/noxfile.py | 2 +- plugins/ext_test/setup.py | 4 ++-- plugins/template/README.md | 30 +++++++++++++++--------------- plugins/template/build-pyenvs.sh | 4 ++-- plugins/template/noxfile.py | 2 +- plugins/template/setup.py | 4 ++-- pyproject.toml | 3 +-- 14 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 886a26579..6191fd405 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -48,7 +48,7 @@ The tables below list all prerequisites along with the minimum required version | Prerequisite | Minimum Version | Purpose | |----------------------------------------------------------|-----------------|----------------------------------------| -| [python](https://www.python.org/downloads/) | `3.8` | Python programming language | +| [python](https://www.python.org/downloads/) | `3.9` | Python programming language | | [pyperclip](https://github.com/asweigart/pyperclip) | `1.8.2` | Cross-platform clipboard functions | | [rich-argparse](https://pypi.org/project/rich-argparse/) | `1.6.0` | Rich help formatters for argparse | | [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.12` | Measure the displayed width of unicode | @@ -96,7 +96,7 @@ on all platforms (Windows, Mac, and Linux). You can install `uv` using instruct You can then install multiple versions of Python using `uv` like so: ```sh -uv python install 3.10 3.11 3.12 3.13 +uv python install 3.9 3.10 3.11 3.12 3.13 ``` ### Forking the project diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f66f7f77..d991fd284 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,15 +2,15 @@ # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions name: "build" -on: [push, pull_request] +on: [ push, pull_request ] jobs: build: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + os: [ ubuntu-latest, macos-latest, windows-latest ] + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 # https://github.com/actions/checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 62070ab0e..542630291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 3.0.0 (TBD) * Breaking Changes + * `cmd2` 3.x supports Python 3.9+ (removed support for Python 3.8) * Removed macros * Enhancements * Simplified the process to set a custom parser for `cmd2's` built-in commands. diff --git a/README.md b/README.md index 5ccd594a4..450c3d602 100755 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u pip install -U cmd2 ``` -cmd2 works with Python 3.8+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies. +cmd2 works with Python 3.9+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies. For information on other installation options, see [Installation Instructions](https://cmd2.readthedocs.io/en/latest/overview/installation.html) in the cmd2 diff --git a/docs/conf.py b/docs/conf.py index 4ff0900b0..cee2b9e10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -190,6 +190,7 @@ ('py:class', 'CommandParent'), ('py:class', 'frame'), ('py:class', 'RawCommandFuncOptionalBoolReturn'), + ('py:class', 'r.Console'), ('py:class', 'rich.console.Console'), ('py:class', 'rich.console.ConsoleRenderable'), ('py:class', 'rich.console.RichCast'), diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 841807c82..c0bd1b45d 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -1,4 +1,3 @@ - Installation Instructions ========================= @@ -7,7 +6,7 @@ Installation Instructions .. _setuptools: https://pypi.org/project/setuptools .. _PyPI: https://pypi.org -``cmd2`` works on Linux, macOS, and Windows. It requires Python 3.8 or +``cmd2`` works on Linux, macOS, and Windows. It requires Python 3.9 or higher, pip_, and setuptools_. If you've got all that, then you can just: .. code-block:: shell @@ -30,7 +29,7 @@ higher, pip_, and setuptools_. If you've got all that, then you can just: Prerequisites ------------- -If you have Python 3 >=3.8 installed from `python.org +If you have Python 3 >=3.9 installed from `python.org `_, you will already have pip_ and setuptools_, but may need to upgrade to the latest versions: diff --git a/plugins/ext_test/build-pyenvs.sh b/plugins/ext_test/build-pyenvs.sh index 20f5c8d43..4b515bbf5 100644 --- a/plugins/ext_test/build-pyenvs.sh +++ b/plugins/ext_test/build-pyenvs.sh @@ -8,7 +8,7 @@ # version numbers are: major.minor.patch # # this script will delete and recreate existing virtualenvs named -# cmd2-3.8, etc. It will also create a .python-version +# cmd2-3.9, etc. It will also create a .python-version # # Prerequisites: # - *nix-ish environment like macOS or Linux @@ -23,7 +23,7 @@ # virtualenvs will be added to '.python-version'. Feel free to modify # this list, but note that this script intentionally won't install # dev, rc, or beta python releases -declare -a pythons=("3.8" "3.9", "3.10", "3.11", "3.12") +declare -a pythons=("3.9", "3.10", "3.11", "3.12", "3.13") # function to find the latest patch of a minor version of python function find_latest_version { diff --git a/plugins/ext_test/noxfile.py b/plugins/ext_test/noxfile.py index 25a95067a..d8aa344bf 100644 --- a/plugins/ext_test/noxfile.py +++ b/plugins/ext_test/noxfile.py @@ -1,7 +1,7 @@ import nox -@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12']) +@nox.session(python=['3.9', '3.10', '3.11', '3.12', '3.13']) def tests(session): session.install('invoke', './[test]') session.run('invoke', 'pytest', '--junit', '--no-pty') diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py index 451104132..a6487e543 100644 --- a/plugins/ext_test/setup.py +++ b/plugins/ext_test/setup.py @@ -33,7 +33,7 @@ license='MIT', package_data=PACKAGE_DATA, packages=['cmd2_ext_test'], - python_requires='>=3.8', + python_requires='>=3.9', install_requires=['cmd2 >= 2, <3'], setup_requires=['setuptools >= 42', 'setuptools_scm >= 3.4'], classifiers=[ @@ -43,11 +43,11 @@ 'Topic :: Software Development :: Libraries :: Python Modules', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], # dependencies for development and testing # $ pip install -e .[dev] diff --git a/plugins/template/README.md b/plugins/template/README.md index aba5f9146..81e1d781d 100644 --- a/plugins/template/README.md +++ b/plugins/template/README.md @@ -231,15 +231,15 @@ automates the creation of these environments. If you prefer to create these virtualenvs by hand, do the following: ``` $ cd cmd2_abbrev -$ pyenv install 3.8.5 -$ pyenv virtualenv -p python3.8 3.8.5 cmd2-3.8 -$ pyenv install 3.9.0 -$ pyenv virtualenv -p python3.9 3.9.0 cmd2-3.9 +$ pyenv install 3.12 +$ pyenv virtualenv -p python3.12 3.12 cmd2-3.12 +$ pyenv install 3.13 +$ pyenv virtualenv -p python3.13 3.13 cmd2-3.13 ``` Now set pyenv to make both of those available at the same time: ``` -$ pyenv local cmd2-3.8 cmd2-3.9 +$ pyenv local cmd2-3.13 cmd2-3.13 ``` Whether you ran the script, or did it by hand, you now have isolated virtualenvs @@ -247,12 +247,12 @@ for each of the major python versions. This table shows various python commands, the version of python which will be executed, and the virtualenv it will utilize. -| Command | python | virtualenv | -| ----------- | ------ | ---------- | -| `python3.8` | 3.8.5 | cmd2-3.8 | -| `python3.9` | 3.9.0 | cmd2-3.9 | -| `pip3.8` | 3.8.5 | cmd2-3.8 | -| `pip3.9` | 3.9.0 | cmd2-3.9 | +| Command | python | virtualenv | +|--------------|--------|------------| +| `python3.12` | 3.12 | cmd2-3.12 | +| `python3.13` | 3.13 | cmd2-3.13 | +| `pip3.12` | 3.12 | cmd2-3.12 | +| `pip3.13` | 3.13 | cmd2-3.13 | ## Install Dependencies @@ -264,11 +264,11 @@ $ pip install -e .[dev] This command also installs `cmd2-myplugin` "in-place", so the package points to the source code instead of copying files to the python `site-packages` folder. -All the dependencies now have been installed in the `cmd2-3.8` +All the dependencies now have been installed in the `cmd2-3.13` virtualenv. If you want to work in other virtualenvs, you'll need to manually select it, and install again:: - $ pyenv shell cmd2-3.4 + $ pyenv shell cmd2-3.13 $ pip install -e .[dev] Now that you have your python environments created, you need to install the @@ -286,8 +286,8 @@ unit tests found in the `tests` directory. ### Use nox to run unit tests in multiple versions of python -The included `noxfile.py` is setup to run the unit tests in python 3.8, 3.9 -3.10, 3.11, and 3.12 You can run your unit tests in all of these versions +The included `noxfile.py` is setup to run the unit tests in python 3.9 +3.10, 3.11, 3.12, and 3.13 You can run your unit tests in all of these versions of python by: ``` $ nox diff --git a/plugins/template/build-pyenvs.sh b/plugins/template/build-pyenvs.sh index 4a6e15789..4b515bbf5 100644 --- a/plugins/template/build-pyenvs.sh +++ b/plugins/template/build-pyenvs.sh @@ -8,7 +8,7 @@ # version numbers are: major.minor.patch # # this script will delete and recreate existing virtualenvs named -# cmd2-3.8, etc. It will also create a .python-version +# cmd2-3.9, etc. It will also create a .python-version # # Prerequisites: # - *nix-ish environment like macOS or Linux @@ -23,7 +23,7 @@ # virtualenvs will be added to '.python-version'. Feel free to modify # this list, but note that this script intentionally won't install # dev, rc, or beta python releases -declare -a pythons=("3.8" "3.9" "3.10" "3.11", "3.12") +declare -a pythons=("3.9", "3.10", "3.11", "3.12", "3.13") # function to find the latest patch of a minor version of python function find_latest_version { diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py index 25a95067a..d8aa344bf 100644 --- a/plugins/template/noxfile.py +++ b/plugins/template/noxfile.py @@ -1,7 +1,7 @@ import nox -@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12']) +@nox.session(python=['3.9', '3.10', '3.11', '3.12', '3.13']) def tests(session): session.install('invoke', './[test]') session.run('invoke', 'pytest', '--junit', '--no-pty') diff --git a/plugins/template/setup.py b/plugins/template/setup.py index 7e872cd8d..60028c115 100644 --- a/plugins/template/setup.py +++ b/plugins/template/setup.py @@ -24,7 +24,7 @@ url='https://github.com/python-cmd2/cmd2-plugin-template', license='MIT', packages=['cmd2_myplugin'], - python_requires='>=3.8', + python_requires='>=3.9', install_requires=['cmd2 >= 2, <3'], setup_requires=['setuptools_scm'], classifiers=[ @@ -34,11 +34,11 @@ 'Topic :: Software Development :: Libraries :: Python Modules', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], # dependencies for development and testing # $ pip install -e .[dev] diff --git a/pyproject.toml b/pyproject.toml index d373e593e..163f42935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python" authors = [{ name = "cmd2 Contributors" }] readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = [ "CLI", "cmd", @@ -26,7 +26,6 @@ classifiers = [ "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 57ffd7b51a891c9d34a29a4af362eb4bb6ca9646 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 10 Nov 2024 13:50:59 -0500 Subject: [PATCH 09/12] Use type hinting generics in standard collections instead of importing from typing (#1377) --- cmd2/__init__.py | 4 +- cmd2/ansi.py | 9 +- cmd2/argparse_completer.py | 40 ++-- cmd2/argparse_custom.py | 48 +++-- cmd2/cmd2.py | 176 +++++++++--------- cmd2/command_definition.py | 3 +- cmd2/decorators.py | 23 +-- cmd2/history.py | 16 +- cmd2/parsing.py | 35 ++-- cmd2/py_bridge.py | 5 +- cmd2/table_creator.py | 18 +- cmd2/transcript.py | 6 +- cmd2/utils.py | 32 ++-- examples/argparse_completion.py | 12 +- examples/async_printing.py | 5 +- examples/basic_completion.py | 11 +- examples/decorator_example.py | 5 +- examples/exit_code.py | 6 +- examples/hooks.py | 5 +- examples/modular_commands/commandset_basic.py | 14 +- .../modular_commands/commandset_complex.py | 9 +- examples/modular_commands_main.py | 3 +- examples/paged_output.py | 7 +- examples/read_input.py | 6 +- examples/scripts/save_help_text.py | 7 +- examples/table_creation.py | 25 ++- pyproject.toml | 2 +- tests/conftest.py | 5 +- tests/test_argparse_completer.py | 32 ++-- tests/test_utils.py | 6 +- tests_isolated/test_commandset/conftest.py | 3 +- .../test_commandset/test_commandset.py | 15 +- 32 files changed, 255 insertions(+), 338 deletions(-) diff --git a/cmd2/__init__.py b/cmd2/__init__.py index abd6d59d5..4b9f65ea9 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -11,8 +11,6 @@ # package is not installed pass -from typing import List - from . import plugin from .ansi import ( Bg, @@ -64,7 +62,7 @@ categorize, ) -__all__: List[str] = [ +__all__: list[str] = [ 'COMMAND_NAME', 'DEFAULT_SHORTCUTS', # ANSI Exports diff --git a/cmd2/ansi.py b/cmd2/ansi.py index c0a3a2f1c..772e9fff6 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -12,7 +12,6 @@ from typing import ( IO, Any, - List, Optional, cast, ) @@ -992,11 +991,11 @@ def style( :raises: TypeError if bg isn't None or a subclass of BgColor :return: the stylized string """ - # List of strings that add style - additions: List[AnsiSequence] = [] + # list of strings that add style + additions: list[AnsiSequence] = [] - # List of strings that remove style - removals: List[AnsiSequence] = [] + # list of strings that remove style + removals: list[AnsiSequence] = [] # Process the style settings if fg is not None: diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 1e4cd8b45..739f2e0d6 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -14,8 +14,6 @@ ) from typing import ( TYPE_CHECKING, - Dict, - List, Optional, Type, Union, @@ -172,7 +170,7 @@ class ArgparseCompleter: """Automatic command line tab completion based on argparse parameters""" def __init__( - self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[Dict[str, List[str]]] = None + self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[dict[str, list[str]]] = None ) -> None: """ Create an ArgparseCompleter @@ -213,8 +211,8 @@ def __init__( self._subcommand_action = action def complete( - self, text: str, line: str, begidx: int, endidx: int, tokens: List[str], *, cmd_set: Optional[CommandSet] = None - ) -> List[str]: + self, text: str, line: str, begidx: int, endidx: int, tokens: list[str], *, cmd_set: Optional[CommandSet] = None + ) -> list[str]: """ Complete text using argparse metadata @@ -245,13 +243,13 @@ def complete( flag_arg_state: Optional[_ArgumentState] = None # Non-reusable flags that we've parsed - matched_flags: List[str] = [] + matched_flags: list[str] = [] # Keeps track of arguments we've seen and any tokens they consumed - consumed_arg_values: Dict[str, List[str]] = dict() # dict(arg_name -> List[tokens]) + consumed_arg_values: dict[str, list[str]] = dict() # dict(arg_name -> l[tokens]) # Completed mutually exclusive groups - completed_mutex_groups: Dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict() + completed_mutex_groups: dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict() def consume_argument(arg_state: _ArgumentState) -> None: """Consuming token as an argument""" @@ -507,7 +505,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: return completion_results - def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]: + def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: list[str]) -> list[str]: """Tab completion routine for a parsers unused flags""" # Build a list of flags that can be tab completed @@ -524,7 +522,7 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche matches = self._cmd2_app.basic_complete(text, line, begidx, endidx, match_against) # Build a dictionary linking actions with their matched flag names - matched_actions: Dict[argparse.Action, List[str]] = dict() + matched_actions: dict[argparse.Action, list[str]] = dict() for flag in matches: action = self._flag_to_action[flag] matched_actions.setdefault(action, []) @@ -541,14 +539,14 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche return matches - def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]: + def _format_completions(self, arg_state: _ArgumentState, completions: Union[list[str], list[CompletionItem]]) -> list[str]: """Format CompletionItems into hint table""" # Nothing to do if we don't have at least 2 completions which are all CompletionItems if len(completions) < 2 or not all(isinstance(c, CompletionItem) for c in completions): - return cast(List[str], completions) + return cast(list[str], completions) - completion_items = cast(List[CompletionItem], completions) + completion_items = cast(list[CompletionItem], completions) # Check if the data being completed have a numerical type all_nums = all(isinstance(c.orig_value, numbers.Number) for c in completion_items) @@ -616,9 +614,9 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0) # Return sorted list of completions - return cast(List[str], completions) + return cast(list[str], completions) - def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]: + def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: list[str]) -> list[str]: """ Supports cmd2's help command in the completion of subcommand names :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -626,7 +624,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text :param tokens: arguments passed to command/subcommand - :return: List of subcommand completions + :return: list of subcommand completions """ # If our parser has subcommands, we must examine the tokens and check if they are subcommands # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter. @@ -645,7 +643,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in break return [] - def format_help(self, tokens: List[str]) -> str: + def format_help(self, tokens: list[str]) -> str: """ Supports cmd2's help command in the retrieval of help text :param tokens: arguments passed to help command @@ -672,17 +670,17 @@ def _complete_arg( begidx: int, endidx: int, arg_state: _ArgumentState, - consumed_arg_values: Dict[str, List[str]], + consumed_arg_values: dict[str, list[str]], *, cmd_set: Optional[CommandSet] = None, - ) -> List[str]: + ) -> list[str]: """ Tab completion routine for an argparse argument :return: list of completions :raises: CompletionError if the completer or choices function this calls raises one """ # Check if the arg provides choices to the user - arg_choices: Union[List[str], ChoicesCallable] + arg_choices: Union[list[str], ChoicesCallable] if arg_state.action.choices is not None: arg_choices = list(arg_state.action.choices) if not arg_choices: @@ -739,7 +737,7 @@ def _complete_arg( # Otherwise use basic_complete on the choices else: # Check if the choices come from a function - completion_items: List[str] = [] + completion_items: list[str] = [] if isinstance(arg_choices, ChoicesCallable): if not arg_choices.is_completer: choices_func = arg_choices.choices_provider diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 010776c54..774c6f6ed 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -250,15 +250,11 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) TYPE_CHECKING, Any, Callable, - Dict, Iterable, - List, NoReturn, Optional, Protocol, Sequence, - Set, - Tuple, Type, Union, cast, @@ -350,7 +346,7 @@ class ChoicesProviderFuncBase(Protocol): Function that returns a list of choices in support of tab completion """ - def __call__(self) -> List[str]: ... # pragma: no cover + def __call__(self) -> list[str]: ... # pragma: no cover @runtime_checkable @@ -359,7 +355,7 @@ class ChoicesProviderFuncWithTokens(Protocol): Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments. """ - def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]: ... # pragma: no cover + def __call__(self, *, arg_tokens: dict[str, list[str]] = {}) -> list[str]: ... # pragma: no cover ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens] @@ -377,7 +373,7 @@ def __call__( line: str, begidx: int, endidx: int, - ) -> List[str]: ... # pragma: no cover + ) -> list[str]: ... # pragma: no cover @runtime_checkable @@ -394,8 +390,8 @@ def __call__( begidx: int, endidx: int, *, - arg_tokens: Dict[str, List[str]] = {}, - ) -> List[str]: ... # pragma: no cover + arg_tokens: dict[str, list[str]] = {}, + ) -> list[str]: ... # pragma: no cover CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens] @@ -598,7 +594,7 @@ def _action_set_descriptive_header(self: argparse.Action, descriptive_header: Op ############################################################################################################ # Patch argparse.Action with accessors for nargs_range attribute ############################################################################################################ -def _action_get_nargs_range(self: argparse.Action) -> Optional[Tuple[int, Union[int, float]]]: +def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[int, float]]]: """ Get the nargs_range attribute of an argparse Action. @@ -609,13 +605,13 @@ def _action_get_nargs_range(self: argparse.Action) -> Optional[Tuple[int, Union[ :param self: argparse Action being queried :return: The value of nargs_range or None if attribute does not exist """ - return cast(Optional[Tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None)) + return cast(Optional[tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None)) setattr(argparse.Action, 'get_nargs_range', _action_get_nargs_range) -def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[Tuple[int, Union[int, float]]]) -> None: +def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[tuple[int, Union[int, float]]]) -> None: """ Set the nargs_range attribute of an argparse Action. @@ -673,7 +669,7 @@ def _action_set_suppress_tab_hint(self: argparse.Action, suppress_tab_hint: bool # Allow developers to add custom action attributes ############################################################################################################ -CUSTOM_ACTION_ATTRIBS: Set[str] = set() +CUSTOM_ACTION_ATTRIBS: set[str] = set() _CUSTOM_ATTRIB_PFX = '_attr_' @@ -746,7 +742,7 @@ def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None: def _add_argument_wrapper( self: argparse._ActionsContainer, *args: Any, - nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None, + nargs: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None] = None, choices_provider: Optional[ChoicesProviderFunc] = None, completer: Optional[CompleterFunc] = None, suppress_tab_hint: bool = False, @@ -797,7 +793,7 @@ def _add_argument_wrapper( nargs_range = None if nargs is not None: - nargs_adjusted: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] + nargs_adjusted: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None] # Check if nargs was given as a range if isinstance(nargs, tuple): # Handle 1-item tuple by setting max to INFINITY @@ -847,7 +843,7 @@ def _add_argument_wrapper( kwargs['nargs'] = nargs_adjusted # Extract registered custom keyword arguments - custom_attribs: Dict[str, Any] = {} + custom_attribs: dict[str, Any] = {} for keyword, value in kwargs.items(): if keyword in CUSTOM_ACTION_ATTRIBS: custom_attribs[keyword] = value @@ -1124,9 +1120,9 @@ def _format_usage( # End cmd2 customization # helper for wrapping lines - def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None) -> List[str]: - lines: List[str] = [] - line: List[str] = [] + def get_lines(parts: list[str], indent: str, prefix: Optional[str] = None) -> list[str]: + lines: list[str] = [] + line: list[str] = [] if prefix is not None: line_len = len(prefix) - 1 else: @@ -1188,7 +1184,7 @@ def _format_action_invocation(self, action: argparse.Action) -> str: return metavar else: - parts: List[str] = [] + parts: list[str] = [] # if the Optional doesn't take a value, format is: # -s, --long @@ -1209,8 +1205,8 @@ def _format_action_invocation(self, action: argparse.Action) -> str: def _determine_metavar( self, action: argparse.Action, - default_metavar: Union[str, Tuple[str, ...]], - ) -> Union[str, Tuple[str, ...]]: + default_metavar: Union[str, tuple[str, ...]], + ) -> Union[str, tuple[str, ...]]: """Custom method to determine what to use as the metavar value of an action""" if action.metavar is not None: result = action.metavar @@ -1226,11 +1222,11 @@ def _determine_metavar( def _metavar_formatter( self, action: argparse.Action, - default_metavar: Union[str, Tuple[str, ...]], - ) -> Callable[[int], Tuple[str, ...]]: + default_metavar: Union[str, tuple[str, ...]], + ) -> Callable[[int], tuple[str, ...]]: metavar = self._determine_metavar(action, default_metavar) - def format(tuple_size: int) -> Tuple[str, ...]: + def format(tuple_size: int) -> tuple[str, ...]: if isinstance(metavar, tuple): return metavar else: @@ -1238,7 +1234,7 @@ def format(tuple_size: int) -> Tuple[str, ...]: return format - def _format_args(self, action: argparse.Action, default_metavar: Union[str, Tuple[str, ...]]) -> str: + def _format_args(self, action: argparse.Action, default_metavar: Union[str, tuple[str, ...]]) -> str: """Customized to handle ranged nargs and make other output less verbose""" metavar = self._determine_metavar(action, default_metavar) metavar_formatter = self._metavar_formatter(action, default_metavar) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 5cbef6cb8..7661b0ba7 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -59,14 +59,10 @@ TYPE_CHECKING, Any, Callable, - Dict, Iterable, - List, Mapping, Optional, - Set, TextIO, - Tuple, Type, TypeVar, Union, @@ -199,7 +195,7 @@ class _SavedCmd2Env: def __init__(self) -> None: self.readline_settings = _SavedReadlineSettings() self.readline_module: Optional[ModuleType] = None - self.history: List[str] = [] + self.history: list[str] = [] self.sys_stdout: Optional[TextIO] = None self.sys_stdin: Optional[TextIO] = None @@ -244,11 +240,11 @@ def __init__( include_py: bool = False, include_ipy: bool = False, allow_cli_args: bool = True, - transcript_files: Optional[List[str]] = None, + transcript_files: Optional[list[str]] = None, allow_redirection: bool = True, - multiline_commands: Optional[List[str]] = None, - terminators: Optional[List[str]] = None, - shortcuts: Optional[Dict[str, str]] = None, + multiline_commands: Optional[list[str]] = None, + terminators: Optional[list[str]] = None, + shortcuts: Optional[dict[str, str]] = None, command_sets: Optional[Iterable[CommandSet]] = None, auto_load_commands: bool = True, allow_clipboard: bool = True, @@ -338,12 +334,12 @@ def __init__( self.max_completion_items = 50 # A dictionary mapping settable names to their Settable instance - self._settables: Dict[str, Settable] = dict() + self._settables: dict[str, Settable] = dict() self._always_prefix_settables: bool = False # CommandSet containers - self._installed_command_sets: Set[CommandSet] = set() - self._cmd_to_command_sets: Dict[str, CommandSet] = {} + self._installed_command_sets: set[CommandSet] = set() + self._cmd_to_command_sets: dict[str, CommandSet] = {} self.build_settables() @@ -365,13 +361,13 @@ def __init__( self.exclude_from_history = ['eof', 'history'] # Keeps track of typed command history in the Python shell - self._py_history: List[str] = [] + self._py_history: list[str] = [] # The name by which Python environments refer to the PyBridge to call app commands self.py_bridge_name = 'app' # Defines app-specific variables/functions available in Python shells and pyscripts - self.py_locals: Dict[str, Any] = dict() + self.py_locals: dict[str, Any] = dict() # True if running inside a Python shell or pyscript, False otherwise self._in_py = False @@ -384,7 +380,7 @@ def __init__( self.last_result: Any = None # Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command - self._script_dir: List[str] = [] + self._script_dir: list[str] = [] # Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt self.sigint_protection = utils.ContextFlag() @@ -415,7 +411,7 @@ def __init__( self.broken_pipe_warning = '' # Commands that will run at the beginning of the command loop - self._startup_commands: List[str] = [] + self._startup_commands: list[str] = [] # If a startup script is provided and exists, then execute it in the startup commands if startup_script: @@ -427,7 +423,7 @@ def __init__( self._startup_commands.append(script_cmd) # Transcript files to run instead of interactive command loop - self._transcript_files: Optional[List[str]] = None + self._transcript_files: Optional[list[str]] = None # Check for command line args if allow_cli_args: @@ -470,7 +466,7 @@ def __init__( # Commands that have been disabled from use. This is to support commands that are only available # during specific states of the application. This dictionary's keys are the command names and its # values are DisabledCommand objects. - self.disabled_commands: Dict[str, DisabledCommand] = dict() + self.disabled_commands: dict[str, DisabledCommand] = dict() # If any command has been categorized, then all other commands that haven't been categorized # will display under this section in the help output. @@ -508,7 +504,7 @@ def __init__( self.formatted_completions = '' # Used by complete() for readline tab completion - self.completion_matches: List[str] = [] + self.completion_matches: list[str] = [] # Use this list if you need to display tab completion suggestions that are different than the actual text # of the matches. For instance, if you are completing strings that contain a common delimiter and you only @@ -516,7 +512,7 @@ def __init__( # still must be returned from your completer function. For an example, look at path_complete() which # uses this to show only the basename of paths as the suggestions. delimiter_complete() also populates # this list. These are ignored if self.formatted_completions is populated. - self.display_matches: List[str] = [] + self.display_matches: list[str] = [] # Used by functions like path_complete() and delimiter_complete() to properly # quote matches that are completed in a delimited fashion @@ -528,7 +524,7 @@ def __init__( self.matches_sorted = False # Command parsers for this Cmd instance. - self._command_parsers: Dict[str, argparse.ArgumentParser] = {} + self._command_parsers: dict[str, argparse.ArgumentParser] = {} # Locates the command parser template or factory and creates an instance-specific parser for command in self.get_all_commands(): @@ -562,7 +558,7 @@ def __init__( # the current command being executed self.current_command: Optional[Statement] = None - def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]: + def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]: """ Find all CommandSets that match the provided CommandSet type. By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that @@ -591,7 +587,7 @@ def _autoload_commands(self) -> None: all_commandset_defs = CommandSet.__subclasses__() existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets] - def load_commandset_by_type(commandset_types: List[Type[CommandSet]]) -> None: + def load_commandset_by_type(commandset_types: list[Type[CommandSet]]) -> None: for cmdset_type in commandset_types: # check if the type has sub-classes. We will only auto-load leaf class types. subclasses = cmdset_type.__subclasses__() @@ -635,7 +631,7 @@ def register_command_set(self, cmdset: CommandSet) -> None: cmdset.on_register(self) methods = cast( - List[Tuple[str, Callable[..., Any]]], + list[tuple[str, Callable[..., Any]]], inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] @@ -776,7 +772,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: cmdset.on_unregister() self._unregister_subcommands(cmdset) - methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( + methods: list[tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') @@ -807,7 +803,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: self._installed_command_sets.remove(cmdset) def _check_uninstallable(self, cmdset: CommandSet) -> None: - methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( + methods: list[tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') @@ -881,7 +877,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}" ) - def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser: + def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser: if not subcmd_names: return action cur_subcmd = subcmd_names.pop(0) @@ -1059,7 +1055,7 @@ def remove_settable(self, name: str) -> None: def build_settables(self) -> None: """Create the dictionary of user-settable parameters""" - def get_allow_style_choices(cli_self: Cmd) -> List[str]: + def get_allow_style_choices(cli_self: Cmd) -> list[str]: """Used to tab complete allow_style values""" return [val.name.lower() for val in ansi.AllowStyle] @@ -1383,7 +1379,7 @@ def _reset_completion_defaults(self) -> None: elif rl_type == RlType.PYREADLINE: readline.rl.mode._display_completions = self._display_matches_pyreadline - def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]: + def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[list[str], list[str]]: """Used by tab completion functions to get all tokens through the one being completed. :param line: the current input line with leading whitespace removed @@ -1454,7 +1450,7 @@ def basic_complete( begidx: int, endidx: int, match_against: Iterable[str], - ) -> List[str]: + ) -> list[str]: """ Basic tab completion function that matches against a list of strings without considering line contents or cursor position. The args required by this function are defined in the header of Python's cmd.py. @@ -1476,7 +1472,7 @@ def delimiter_complete( endidx: int, match_against: Iterable[str], delimiter: str, - ) -> List[str]: + ) -> list[str]: """ Performs tab completion against a list but each match is split on a delimiter and only the portion of the match being tab completed is shown as the completion suggestions. @@ -1542,10 +1538,10 @@ def flag_based_complete( line: str, begidx: int, endidx: int, - flag_dict: Dict[str, Union[Iterable[str], CompleterFunc]], + flag_dict: dict[str, Union[Iterable[str], CompleterFunc]], *, all_else: Union[None, Iterable[str], CompleterFunc] = None, - ) -> List[str]: + ) -> list[str]: """Tab completes based on a particular flag preceding the token being completed. :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1594,7 +1590,7 @@ def index_based_complete( index_dict: Mapping[int, Union[Iterable[str], CompleterFunc]], *, all_else: Optional[Union[Iterable[str], CompleterFunc]] = None, - ) -> List[str]: + ) -> list[str]: """Tab completes based on a fixed position in the input string. :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1639,7 +1635,7 @@ def index_based_complete( def path_complete( self, text: str, line: str, begidx: int, endidx: int, *, path_filter: Optional[Callable[[str], bool]] = None - ) -> List[str]: + ) -> list[str]: """Performs completion of local file system paths :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1653,7 +1649,7 @@ def path_complete( """ # Used to complete ~ and ~user strings - def complete_users() -> List[str]: + def complete_users() -> list[str]: users = [] # Windows lacks the pwd module so we can't get a list of users. @@ -1780,7 +1776,7 @@ def complete_users() -> List[str]: return matches - def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> List[str]: + def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> list[str]: """Performs completion of executables either in a user's path or a given path :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1805,7 +1801,7 @@ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, text, line, begidx, endidx, path_filter=lambda path: os.path.isdir(path) or os.access(path, os.X_OK) ) - def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> List[str]: + def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> list[str]: """Called by complete() as the first tab completion function for all commands It determines if it should tab complete for redirection (|, >, >>) or use the completer function for the current command @@ -1886,7 +1882,7 @@ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, com return compfunc(text, line, begidx, endidx) @staticmethod - def _pad_matches_to_display(matches_to_display: List[str]) -> Tuple[List[str], int]: # pragma: no cover + def _pad_matches_to_display(matches_to_display: list[str]) -> tuple[list[str], int]: # pragma: no cover """Adds padding to the matches being displayed as tab completion suggestions. The default padding of readline/pyreadine is small and not visually appealing especially if matches have spaces. It appears very squished together. @@ -1908,7 +1904,7 @@ def _pad_matches_to_display(matches_to_display: List[str]) -> Tuple[List[str], i return [cur_match + padding for cur_match in matches_to_display], len(padding) def _display_matches_gnu_readline( - self, substitution: str, matches: List[str], longest_match_length: int + self, substitution: str, matches: list[str], longest_match_length: int ) -> None: # pragma: no cover """Prints a match list using GNU readline's rl_display_match_list() @@ -1956,7 +1952,7 @@ def _display_matches_gnu_readline( # rl_display_match_list() expects matches to be in argv format where # substitution is the first element, followed by the matches, and then a NULL. - strings_array = cast(List[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()) + strings_array = cast(list[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()) # Copy in the encoded strings and add a NULL to the end strings_array[0] = encoded_substitution @@ -1969,7 +1965,7 @@ def _display_matches_gnu_readline( # Redraw prompt and input line rl_force_redisplay() - def _display_matches_pyreadline(self, matches: List[str]) -> None: # pragma: no cover + def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no cover """Prints a match list using pyreadline3's _display_completions() :param matches: the tab completion matches to display @@ -2312,15 +2308,15 @@ def in_pyscript(self) -> bool: return self._in_py @property - def aliases(self) -> Dict[str, str]: + def aliases(self) -> dict[str, str]: """Read-only property to access the aliases stored in the StatementParser""" return self.statement_parser.aliases - def get_names(self) -> List[str]: + def get_names(self) -> list[str]: """Return an alphabetized list of names comprising the attributes of the cmd2 class instance.""" return dir(self) - def get_all_commands(self) -> List[str]: + def get_all_commands(self) -> list[str]: """Return a list of all commands""" return [ name[len(constants.COMMAND_FUNC_PREFIX) :] @@ -2328,7 +2324,7 @@ def get_all_commands(self) -> List[str]: if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name)) ] - def get_visible_commands(self) -> List[str]: + def get_visible_commands(self) -> list[str]: """Return a list of commands that have not been hidden or disabled""" return [ command @@ -2339,9 +2335,9 @@ def get_visible_commands(self) -> List[str]: # Table displayed when tab completing aliases _alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None) - def _get_alias_completion_items(self) -> List[CompletionItem]: + def _get_alias_completion_items(self) -> list[CompletionItem]: """Return list of alias names and values as CompletionItems""" - results: List[CompletionItem] = [] + results: list[CompletionItem] = [] for cur_key in self.aliases: row_data = [self.aliases[cur_key]] @@ -2352,9 +2348,9 @@ def _get_alias_completion_items(self) -> List[CompletionItem]: # Table displayed when tab completing Settables _settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None) - def _get_settable_completion_items(self) -> List[CompletionItem]: + def _get_settable_completion_items(self) -> list[CompletionItem]: """Return list of Settable names, values, and descriptions as CompletionItems""" - results: List[CompletionItem] = [] + results: list[CompletionItem] = [] for cur_key in self.settables: row_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description] @@ -2362,13 +2358,13 @@ def _get_settable_completion_items(self) -> List[CompletionItem]: return results - def _get_commands_and_aliases_for_completion(self) -> List[str]: + def _get_commands_and_aliases_for_completion(self) -> list[str]: """Return a list of visible commands and aliases for tab completion""" visible_commands = set(self.get_visible_commands()) alias_names = set(self.aliases) return list(visible_commands | alias_names) - def get_help_topics(self) -> List[str]: + def get_help_topics(self) -> list[str]: """Return a list of help topics""" all_topics = [ name[len(constants.HELP_FUNC_PREFIX) :] @@ -2471,7 +2467,7 @@ def postloop(self) -> None: """ pass - def parseline(self, line: str) -> Tuple[str, str, str]: + def parseline(self, line: str) -> tuple[str, str, str]: """Parse the line into a command name and a string containing the arguments. NOTE: This is an override of a parent class method. It is only used by other parent class methods. @@ -2634,7 +2630,7 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) def runcmds_plus_hooks( self, - cmds: Union[List[HistoryItem], List[str]], + cmds: Union[list[HistoryItem], list[str]], *, add_to_history: bool = True, stop_on_keyboard_interrupt: bool = False, @@ -2793,7 +2789,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: # Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs, # our sigint handler will forward it only to the most recent pipe process. This makes sure pipe # processes close in the right order (most recent first). - kwargs: Dict[str, Any] = dict() + kwargs: dict[str, Any] = dict() if sys.platform == 'win32': kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP else: @@ -2992,7 +2988,7 @@ def read_input( self, prompt: str, *, - history: Optional[List[str]] = None, + history: Optional[list[str]] = None, completion_mode: utils.CompletionMode = utils.CompletionMode.NONE, preserve_quotes: bool = False, choices: Optional[Iterable[Any]] = None, @@ -3033,7 +3029,7 @@ def read_input( """ readline_configured = False saved_completer: Optional[CompleterFunc] = None - saved_history: Optional[List[str]] = None + saved_history: Optional[list[str]] = None def configure_readline() -> None: """Configure readline tab completion and history""" @@ -3419,7 +3415,7 @@ def _build_alias_list_parser() -> Cmd2ArgumentParser: @as_subcommand_to('alias', 'list', _build_alias_list_parser, help="list aliases") def _alias_list(self, args: argparse.Namespace) -> None: """List some or all aliases as 'alias create' commands.""" - self.last_result = {} # Dict[alias_name, alias_value] + self.last_result = {} # dict[alias_name, alias_value] tokens_to_quote = constants.REDIRECTION_TOKENS tokens_to_quote.extend(self.statement_parser.terminators) @@ -3429,7 +3425,7 @@ def _alias_list(self, args: argparse.Namespace) -> None: else: to_list = sorted(self.aliases, key=self.default_sort_key) - not_found: List[str] = [] + not_found: list[str] = [] for name in to_list: if name not in self.aliases: not_found.append(name) @@ -3451,7 +3447,7 @@ def _alias_list(self, args: argparse.Namespace) -> None: for name in not_found: self.perror(f"Alias '{name}' not found") - def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: """Completes the command argument of help""" # Complete token against topics and visible commands @@ -3461,8 +3457,8 @@ def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) return self.basic_complete(text, line, begidx, endidx, strs_to_match) def complete_help_subcommands( - self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]] - ) -> List[str]: + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]] + ) -> list[str]: """Completes the subcommands argument of help""" # Make sure we have a command whose subcommands we will complete @@ -3545,7 +3541,7 @@ def do_help(self, args: argparse.Namespace) -> None: self.perror(err_msg, apply_style=False) self.last_result = False - def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxcol: int) -> None: + def print_topics(self, header: str, cmds: Optional[list[str]], cmdlen: int, maxcol: int) -> None: """ Print groups of commands and topics in columns and an optional header Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters @@ -3563,7 +3559,7 @@ def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxc self.columnize(cmds, maxcol - 1) self.poutput() - def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> None: + def columnize(self, str_list: Optional[list[str]], display_width: int = 80) -> None: """Display a list of single-line strings as a compact set of columns. Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters @@ -3639,14 +3635,14 @@ def _help_menu(self, verbose: bool = False) -> None: self.print_topics(self.misc_header, help_topics, 15, 80) self.print_topics(self.undoc_header, cmds_undoc, 15, 80) - def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str], List[str]]: + def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str], list[str]]: # Get a sorted list of help topics help_topics = sorted(self.get_help_topics(), key=self.default_sort_key) # Get a sorted list of visible command names visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key) - cmds_doc: List[str] = [] - cmds_undoc: List[str] = [] - cmds_cats: Dict[str, List[str]] = {} + cmds_doc: list[str] = [] + cmds_undoc: list[str] = [] + cmds_cats: dict[str, list[str]] = {} for command in visible_commands: func = self.cmd_func(command) has_help_func = False @@ -3669,7 +3665,7 @@ def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str cmds_undoc.append(command) return cmds_cats, cmds_doc, cmds_undoc, help_topics - def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None: + def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None: """Customized version of print_topics that can switch between verbose or traditional output""" import io @@ -3776,7 +3772,7 @@ def do_quit(self, _: argparse.Namespace) -> Optional[bool]: self.last_result = True return True - def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any: + def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any: """Presents a numbered menu to the user. Modeled after the bash shell's SELECT. Returns the item chosen. @@ -3787,12 +3783,12 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p | a list of tuples -> interpreted as (value, text), so that the return value can differ from the text advertised to the user""" - local_opts: Union[List[str], List[Tuple[Any, Optional[str]]]] + local_opts: Union[list[str], list[tuple[Any, Optional[str]]]] if isinstance(opts, str): - local_opts = cast(List[Tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split()))) + local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split()))) else: local_opts = opts - fulloptions: List[Tuple[Any, Optional[str]]] = [] + fulloptions: list[tuple[Any, Optional[str]]] = [] for opt in local_opts: if isinstance(opt, str): fulloptions.append((opt, opt)) @@ -3826,8 +3822,8 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p self.poutput(f"'{response}' isn't a valid choice. Pick a number between 1 and {len(fulloptions)}:") def complete_set_value( - self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]] - ) -> List[str]: + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]] + ) -> list[str]: """Completes the value argument of set""" param = arg_tokens['param'][0] try: @@ -3933,7 +3929,7 @@ def do_set(self, args: argparse.Namespace) -> None: max_name_width = max([ansi.style_aware_wcswidth(param) for param in to_show]) max_name_width = max(max_name_width, ansi.style_aware_wcswidth(name_label)) - cols: List[Column] = [ + cols: list[Column] = [ Column(name_label, width=max_name_width), Column('Value', width=30), Column('Description', width=60), @@ -3943,7 +3939,7 @@ def do_set(self, args: argparse.Namespace) -> None: self.poutput(table.generate_header()) # Build the table and populate self.last_result - self.last_result = {} # Dict[settable_name, settable_value] + self.last_result = {} # dict[settable_name, settable_value] for param in sorted(to_show, key=self.default_sort_key): settable = self.settables[param] @@ -3968,7 +3964,7 @@ def do_shell(self, args: argparse.Namespace) -> None: import signal import subprocess - kwargs: Dict[str, Any] = dict() + kwargs: dict[str, Any] = dict() # Set OS-specific parameters if sys.platform.startswith('win'): @@ -4555,7 +4551,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]: self.last_result = history return None - def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryItem]': + def _get_history(self, args: argparse.Namespace) -> OrderedDict[int, HistoryItem]: """If an argument was supplied, then retrieve partial contents of the history; otherwise retrieve entire history. This function returns a dictionary with history items keyed by their 1-based index in ascending order. @@ -4633,11 +4629,11 @@ def _initialize_history(self, hist_file: str) -> None: try: import lzma as decompress_lib - decompress_exceptions: Tuple[type[Exception]] = (decompress_lib.LZMAError,) + decompress_exceptions: tuple[type[Exception]] = (decompress_lib.LZMAError,) except ModuleNotFoundError: # pragma: no cover import bz2 as decompress_lib # type: ignore[no-redef] - decompress_exceptions: Tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef] + decompress_exceptions: tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef] try: history_json = decompress_lib.decompress(compressed_bytes).decode(encoding='utf-8') @@ -4694,7 +4690,7 @@ def _persist_history(self) -> None: def _generate_transcript( self, - history: Union[List[HistoryItem], List[str]], + history: Union[list[HistoryItem], list[str]], transcript_file: str, *, add_to_history: bool = True, @@ -4968,7 +4964,7 @@ def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]: # self.last_result will be set by do_run_script() return self.do_run_script(utils.quote_string(relative_path)) - def _run_transcript_tests(self, transcript_paths: List[str]) -> None: + def _run_transcript_tests(self, transcript_paths: list[str]) -> None: """Runs transcript tests for provided file(s). This is called when either -t is provided on the command line or the transcript_files argument is provided @@ -5355,12 +5351,12 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override] ### def _initialize_plugin_system(self) -> None: """Initialize the plugin system""" - self._preloop_hooks: List[Callable[[], None]] = [] - self._postloop_hooks: List[Callable[[], None]] = [] - self._postparsing_hooks: List[Callable[[plugin.PostparsingData], plugin.PostparsingData]] = [] - self._precmd_hooks: List[Callable[[plugin.PrecommandData], plugin.PrecommandData]] = [] - self._postcmd_hooks: List[Callable[[plugin.PostcommandData], plugin.PostcommandData]] = [] - self._cmdfinalization_hooks: List[Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]] = [] + self._preloop_hooks: list[Callable[[], None]] = [] + self._postloop_hooks: list[Callable[[], None]] = [] + self._postparsing_hooks: list[Callable[[plugin.PostparsingData], plugin.PostparsingData]] = [] + self._precmd_hooks: list[Callable[[plugin.PrecommandData], plugin.PrecommandData]] = [] + self._postcmd_hooks: list[Callable[[plugin.PostcommandData], plugin.PostcommandData]] = [] + self._cmdfinalization_hooks: list[Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]] = [] @classmethod def _validate_callable_param_count(cls, func: Callable[..., Any], count: int) -> None: @@ -5494,7 +5490,7 @@ def _resolve_func_self( else: # Search all registered CommandSets func_self = None - candidate_sets: List[CommandSet] = [] + candidate_sets: list[CommandSet] = [] for installed_cmd_set in self._installed_command_sets: if type(installed_cmd_set) == func_class: # noqa: E721 # Case 2: CommandSet is an exact type match for the function's CommandSet diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 1c1ff0cf9..f6d69322c 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -6,7 +6,6 @@ from typing import ( TYPE_CHECKING, Callable, - Dict, Mapping, Optional, Type, @@ -97,7 +96,7 @@ def __init__(self) -> None: # accessed by child classes using the self._cmd property. self.__cmd_internal: Optional[cmd2.Cmd] = None - self._settables: Dict[str, Settable] = {} + self._settables: dict[str, Settable] = {} self._settable_prefix = self.__class__.__name__ @property diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 72a675f9b..da9a7ba26 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -6,11 +6,8 @@ TYPE_CHECKING, Any, Callable, - Dict, - List, Optional, Sequence, - Tuple, Type, TypeVar, Union, @@ -77,7 +74,7 @@ def cat_decorator(func: CommandFunc) -> CommandFunc: # in cmd2 command functions/callables. As long as the 2-ple of arguments we expect to be there can be # found we can swap out the statement with each decorator's specific parameters ########################## -def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Statement, str]]: +def _parse_positionals(args: tuple[Any, ...]) -> tuple['cmd2.Cmd', Union[Statement, str]]: """ Helper function for cmd2 decorators to inspect the positional arguments until the cmd2.Cmd argument is found Assumes that we will find cmd2.Cmd followed by the command statement object or string. @@ -101,7 +98,7 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme raise TypeError('Expected arguments: cmd: cmd2.Cmd, statement: Union[Statement, str] Not found') # pragma: no cover -def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> List[Any]: +def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> list[Any]: """ Helper function for cmd2 decorators to swap the Statement parameter with one or more decorator-specific parameters @@ -118,13 +115,13 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> #: Function signature for an Command Function that accepts a pre-processed argument list from user input #: and optionally returns a boolean -ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, List[str]], Optional[bool]] +ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, list[str]], Optional[bool]] #: Function signature for an Command Function that accepts a pre-processed argument list from user input #: and returns a boolean -ArgListCommandFuncBoolReturn = Callable[[CommandParent, List[str]], bool] +ArgListCommandFuncBoolReturn = Callable[[CommandParent, list[str]], bool] #: Function signature for an Command Function that accepts a pre-processed argument list from user input #: and returns Nothing -ArgListCommandFuncNoneReturn = Callable[[CommandParent, List[str]], None] +ArgListCommandFuncNoneReturn = Callable[[CommandParent, list[str]], None] #: Aggregate of all accepted function signatures for Command Functions that accept a pre-processed argument list ArgListCommandFunc = Union[ @@ -206,7 +203,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: """ # Set the prog value for this parser parser.prog = prog - req_args: List[str] = [] + req_args: list[str] = [] # Set the prog value for the parser's subcommands for action in parser._actions: @@ -330,7 +327,7 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOpt """ @functools.wraps(func) - def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]: + def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]: """ Command function wrapper which translates command line into argparse Namespace and calls actual command function @@ -361,7 +358,7 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]: namespace = ns_provider(provider_self if provider_self is not None else cmd2_app) try: - new_args: Union[Tuple[argparse.Namespace], Tuple[argparse.Namespace, List[str]]] + new_args: Union[tuple[argparse.Namespace], tuple[argparse.Namespace, list[str]]] if with_unknown_args: new_args = arg_parser.parse_known_args(parsed_arglist, namespace) else: @@ -406,7 +403,7 @@ def as_subcommand_to( ], *, help: Optional[str] = None, - aliases: Optional[List[str]] = None, + aliases: Optional[list[str]] = None, ) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: """ Tag this method as a subcommand to an existing argparse decorated command. @@ -428,7 +425,7 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> ArgparseCommandFu setattr(func, constants.SUBCMD_ATTR_NAME, subcommand) # Keyword arguments for subparsers.add_parser() - add_parser_kwargs: Dict[str, Any] = dict() + add_parser_kwargs: dict[str, Any] = dict() if help is not None: add_parser_kwargs['help'] = help if aliases: diff --git a/cmd2/history.py b/cmd2/history.py index ea157cd34..707ebd3e3 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -14,9 +14,7 @@ from typing import ( Any, Callable, - Dict, Iterable, - List, Optional, Union, overload, @@ -132,12 +130,12 @@ def pr(self, idx: int, script: bool = False, expanded: bool = False, verbose: bo return ret_str - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Utility method to convert this HistoryItem into a dictionary for use in persistent JSON history files""" return {HistoryItem._statement_field: self.statement.to_dict()} @staticmethod - def from_dict(source_dict: Dict[str, Any]) -> 'HistoryItem': + def from_dict(source_dict: dict[str, Any]) -> 'HistoryItem': """ Utility method to restore a HistoryItem from a dictionary @@ -149,7 +147,7 @@ def from_dict(source_dict: Dict[str, Any]) -> 'HistoryItem': return HistoryItem(Statement.from_dict(statement_dict)) -class History(List[HistoryItem]): +class History(list[HistoryItem]): """A list of :class:`~cmd2.history.HistoryItem` objects with additional methods for searching and managing the list. @@ -242,7 +240,7 @@ def get(self, index: int) -> HistoryItem: # spanpattern = re.compile(r'^\s*(?P-?[1-9]\d*)?(?P:|(\.{2,}))(?P-?[1-9]\d*)?\s*$') - def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': + def span(self, span: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]: """Return a slice of the History list :param span: string containing an index or a slice @@ -291,7 +289,7 @@ def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, return self._build_result_dictionary(start, end) - def str_search(self, search: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': + def str_search(self, search: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]: """Find history items which contain a given string :param search: the string to search for @@ -310,7 +308,7 @@ def isin(history_item: HistoryItem) -> bool: start = 0 if include_persisted else self.session_start_index return self._build_result_dictionary(start, len(self), isin) - def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': + def regex_search(self, regex: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]: """Find history items which match a given regular expression :param regex: the regular expression to search for. @@ -346,7 +344,7 @@ def truncate(self, max_length: int) -> None: def _build_result_dictionary( self, start: int, end: int, filter_func: Optional[Callable[[HistoryItem], bool]] = None - ) -> 'OrderedDict[int, HistoryItem]': + ) -> OrderedDict[int, HistoryItem]: """ Build history search results :param start: start index to search from diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 7e59a558d..b72fdf684 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -10,11 +10,8 @@ ) from typing import ( Any, - Dict, Iterable, - List, Optional, - Tuple, Union, ) @@ -27,7 +24,7 @@ ) -def shlex_split(str_to_split: str) -> List[str]: +def shlex_split(str_to_split: str) -> list[str]: """ A wrapper around shlex.split() that uses cmd2's preferred arguments. This allows other classes to easily call split() the same way StatementParser does. @@ -79,7 +76,7 @@ class Statement(str): command: str = '' # list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted - arg_list: List[str] = field(default_factory=list) + arg_list: list[str] = field(default_factory=list) # if the command is a multiline command, the name of the command, otherwise empty multiline_command: str = '' @@ -157,7 +154,7 @@ def expanded_command_line(self) -> str: return self.command_and_args + self.post_command @property - def argv(self) -> List[str]: + def argv(self) -> list[str]: """a list of arguments a-la ``sys.argv``. The first element of the list is the command after shortcut expansion. @@ -176,12 +173,12 @@ def argv(self) -> List[str]: return rtn - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Utility method to convert this Statement into a dictionary for use in persistent JSON history files""" return self.__dict__.copy() @staticmethod - def from_dict(source_dict: Dict[str, Any]) -> 'Statement': + def from_dict(source_dict: dict[str, Any]) -> 'Statement': """ Utility method to restore a Statement from a dictionary @@ -209,8 +206,8 @@ def __init__( self, terminators: Optional[Iterable[str]] = None, multiline_commands: Optional[Iterable[str]] = None, - aliases: Optional[Dict[str, str]] = None, - shortcuts: Optional[Dict[str, str]] = None, + aliases: Optional[dict[str, str]] = None, + shortcuts: Optional[dict[str, str]] = None, ) -> None: """Initialize an instance of StatementParser. @@ -222,13 +219,13 @@ def __init__( :param aliases: dictionary containing aliases :param shortcuts: dictionary containing shortcuts """ - self.terminators: Tuple[str, ...] + self.terminators: tuple[str, ...] if terminators is None: self.terminators = (constants.MULTILINE_TERMINATOR,) else: self.terminators = tuple(terminators) - self.multiline_commands: Tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else () - self.aliases: Dict[str, str] = aliases if aliases is not None else {} + self.multiline_commands: tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else () + self.aliases: dict[str, str] = aliases if aliases is not None else {} if shortcuts is None: shortcuts = constants.DEFAULT_SHORTCUTS @@ -269,7 +266,7 @@ def __init__( expr = rf'\A\s*(\S*?)({second_group})' self._command_pattern = re.compile(expr) - def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[bool, str]: + def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> tuple[bool, str]: """Determine whether a word is a valid name for a command. Commands cannot include redirection characters, whitespace, @@ -320,7 +317,7 @@ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[b errmsg = '' return valid, errmsg - def tokenize(self, line: str) -> List[str]: + def tokenize(self, line: str) -> list[str]: """ Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed. @@ -558,7 +555,7 @@ def parse_command_only(self, rawinput: str) -> Statement: def get_command_arg_list( self, command_name: str, to_parse: Union[Statement, str], preserve_quotes: bool - ) -> Tuple[Statement, List[str]]: + ) -> tuple[Statement, list[str]]: """ Convenience method used by the argument parsing decorators. @@ -627,7 +624,7 @@ def _expand(self, line: str) -> str: return line @staticmethod - def _command_and_args(tokens: List[str]) -> Tuple[str, str]: + def _command_and_args(tokens: list[str]) -> tuple[str, str]: """Given a list of tokens, return a tuple of the command and the args as a string. """ @@ -642,7 +639,7 @@ def _command_and_args(tokens: List[str]) -> Tuple[str, str]: return command, args - def split_on_punctuation(self, tokens: List[str]) -> List[str]: + def split_on_punctuation(self, tokens: list[str]) -> list[str]: """Further splits tokens from a command line using punctuation characters. Punctuation characters are treated as word breaks when they are in @@ -652,7 +649,7 @@ def split_on_punctuation(self, tokens: List[str]) -> List[str]: :param tokens: the tokens as parsed by shlex :return: a new list of tokens, further split using punctuation """ - punctuation: List[str] = [] + punctuation: list[str] = [] punctuation.extend(self.terminators) punctuation.extend(constants.REDIRECTION_CHARS) diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 7873f9dcf..e4aa505f3 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -13,7 +13,6 @@ IO, TYPE_CHECKING, Any, - List, NamedTuple, Optional, TextIO, @@ -99,9 +98,9 @@ def __init__(self, cmd2_app: 'cmd2.Cmd', *, add_to_history: bool = True) -> None # Tells if any of the commands run via __call__ returned True for stop self.stop = False - def __dir__(self) -> List[str]: + def __dir__(self) -> list[str]: """Return a custom set of attribute names""" - attributes: List[str] = [] + attributes: list[str] = [] attributes.insert(0, 'cmd_echo') return attributes diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 8abe66265..f86cccb2c 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -17,10 +17,8 @@ from typing import ( Any, Deque, - List, Optional, Sequence, - Tuple, Union, ) @@ -156,7 +154,7 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None: col.width = max(1, ansi.widest_line(col.header)) @staticmethod - def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_last_word: bool) -> Tuple[str, int, int]: + def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_last_word: bool) -> tuple[str, int, int]: """ Used by _wrap_text() to wrap a long word over multiple lines @@ -164,7 +162,7 @@ def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_ :param max_width: maximum display width of a line :param max_lines: maximum lines to wrap before ending the last line displayed with an ellipsis :param is_last_word: True if this is the last word of the total text being wrapped - :return: Tuple(wrapped text, lines used, display width of last line) + :return: tuple(wrapped text, lines used, display width of last line) """ styles_dict = utils.get_styles_dict(word) wrapped_buf = io.StringIO() @@ -382,7 +380,7 @@ def add_word(word_to_add: str, is_last_word: bool) -> None: return wrapped_buf.getvalue() - def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> Tuple[Deque[str], int]: + def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> tuple[Deque[str], int]: """ Generate the lines of a table cell @@ -392,7 +390,7 @@ def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fil :param col: Column definition for this cell :param fill_char: character that fills remaining space in a cell. If your text has a background color, then give fill_char the same background color. (Cannot be a line breaking character) - :return: Tuple(deque of cell lines, display width of the cell) + :return: tuple(deque of cell lines, display width of the cell) """ # Convert data to string and replace tabs with spaces data_str = str(cell_data).replace('\t', SPACE * self.tab_width) @@ -654,7 +652,7 @@ def generate_header(self) -> str: inter_cell = self.apply_header_bg(self.column_spacing * SPACE) # Apply background color to header text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for col in self.cols: if col.style_header_text: to_display.append(self.apply_header_bg(col.header)) @@ -694,7 +692,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str: inter_cell = self.apply_data_bg(self.column_spacing * SPACE) # Apply background color to data text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for index, col in enumerate(self.cols): if col.style_data_text: to_display.append(self.apply_data_bg(row_data[index])) @@ -949,7 +947,7 @@ def generate_header(self) -> str: post_line = self.apply_header_bg(self.padding * SPACE) + self.apply_border_color('║') # Apply background color to header text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for col in self.cols: if col.style_header_text: to_display.append(self.apply_header_bg(col.header)) @@ -993,7 +991,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str: post_line = self.apply_data_bg(self.padding * SPACE) + self.apply_border_color('║') # Apply background color to data text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for index, col in enumerate(self.cols): if col.style_data_text: to_display.append(self.apply_data_bg(row_data[index])) diff --git a/cmd2/transcript.py b/cmd2/transcript.py index f4781fd99..47c76878e 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -15,10 +15,8 @@ class is used in cmd2.py::run_transcript_tests() from typing import ( TYPE_CHECKING, Iterator, - List, Optional, TextIO, - Tuple, cast, ) @@ -66,7 +64,7 @@ def runTest(self) -> None: # was testall def _fetchTranscripts(self) -> None: self.transcripts = {} - testfiles = cast(List[str], getattr(self.cmdapp, 'testfiles', [])) + testfiles = cast(list[str], getattr(self.cmdapp, 'testfiles', [])) for fname in testfiles: tfile = open(fname) self.transcripts[fname] = iter(tfile.readlines()) @@ -184,7 +182,7 @@ def _transform_transcript_expected(self, s: str) -> str: return regex @staticmethod - def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> Tuple[str, int, int]: + def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> tuple[str, int, int]: """Find the next slash in {s} after {start} that is not preceded by a backslash. If we find an escaped slash, add everything up to and including it to regex, diff --git a/cmd2/utils.py b/cmd2/utils.py index f2d1af5f0..1cac0a545 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -23,9 +23,7 @@ TYPE_CHECKING, Any, Callable, - Dict, Iterable, - List, Optional, TextIO, Type, @@ -160,7 +158,7 @@ def __init__( """ if val_type is bool: - def get_bool_choices(_) -> List[str]: # type: ignore[no-untyped-def] + def get_bool_choices(_) -> list[str]: # type: ignore[no-untyped-def] """Used to tab complete lowercase boolean values""" return ['true', 'false'] @@ -235,7 +233,7 @@ def is_text_file(file_path: str) -> bool: return valid_text_file -def remove_duplicates(list_to_prune: List[_T]) -> List[_T]: +def remove_duplicates(list_to_prune: list[_T]) -> list[_T]: """Removes duplicates from a list while preserving order of the items. :param list_to_prune: the list being pruned of duplicates @@ -257,7 +255,7 @@ def norm_fold(astr: str) -> str: return unicodedata.normalize('NFC', astr).casefold() -def alphabetical_sort(list_to_sort: Iterable[str]) -> List[str]: +def alphabetical_sort(list_to_sort: Iterable[str]) -> list[str]: """Sorts a list of strings alphabetically. For example: ['a1', 'A11', 'A2', 'a22', 'a3'] @@ -284,7 +282,7 @@ def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]: return norm_fold(input_str) -def natural_keys(input_str: str) -> List[Union[int, str]]: +def natural_keys(input_str: str) -> list[Union[int, str]]: """ Converts a string into a list of integers and strings to support natural sorting (see natural_sort). @@ -295,7 +293,7 @@ def natural_keys(input_str: str) -> List[Union[int, str]]: return [try_int_or_force_to_lower_case(substr) for substr in re.split(r'(\d+)', input_str)] -def natural_sort(list_to_sort: Iterable[str]) -> List[str]: +def natural_sort(list_to_sort: Iterable[str]) -> list[str]: """ Sorts a list of strings case insensitively as well as numerically. @@ -311,7 +309,7 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]: return sorted(list_to_sort, key=natural_keys) -def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None: +def quote_specific_tokens(tokens: list[str], tokens_to_quote: list[str]) -> None: """ Quote specific tokens in a list @@ -323,7 +321,7 @@ def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None tokens[i] = quote_string(token) -def unquote_specific_tokens(tokens: List[str], tokens_to_unquote: List[str]) -> None: +def unquote_specific_tokens(tokens: list[str], tokens_to_unquote: list[str]) -> None: """ Unquote specific tokens in a list @@ -357,7 +355,7 @@ def expand_user(token: str) -> str: return token -def expand_user_in_tokens(tokens: List[str]) -> None: +def expand_user_in_tokens(tokens: list[str]) -> None: """ Call expand_user() on all tokens in a list of strings :param tokens: tokens to expand @@ -399,7 +397,7 @@ def find_editor() -> Optional[str]: return editor -def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]: +def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> list[str]: """Return a list of file paths based on a glob pattern. Only files are returned, not directories, and optionally only files for which the user has a specified access to. @@ -411,7 +409,7 @@ def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]: return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)] -def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List[str]: +def files_from_glob_patterns(patterns: list[str], access: int = os.F_OK) -> list[str]: """Return a list of file paths based on a list of glob patterns. Only files are returned, not directories, and optionally only files for which the user has a specified access to. @@ -427,7 +425,7 @@ def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List return files -def get_exes_in_path(starts_with: str) -> List[str]: +def get_exes_in_path(starts_with: str) -> list[str]: """Returns names of executables in a user's path :param starts_with: what the exes should start with. leave blank for all exes in path. @@ -748,7 +746,7 @@ def __init__( self.saved_redirecting = saved_redirecting -def _remove_overridden_styles(styles_to_parse: List[str]) -> List[str]: +def _remove_overridden_styles(styles_to_parse: list[str]) -> list[str]: """ Utility function for align_text() / truncate_line() which filters a style list down to only those which would still be in effect if all were processed in order. @@ -769,7 +767,7 @@ class StyleState: def __init__(self) -> None: # Contains styles still in effect, keyed by their index in styles_to_parse - self.style_dict: Dict[int, str] = dict() + self.style_dict: dict[int, str] = dict() # Indexes into style_dict self.reset_all: Optional[int] = None @@ -907,7 +905,7 @@ def align_text( # ANSI style sequences that may affect subsequent lines will be cancelled by the fill_char's style. # To avoid this, we save styles which are still in effect so we can restore them when beginning the next line. # This also allows lines to be used independently and still have their style. TableCreator does this. - previous_styles: List[str] = [] + previous_styles: list[str] = [] for index, line in enumerate(lines): if index > 0: @@ -1118,7 +1116,7 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: return truncated_buf.getvalue() -def get_styles_dict(text: str) -> Dict[int, str]: +def get_styles_dict(text: str) -> dict[int, str]: """ Return an OrderedDict containing all ANSI style sequences found in a string diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py index daad63ab0..6b7064883 100755 --- a/examples/argparse_completion.py +++ b/examples/argparse_completion.py @@ -5,10 +5,6 @@ """ import argparse -from typing import ( - Dict, - List, -) from cmd2 import ( Cmd, @@ -28,11 +24,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] - def choices_provider(self) -> List[str]: + def choices_provider(self) -> list[str]: """A choices provider is useful when the choice list is based on instance data of your application""" return self.sport_item_strs - def choices_completion_error(self) -> List[str]: + def choices_completion_error(self) -> list[str]: """ CompletionErrors can be raised if an error occurs while tab completing. @@ -44,14 +40,14 @@ def choices_completion_error(self) -> List[str]: return self.sport_item_strs raise CompletionError("debug must be true") - def choices_completion_item(self) -> List[CompletionItem]: + def choices_completion_item(self) -> list[CompletionItem]: """Return CompletionItem instead of strings. These give more context to what's being tab completed.""" fancy_item = "These things can\ncontain newlines and\n" fancy_item += ansi.style("styled text!!", fg=ansi.Fg.LIGHT_YELLOW, underline=True) items = {1: "My item", 2: "Another item", 3: "Yet another item", 4: fancy_item} return [CompletionItem(item_id, description) for item_id, description in items.items()] - def choices_arg_tokens(self, arg_tokens: Dict[str, List[str]]) -> List[str]: + def choices_arg_tokens(self, arg_tokens: dict[str, list[str]]) -> list[str]: """ If a choices or completer function/method takes a value called arg_tokens, then it will be passed a dictionary that maps the command line tokens up through the one being completed diff --git a/examples/async_printing.py b/examples/async_printing.py index e94ee89a0..221c52e7e 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -8,9 +8,6 @@ import random import threading import time -from typing import ( - List, -) import cmd2 from cmd2 import ( @@ -89,7 +86,7 @@ def do_stop_alerts(self, _): else: print("The alert thread is already stopped") - def _get_alerts(self) -> List[str]: + def _get_alerts(self) -> list[str]: """ Reports alerts :return: the list of alerts diff --git a/examples/basic_completion.py b/examples/basic_completion.py index c713f2b0d..ba32d67bb 100755 --- a/examples/basic_completion.py +++ b/examples/basic_completion.py @@ -14,13 +14,10 @@ """ import functools -from typing import ( - List, -) import cmd2 -# List of strings used with completion functions +# list of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] @@ -46,7 +43,7 @@ def do_flag_based(self, statement: cmd2.Statement): """ self.poutput("Args: {}".format(statement.args)) - def complete_flag_based(self, text, line, begidx, endidx) -> List[str]: + def complete_flag_based(self, text, line, begidx, endidx) -> list[str]: """Completion function for do_flag_based""" flag_dict = { # Tab complete food items after -f and --food flags in command line @@ -66,7 +63,7 @@ def do_index_based(self, statement: cmd2.Statement): """Tab completes first 3 arguments using index_based_complete""" self.poutput("Args: {}".format(statement.args)) - def complete_index_based(self, text, line, begidx, endidx) -> List[str]: + def complete_index_based(self, text, line, begidx, endidx) -> list[str]: """Completion function for do_index_based""" index_dict = { 1: food_item_strs, # Tab complete food items at index 1 in command line @@ -87,7 +84,7 @@ def do_raise_error(self, statement: cmd2.Statement): """Demonstrates effect of raising CompletionError""" self.poutput("Args: {}".format(statement.args)) - def complete_raise_error(self, text, line, begidx, endidx) -> List[str]: + def complete_raise_error(self, text, line, begidx, endidx) -> list[str]: """ CompletionErrors can be raised if an error occurs while tab completing. diff --git a/examples/decorator_example.py b/examples/decorator_example.py index ea8fd3b50..2a5449dc9 100755 --- a/examples/decorator_example.py +++ b/examples/decorator_example.py @@ -12,9 +12,6 @@ """ import argparse -from typing import ( - List, -) import cmd2 @@ -76,7 +73,7 @@ def do_tag(self, args: argparse.Namespace): self.poutput('<{0}>{1}'.format(args.tag, ' '.join(args.content))) @cmd2.with_argument_list - def do_tagg(self, arglist: List[str]): + def do_tagg(self, arglist: list[str]): """version of creating an html tag using arglist instead of argparser""" if len(arglist) >= 2: tag = arglist[0] diff --git a/examples/exit_code.py b/examples/exit_code.py index d8e538ced..12d73176f 100755 --- a/examples/exit_code.py +++ b/examples/exit_code.py @@ -2,10 +2,6 @@ # coding=utf-8 """A simple example demonstrating the following how to emit a non-zero exit code in your cmd2 application.""" -from typing import ( - List, -) - import cmd2 @@ -16,7 +12,7 @@ def __init__(self): super().__init__() @cmd2.with_argument_list - def do_exit(self, arg_list: List[str]) -> bool: + def do_exit(self, arg_list: list[str]) -> bool: """Exit the application with an optional exit code. Usage: exit [exit_code] diff --git a/examples/hooks.py b/examples/hooks.py index 97b90739d..6c1001e50 100755 --- a/examples/hooks.py +++ b/examples/hooks.py @@ -10,9 +10,6 @@ """ import re -from typing import ( - List, -) import cmd2 @@ -102,7 +99,7 @@ def proof_hook(self, data: cmd2.plugin.PostcommandData) -> cmd2.plugin.Postcomma return data @cmd2.with_argument_list - def do_list(self, arglist: List[str]) -> None: + def do_list(self, arglist: list[str]) -> None: """Generate a list of 10 numbers.""" if arglist: first = arglist[0] diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py index a4b7582f5..4fe63bbfa 100644 --- a/examples/modular_commands/commandset_basic.py +++ b/examples/modular_commands/commandset_basic.py @@ -3,10 +3,6 @@ A simple example demonstrating a loadable command set """ -from typing import ( - List, -) - from cmd2 import ( Cmd, CommandSet, @@ -19,7 +15,7 @@ @with_default_category('Basic Completion') class BasicCompletionCommandSet(CommandSet): - # List of strings used with completion functions + # list of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] @@ -40,7 +36,7 @@ def do_flag_based(self, cmd: Cmd, statement: Statement): """ self._cmd.poutput("Args: {}".format(statement.args)) - def complete_flag_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_flag_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> list[str]: """Completion function for do_flag_based""" flag_dict = { # Tab complete food items after -f and --food flags in command line @@ -60,7 +56,7 @@ def do_index_based(self, cmd: Cmd, statement: Statement): """Tab completes first 3 arguments using index_based_complete""" self._cmd.poutput("Args: {}".format(statement.args)) - def complete_index_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_index_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> list[str]: """Completion function for do_index_based""" index_dict = { 1: self.food_item_strs, # Tab complete food items at index 1 in command line @@ -74,14 +70,14 @@ def do_delimiter_complete(self, cmd: Cmd, statement: Statement): """Tab completes files from a list using delimiter_complete""" self._cmd.poutput("Args: {}".format(statement.args)) - def complete_delimiter_complete(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_delimiter_complete(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> list[str]: return cmd.delimiter_complete(text, line, begidx, endidx, match_against=self.file_strs, delimiter='/') def do_raise_error(self, cmd: Cmd, statement: Statement): """Demonstrates effect of raising CompletionError""" self._cmd.poutput("Args: {}".format(statement.args)) - def complete_raise_error(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_raise_error(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> list[str]: """ CompletionErrors can be raised if an error occurs while tab completing. diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py index 7ab84ac3f..e89884d77 100644 --- a/examples/modular_commands/commandset_complex.py +++ b/examples/modular_commands/commandset_complex.py @@ -5,9 +5,6 @@ """ import argparse -from typing import ( - List, -) import cmd2 @@ -25,7 +22,7 @@ def do_banana(self, statement: cmd2.Statement): cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) @cmd2.with_argparser(cranberry_parser, with_unknown_args=True) - def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]): + def do_cranberry(self, ns: argparse.Namespace, unknown: list[str]): self._cmd.poutput('Cranberry {}!!'.format(ns.arg1)) if unknown and len(unknown): self._cmd.poutput('Unknown: ' + ', '.join(['{}'] * len(unknown)).format(*unknown)) @@ -36,12 +33,12 @@ def help_cranberry(self): @cmd2.with_argument_list @cmd2.with_category('Also Alone') - def do_durian(self, args: List[str]): + def do_durian(self, args: list[str]): """Durian Command""" self._cmd.poutput('{} Arguments: '.format(len(args))) self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args)) - def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self._cmd.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting']) elderberry_parser = cmd2.Cmd2ArgumentParser() diff --git a/examples/modular_commands_main.py b/examples/modular_commands_main.py index 74483987b..00f5bce95 100755 --- a/examples/modular_commands_main.py +++ b/examples/modular_commands_main.py @@ -8,7 +8,6 @@ import argparse from typing import ( Iterable, - List, Optional, ) @@ -35,7 +34,7 @@ def __init__(self, command_sets: Optional[Iterable[CommandSet]] = None): super().__init__(command_sets=command_sets) self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] - def choices_provider(self) -> List[str]: + def choices_provider(self) -> list[str]: """A choices provider is useful when the choice list is based on instance data of your application""" return self.sport_item_strs diff --git a/examples/paged_output.py b/examples/paged_output.py index 0f7173b2e..4f4f8a5a5 100755 --- a/examples/paged_output.py +++ b/examples/paged_output.py @@ -3,9 +3,6 @@ """A simple example demonstrating the using paged output via the ppaged() method.""" import os -from typing import ( - List, -) import cmd2 @@ -27,7 +24,7 @@ def page_file(self, file_path: str, chop: bool = False): self.pexcept('Error reading {!r}: {}'.format(filename, ex)) @cmd2.with_argument_list - def do_page_wrap(self, args: List[str]): + def do_page_wrap(self, args: list[str]): """Read in a text file and display its output in a pager, wrapping long lines if they don't fit. Usage: page_wrap @@ -40,7 +37,7 @@ def do_page_wrap(self, args: List[str]): complete_page_wrap = cmd2.Cmd.path_complete @cmd2.with_argument_list - def do_page_truncate(self, args: List[str]): + def do_page_truncate(self, args: list[str]): """Read in a text file and display its output in a pager, truncating long lines if they don't fit. Truncated lines can still be accessed by scrolling to the right using the arrow keys. diff --git a/examples/read_input.py b/examples/read_input.py index bfc43380b..87f4719f7 100755 --- a/examples/read_input.py +++ b/examples/read_input.py @@ -4,10 +4,6 @@ A simple example demonstrating the various ways to call cmd2.Cmd.read_input() for input history and tab completion """ -from typing import ( - List, -) - import cmd2 EXAMPLE_COMMANDS = "Example Commands" @@ -64,7 +60,7 @@ def do_custom_choices(self, _) -> None: else: self.custom_history.append(input_str) - def choices_provider(self) -> List[str]: + def choices_provider(self) -> list[str]: """Example choices provider function""" return ["from_provider_1", "from_provider_2", "from_provider_3"] diff --git a/examples/scripts/save_help_text.py b/examples/scripts/save_help_text.py index b8ba9624d..3590dd328 100644 --- a/examples/scripts/save_help_text.py +++ b/examples/scripts/save_help_text.py @@ -8,15 +8,12 @@ import argparse import os import sys -from typing import ( - List, - TextIO, -) +from typing import TextIO ASTERISKS = "********************************************************" -def get_sub_commands(parser: argparse.ArgumentParser) -> List[str]: +def get_sub_commands(parser: argparse.ArgumentParser) -> list[str]: """Get a list of subcommands for an ArgumentParser""" sub_cmds = [] diff --git a/examples/table_creation.py b/examples/table_creation.py index 852f2d84d..485c4c05e 100755 --- a/examples/table_creation.py +++ b/examples/table_creation.py @@ -4,10 +4,7 @@ import functools import sys -from typing import ( - Any, - List, -) +from typing import Any from cmd2 import ( EightBitBg, @@ -63,8 +60,8 @@ def __init__(self, name: str, birthday: str, place_of_birth: str) -> None: self.name = name self.birthday = birthday self.place_of_birth = place_of_birth - self.books: List[Book] = [] - self.relatives: List[Relative] = [] + self.books: list[Book] = [] + self.relatives: list[Relative] = [] def ansi_print(text): @@ -76,7 +73,7 @@ def basic_tables(): """Demonstrates basic examples of the table classes""" # Table data which demonstrates handling of wrapping and text styles - data_list: List[List[Any]] = list() + data_list: list[list[Any]] = list() data_list.append(["Billy Smith", "123 Sesame St.\n" "Fake Town, USA 33445", DollarFormatter(100333.03)]) data_list.append( [ @@ -96,7 +93,7 @@ def basic_tables(): data_list.append(["John Jones", "9235 Highway 32\n" + green("Greenville") + ", SC 29604", DollarFormatter(82987.71)]) # Table Columns (width does not account for any borders or padding which may be added) - columns: List[Column] = list() + columns: list[Column] = list() columns.append(Column("Name", width=20)) columns.append(Column("Address", width=38)) columns.append( @@ -123,7 +120,7 @@ def nested_tables(): """ # Create data for this example - author_data: List[Author] = [] + author_data: list[Author] = [] author_1 = Author("Frank Herbert", "10/08/1920", "Tacoma, Washington") author_1.books.append(Book("Dune", "1965")) author_1.books.append(Book("Dune Messiah", "1969")) @@ -159,7 +156,7 @@ def nested_tables(): # Define table which presents Author data fields vertically with no header. # This will be nested in the parent table's first column. - author_columns: List[Column] = list() + author_columns: list[Column] = list() author_columns.append(Column("", width=14)) author_columns.append(Column("", width=20)) @@ -174,7 +171,7 @@ def nested_tables(): # Define AlternatingTable for books checked out by people in the first table. # This will be nested in the parent table's second column. - books_columns: List[Column] = list() + books_columns: list[Column] = list() books_columns.append(Column(ansi.style("Title", bold=True), width=25)) books_columns.append( Column( @@ -196,7 +193,7 @@ def nested_tables(): # Define BorderedTable for relatives of the author # This will be nested in the parent table's third column. - relative_columns: List[Column] = list() + relative_columns: list[Column] = list() relative_columns.append(Column(ansi.style("Name", bold=True), width=25)) relative_columns.append(Column(ansi.style("Relationship", bold=True), width=12)) @@ -220,7 +217,7 @@ def nested_tables(): ) # Define parent AlternatingTable which contains Author and Book tables - parent_tbl_columns: List[Column] = list() + parent_tbl_columns: list[Column] = list() # All of the nested tables already have background colors. Set style_data_text # to False so the parent AlternatingTable does not apply background color to them. @@ -242,7 +239,7 @@ def nested_tables(): ) # Construct the tables - parent_table_data: List[List[Any]] = [] + parent_table_data: list[list[Any]] = [] for row, author in enumerate(author_data, start=1): # First build the author table and color it based on row number author_tbl = even_author_tbl if row % 2 == 0 else odd_author_tbl diff --git a/pyproject.toml b/pyproject.toml index 163f42935..3007f8dcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -205,7 +205,7 @@ select = [ "FA", # flake8-future-annotations # "FBT", # flake8-boolean-trap "G", # flake8-logging-format - # "I", # isort + "I", # isort "ICN", # flake8-import-conventions # "INP", # flake8-no-pep420 "INT", # flake8-gettext diff --git a/tests/conftest.py b/tests/conftest.py index 20b9ebd31..aae8a3bdf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,6 @@ redirect_stdout, ) from typing import ( - List, Optional, Union, ) @@ -32,7 +31,7 @@ def verify_help_text( - cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None + cmd2_app: cmd2.Cmd, help_output: Union[str, list[str]], verbose_strings: Optional[list[str]] = None ) -> None: """This function verifies that all expected commands are present in the help text. @@ -195,7 +194,7 @@ def get_endidx(): return app.complete(text, 0) -def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser: +def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser: if not subcmd_names: return action cur_subcmd = subcmd_names.pop(0) diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index 1f9178f88..e06b53a89 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -6,11 +6,7 @@ import argparse import numbers -from typing import ( - Dict, - List, - cast, -) +from typing import cast import pytest @@ -39,11 +35,11 @@ standalone_completions = ['standalone', 'completer'] -def standalone_choice_provider(cli: cmd2.Cmd) -> List[str]: +def standalone_choice_provider(cli: cmd2.Cmd) -> list[str]: return standalone_choices -def standalone_completer(cli: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: +def standalone_completer(cli: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> list[str]: return cli.basic_complete(text, line, begidx, endidx, standalone_completions) @@ -125,11 +121,11 @@ def do_pos_and_flag(self, args: argparse.Namespace) -> None: # This tests that CompletionItems created with numerical values are sorted as numbers. num_completion_items = [CompletionItem(5, "Five"), CompletionItem(1.5, "One.Five"), CompletionItem(2, "Five")] - def choices_provider(self) -> List[str]: + def choices_provider(self) -> list[str]: """Method that provides choices""" return self.choices_from_provider - def completion_item_method(self) -> List[CompletionItem]: + def completion_item_method(self) -> list[CompletionItem]: """Choices method that returns CompletionItems""" items = [] for i in range(0, 10): @@ -191,13 +187,13 @@ def do_choices(self, args: argparse.Namespace) -> None: completions_for_pos_1 = ['completions', 'positional_1', 'probably', 'missed', 'spot'] completions_for_pos_2 = ['completions', 'positional_2', 'probably', 'missed', 'me'] - def flag_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def flag_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_flag) - def pos_1_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def pos_1_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_1) - def pos_2_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def pos_2_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_2) completer_parser = Cmd2ArgumentParser() @@ -265,11 +261,11 @@ def do_hint(self, args: argparse.Namespace) -> None: ############################################################################################################ # Begin code related to CompletionError ############################################################################################################ - def completer_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def completer_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: """Raises CompletionError""" raise CompletionError('completer broke something') - def choice_raise_error(self) -> List[str]: + def choice_raise_error(self) -> list[str]: """Raises CompletionError""" raise CompletionError('choice broke something') @@ -284,13 +280,13 @@ def do_raise_completion_error(self, args: argparse.Namespace) -> None: ############################################################################################################ # Begin code related to receiving arg_tokens ############################################################################################################ - def choices_takes_arg_tokens(self, arg_tokens: Dict[str, List[str]]) -> List[str]: + def choices_takes_arg_tokens(self, arg_tokens: dict[str, list[str]]) -> list[str]: """Choices function that receives arg_tokens from ArgparseCompleter""" return [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] def completer_takes_arg_tokens( - self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]] - ) -> List[str]: + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]] + ) -> list[str]: """Completer function that receives arg_tokens from ArgparseCompleter""" match_against = [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] return self.basic_complete(text, line, begidx, endidx, match_against) @@ -1204,7 +1200,7 @@ def test_complete_standalone(ac_app, flag, completions): # Custom ArgparseCompleter-based class class CustomCompleter(argparse_completer.ArgparseCompleter): - def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]: + def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: list[str]) -> list[str]: """Override so flags with 'complete_when_ready' set to True will complete only when app is ready""" # Find flags which should not be completed and place them in matched_flags diff --git a/tests/test_utils.py b/tests/test_utils.py index a173f7f45..665261abf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -336,10 +336,6 @@ def test_context_flag_exit_err(context_flag): def test_remove_overridden_styles(): - from typing import ( - List, - ) - from cmd2 import ( Bg, EightBitBg, @@ -350,7 +346,7 @@ def test_remove_overridden_styles(): TextStyle, ) - def make_strs(styles_list: List[ansi.AnsiSequence]) -> List[str]: + def make_strs(styles_list: list[ansi.AnsiSequence]) -> list[str]: return [str(s) for s in styles_list] # Test Reset All diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index e70185a89..423523b5d 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -9,7 +9,6 @@ redirect_stdout, ) from typing import ( - List, Optional, Union, ) @@ -34,7 +33,7 @@ def verify_help_text( - cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None + cmd2_app: cmd2.Cmd, help_output: Union[str, list[str]], verbose_strings: Optional[list[str]] = None ) -> None: """This function verifies that all expected commands are present in the help text. diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 0b488952c..18c3d74ab 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -6,9 +6,6 @@ import argparse import signal -from typing import ( - List, -) import pytest @@ -61,7 +58,7 @@ def do_banana(self, statement: cmd2.Statement): cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) @cmd2.with_argparser(cranberry_parser, with_unknown_args=True) - def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]): + def do_cranberry(self, ns: argparse.Namespace, unknown: list[str]): self._cmd.poutput('Cranberry {}!!'.format(ns.arg1)) if unknown and len(unknown): self._cmd.poutput('Unknown: ' + ', '.join(['{}'] * len(unknown)).format(*unknown)) @@ -72,13 +69,13 @@ def help_cranberry(self): @cmd2.with_argument_list @cmd2.with_category('Also Alone') - def do_durian(self, args: List[str]): + def do_durian(self, args: list[str]): """Durian Command""" self._cmd.poutput('{} Arguments: '.format(len(args))) self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args)) self._cmd.last_result = {'args': args} - def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self._cmd.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting']) elderberry_parser = cmd2.Cmd2ArgumentParser() @@ -450,7 +447,7 @@ def __init__(self, dummy): def do_arugula(self, _: cmd2.Statement): self._cmd.poutput('Arugula') - def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return ['quartered', 'diced'] bokchoy_parser = cmd2.Cmd2ArgumentParser() @@ -688,7 +685,7 @@ def cut_banana(self, ns: argparse.Namespace): """Cut banana""" self.poutput('cutting banana: ' + ns.direction) - def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return ['quartered', 'diced'] bokchoy_parser = cmd2.Cmd2ArgumentParser() @@ -743,7 +740,7 @@ def __init__(self, dummy): """dummy variable prevents this from being autoloaded in other tests""" super(SupportFuncProvider, self).__init__() - def complete_states(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_states(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: assert self is complete_states_expected_self return self._cmd.basic_complete(text, line, begidx, endidx, self.states) From 4616b09b786d429558b35e72d6c81ffbed2af79c Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 28 Nov 2024 14:32:28 -0500 Subject: [PATCH 10/12] Don't use parser description in verbose help output since it may be a Rich object. --- cmd2/cmd2.py | 10 +++------- tests/test_cmd2.py | 5 +++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 683c1c1e3..649dccdfb 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -273,8 +273,8 @@ def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]: _set_parser_prog(parser, 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 @@ -3753,12 +3753,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() diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index f7e6687cd..21098ef8d 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1256,9 +1256,10 @@ def test_help_multiline_docstring(help_app): assert help_app.last_result is True -def test_help_verbose_uses_parser_description(help_app: HelpApp): +def test_help_verbose_uses_parser_docstring(help_app: HelpApp): 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): From 825c90d880bc60c459322359efdd07e56bb2242e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 19 Jan 2025 03:03:26 -0500 Subject: [PATCH 11/12] Fixed missing date in change log. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efa9dcfce..9861b33a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - Added `RawDescriptionCmd2HelpFormatter`, `RawTextCmd2HelpFormatter`, `ArgumentDefaultsCmd2HelpFormatter`, and `MetavarTypeCmd2HelpFormatter` and they all use `rich-argparse`. -## 2.5.9 (TBD) +## 2.5.9 (January 17, 2025) - Bug Fixes - Fixed 'index out of range' error when passing no arguments to an argparse-based command function. From bdc60233cdcf8486c5ddb33231aa9ddb84c467cf Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 25 Jan 2025 10:45:23 -0500 Subject: [PATCH 12/12] Fix error in how exception is documented so docs build without warnings --- cmd2/cmd2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 12aae9acc..123b7705a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2752,7 +2752,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: :param statement: a parsed statement from the user :return: A bool telling if an error occurred and a utils.RedirectionSavedState object - :raises: RedirectionError if an error occurs trying to pipe or redirect + :raises RedirectionError: if an error occurs trying to pipe or redirect """ import io import subprocess