summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-09-17 23:44:07 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2020-09-17 23:44:07 -0400
commit0f11ffa3b992e3f777b96dfe46d4274bfca0dcc8 (patch)
tree8167147ff11f5c9c7aaf1008555248c9c1cd7850 /cmd2
parentc50def1eff00f7f44adcb911d215ace65d16aa8a (diff)
parentd348f09cf848a566a43b30e04aa6d3adbc4de8bc (diff)
downloadcmd2-git-0f11ffa3b992e3f777b96dfe46d4274bfca0dcc8.tar.gz
Merge branch 'master' into 2.0
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/cmd2.py59
-rw-r--r--cmd2/command_definition.py24
-rw-r--r--cmd2/constants.py1
-rw-r--r--cmd2/utils.py26
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.