diff options
| author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-09-17 23:44:07 -0400 |
|---|---|---|
| committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-09-17 23:44:07 -0400 |
| commit | 0f11ffa3b992e3f777b96dfe46d4274bfca0dcc8 (patch) | |
| tree | 8167147ff11f5c9c7aaf1008555248c9c1cd7850 /cmd2 | |
| parent | c50def1eff00f7f44adcb911d215ace65d16aa8a (diff) | |
| parent | d348f09cf848a566a43b30e04aa6d3adbc4de8bc (diff) | |
| download | cmd2-git-0f11ffa3b992e3f777b96dfe46d4274bfca0dcc8.tar.gz | |
Merge branch 'master' into 2.0
Diffstat (limited to 'cmd2')
| -rw-r--r-- | cmd2/cmd2.py | 59 | ||||
| -rw-r--r-- | cmd2/command_definition.py | 24 | ||||
| -rw-r--r-- | cmd2/constants.py | 1 | ||||
| -rw-r--r-- | cmd2/utils.py | 26 |
4 files changed, 84 insertions, 26 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 86e511c7..af046612 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -47,7 +47,7 @@ from . import ansi, constants, plugin, utils from .argparse_custom import DEFAULT_ARGUMENT_PARSER, CompletionItem from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .command_definition import CommandSet -from .constants import COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX +from .constants import CLASS_ATTR_DEFAULT_HELP_CATEGORY, COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX from .decorators import with_argparser, as_subcommand_to from .exceptions import ( CommandSetRegistrationError, @@ -485,6 +485,8 @@ class Cmd(cmd.Cmd): predicate=lambda meth: isinstance(meth, Callable) and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX)) + default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) + installed_attributes = [] try: for method_name, method in methods: @@ -507,6 +509,9 @@ class Cmd(cmd.Cmd): self._cmd_to_command_sets[command] = cmdset + if default_category and not hasattr(method, constants.CMD_ATTR_HELP_CATEGORY): + utils.categorize(method, default_category) + self._installed_command_sets.append(cmdset) self._register_subcommands(cmdset) @@ -2824,6 +2829,9 @@ class Cmd(cmd.Cmd): " alias create save_results print_results \">\" out.txt\n") alias_create_parser = DEFAULT_ARGUMENT_PARSER(description=alias_create_description, epilog=alias_create_epilog) + alias_create_parser.add_argument('-s', '--silent', action='store_true', + help='do not print message confirming alias was created or\n' + 'overwritten') 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) @@ -2833,7 +2841,6 @@ class Cmd(cmd.Cmd): @as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower()) def _alias_create(self, args: argparse.Namespace) -> None: """Create or overwrite an alias""" - # Validate the alias name valid, errmsg = self.statement_parser.is_valid_command(args.name) if not valid: @@ -2859,18 +2866,20 @@ class Cmd(cmd.Cmd): value += ' ' + ' '.join(args.command_args) # Set the alias - result = "overwritten" if args.name in self.aliases else "created" + if not args.silent: + result = "overwritten" if args.name in self.aliases else "created" + self.poutput("Alias '{}' {}".format(args.name, result)) + self.aliases[args.name] = value - self.poutput("Alias '{}' {}".format(args.name, result)) # alias -> delete alias_delete_help = "delete aliases" alias_delete_description = "Delete specified aliases or all aliases if --all is used" alias_delete_parser = 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='Value') - alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases") @as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help) def _alias_delete(self, args: argparse.Namespace) -> None: @@ -2896,21 +2905,29 @@ class Cmd(cmd.Cmd): "Without arguments, all aliases will be listed.") alias_list_parser = DEFAULT_ARGUMENT_PARSER(description=alias_list_description) + alias_list_parser.add_argument('-w', '--with_silent', action='store_true', + help="include --silent flag with listed aliases\n" + "Use this option when saving to a startup script that\n" + "should silently create aliases.") alias_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='alias(es) to list', choices_provider=_get_alias_completion_items, descriptive_header='Value') @as_subcommand_to('alias', 'list', alias_list_parser, help=alias_delete_help) def _alias_list(self, args: argparse.Namespace) -> None: """List some or all aliases""" + create_cmd = "alias create" + if args.with_silent: + create_cmd += " --silent" + if args.names: for cur_name in utils.remove_duplicates(args.names): if cur_name in self.aliases: - self.poutput("alias create {} {}".format(cur_name, self.aliases[cur_name])) + self.poutput("{} {} {}".format(create_cmd, cur_name, self.aliases[cur_name])) else: self.perror("Alias '{}' not found".format(cur_name)) else: for cur_alias in sorted(self.aliases, key=self.default_sort_key): - self.poutput("alias create {} {}".format(cur_alias, self.aliases[cur_alias])) + self.poutput("{} {} {}".format(create_cmd, cur_alias, self.aliases[cur_alias])) ############################################################# # Parsers and functions for macro command and subcommands @@ -2974,6 +2991,9 @@ class Cmd(cmd.Cmd): " will only complete paths while typing a macro.") macro_create_parser = DEFAULT_ARGUMENT_PARSER(description=macro_create_description, epilog=macro_create_epilog) + macro_create_parser.add_argument('-s', '--silent', action='store_true', + help='do not print message confirming macro was created or\n' + 'overwritten') 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) @@ -2983,7 +3003,6 @@ class Cmd(cmd.Cmd): @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""" - # Validate the macro name valid, errmsg = self.statement_parser.is_valid_command(args.name) if not valid: @@ -3056,17 +3075,19 @@ class Cmd(cmd.Cmd): break # Set the macro - result = "overwritten" if args.name in self.macros else "created" + if not args.silent: + result = "overwritten" if args.name in self.macros else "created" + self.poutput("Macro '{}' {}".format(args.name, result)) + self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list) - self.poutput("Macro '{}' {}".format(args.name, result)) # macro -> delete macro_delete_help = "delete macros" macro_delete_description = "Delete specified macros or all macros if --all is used" macro_delete_parser = 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='Value') - macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros") @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help) def _macro_delete(self, args: argparse.Namespace) -> None: @@ -3092,21 +3113,29 @@ class Cmd(cmd.Cmd): "Without arguments, all macros will be listed.") macro_list_parser = DEFAULT_ARGUMENT_PARSER(description=macro_list_description) + macro_list_parser.add_argument('-w', '--with_silent', action='store_true', + help="include --silent flag with listed macros\n" + "Use this option when saving to a startup script that\n" + "should silently create macros.") macro_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='macro(s) to list', choices_provider=_get_macro_completion_items, descriptive_header='Value') @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""" + create_cmd = "macro create" + if args.with_silent: + create_cmd += " --silent" + if args.names: for cur_name in utils.remove_duplicates(args.names): if cur_name in self.macros: - self.poutput("macro create {} {}".format(cur_name, self.macros[cur_name].value)) + self.poutput("{} {} {}".format(create_cmd, cur_name, self.macros[cur_name].value)) else: self.perror("Macro '{}' not found".format(cur_name)) else: for cur_macro in sorted(self.macros, key=self.default_sort_key): - self.poutput("macro create {} {}".format(cur_macro, self.macros[cur_macro].value)) + self.poutput("{} {} {}".format(create_cmd, cur_macro, self.macros[cur_macro].value)) def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: """Completes the command argument of help""" @@ -3138,12 +3167,12 @@ class Cmd(cmd.Cmd): help_parser = 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) - help_parser.add_argument('-v', '--verbose', action='store_true', - help="print a list of all commands with descriptions of each") # 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: diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 64adaada..3f05792c 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -4,7 +4,7 @@ Supports the definition of commands in separate classes to be composed into cmd2 """ from typing import Optional, Type -from .constants import COMMAND_FUNC_PREFIX +from .constants import CLASS_ATTR_DEFAULT_HELP_CATEGORY, COMMAND_FUNC_PREFIX from .exceptions import CommandSetRegistrationError # Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues @@ -17,22 +17,40 @@ except ImportError: # pragma: no cover pass -def with_default_category(category: str): +def with_default_category(category: str, *, heritable: bool = True): """ Decorator that applies a category to all ``do_*`` command methods in a class that do not already have a category specified. + CommandSets that are decorated by this with `heritable` set to True (default) will set a class attribute that is + inherited by all subclasses unless overridden. All commands of this CommandSet and all subclasses of this CommandSet + that do not declare an explicit category will be placed in this category. Subclasses may use this decorator to + override the default category. + + If `heritable` is set to False, then only the commands declared locally to this CommandSet will be placed in the + specified category. Dynamically created commands, and commands declared in sub-classes will not receive this + category. + :param category: category to put all uncategorized commands in + :param heritable: Flag whether this default category should apply to sub-classes. Defaults to True :return: decorator function """ def decorate_class(cls: Type[CommandSet]): + if heritable: + setattr(cls, CLASS_ATTR_DEFAULT_HELP_CATEGORY, category) + from .constants import CMD_ATTR_HELP_CATEGORY import inspect from .decorators import with_category + # get members of the class that meet the following criteria: + # 1. Must be a function + # 2. Must start with COMMAND_FUNC_PREFIX (do_) + # 3. Must be a member of the class being decorated and not one inherited from a parent declaration methods = inspect.getmembers( cls, - predicate=lambda meth: inspect.isfunction(meth) and meth.__name__.startswith(COMMAND_FUNC_PREFIX)) + predicate=lambda meth: inspect.isfunction(meth) and meth.__name__.startswith(COMMAND_FUNC_PREFIX) + and meth in inspect.getmro(cls)[0].__dict__.values()) category_decorator = with_category(category) for method in methods: if not hasattr(method[1], CMD_ATTR_HELP_CATEGORY): diff --git a/cmd2/constants.py b/cmd2/constants.py index 8aac5857..9f29be86 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -39,6 +39,7 @@ COMPLETER_FUNC_PREFIX = 'complete_' # The custom help category a command belongs to CMD_ATTR_HELP_CATEGORY = 'help_category' +CLASS_ATTR_DEFAULT_HELP_CATEGORY = 'cmd2_default_help_category' # The argparse parser for the command CMD_ATTR_ARGPARSER = 'argparser' diff --git a/cmd2/utils.py b/cmd2/utils.py index b89d57bb..cd716083 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -13,8 +13,9 @@ import re import subprocess import sys import threading + import unicodedata -from typing import Any, Callable, Dict, Iterable, List, Optional, TextIO, Union +from typing import Any, Callable, Dict, IO, Iterable, List, Optional, TextIO, Type, Union from . import constants @@ -433,10 +434,15 @@ class StdSim: """Get the internal contents as bytes""" return bytes(self.buffer.byte_buf) - def read(self) -> str: + def read(self, size: Optional[int] = -1) -> str: """Read from the internal contents as a str and then clear them out""" - result = self.getvalue() - self.clear() + if size is None or size == -1: + result = self.getvalue() + self.clear() + else: + result = self.buffer.byte_buf[:size].decode(encoding=self.encoding, errors=self.errors) + self.buffer.byte_buf = self.buffer.byte_buf[size:] + return result def readbytes(self) -> bytes: @@ -631,7 +637,7 @@ class ContextFlag: class RedirectionSavedState: """Created by each command to store information required to restore state after redirection""" - def __init__(self, self_stdout: Union[StdSim, TextIO], sys_stdout: Union[StdSim, TextIO], + def __init__(self, self_stdout: Union[StdSim, IO[str]], sys_stdout: Union[StdSim, IO[str]], pipe_proc_reader: Optional[ProcReader], saved_redirecting: bool) -> None: """ RedirectionSavedState initializer @@ -972,11 +978,12 @@ def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None :Example: + >>> import cmd2 >>> class MyApp(cmd2.Cmd): >>> def do_echo(self, arglist): >>> self.poutput(' '.join(arglist) >>> - >>> utils.categorize(do_echo, "Text Processing") + >>> cmd2.utils.categorize(do_echo, "Text Processing") For an alternative approach to categorizing commands using a decorator, see :func:`~cmd2.decorators.with_category` @@ -985,10 +992,13 @@ def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None for item in func: setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category) else: - setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) + if inspect.ismethod(func): + setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) + else: + setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) -def get_defining_class(meth): +def get_defining_class(meth) -> Type: """ Attempts to resolve the class that defined a method. |
