Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# .coveragerc to control coverage.py
[run]
# Source
source = cmd2/
source = plugins/*/cmd2_*/
cmd2/
# (boolean, default False): whether to measure branch coverage in addition to statement coverage.
branch = False

Expand Down
3 changes: 2 additions & 1 deletion cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import importlib_metadata
try:
__version__ = importlib_metadata.version(__name__)
except importlib_metadata.PackageNotFoundError:
except importlib_metadata.PackageNotFoundError: # pragma: no cover
# package is not installed
pass

Expand All @@ -31,6 +31,7 @@
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
from .decorators import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
from .exceptions import Cmd2ArgparseError, SkipPostcommandHooks
from . import plugin
from .parsing import Statement
from .py_bridge import CommandResult
from .utils import categorize, CompletionError, Settable
53 changes: 27 additions & 26 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
self.max_completion_items = 50

# A dictionary mapping settable names to their Settable instance
self.settables = dict()
self.settables = dict() # type: Dict[str, Settable]
self.build_settables()

# Use as prompt for multiline commands on the 2nd+ line of input
Expand All @@ -220,7 +220,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
self.exclude_from_history = ['eof', 'history']

# Dictionary of macro names and their values
self.macros = dict()
self.macros = dict() # type: Dict[str, Macro]

# Keeps track of typed command history in the Python shell
self._py_history = []
Expand Down Expand Up @@ -249,14 +249,14 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
self.last_result = None

# Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command
self._script_dir = []
self._script_dir = [] # type: List[str]

# Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt
self.sigint_protection = utils.ContextFlag()

# If the current command created a process to pipe to, then this will be a ProcReader object.
# Otherwise it will be None. It's used to know when a pipe process can be killed and/or waited upon.
self._cur_pipe_proc_reader = None
self._cur_pipe_proc_reader = None # type: Optional[utils.ProcReader]

# Used to keep track of whether we are redirecting or piping output
self._redirecting = False
Expand All @@ -280,7 +280,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
self.broken_pipe_warning = ''

# Commands that will run at the beginning of the command loop
self._startup_commands = []
self._startup_commands = [] # type: List[str]

# If a startup script is provided and exists, then execute it in the startup commands
if startup_script:
Expand All @@ -289,7 +289,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
self._startup_commands.append("run_script {}".format(utils.quote_string(startup_script)))

# Transcript files to run instead of interactive command loop
self._transcript_files = None
self._transcript_files = None # type: Optional[List[str]]

# Check for command line args
if allow_cli_args:
Expand Down Expand Up @@ -333,7 +333,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
# 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()
self.disabled_commands = dict() # type: Dict[str, DisabledCommand]

# If any command has been categorized, then all other commands that haven't been categorized
# will display under this section in the help output.
Expand Down Expand Up @@ -1910,7 +1910,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
self._cur_pipe_proc_reader, self._redirecting)

# The ProcReader for this command
cmd_pipe_proc_reader = None
cmd_pipe_proc_reader = None # type: Optional[utils.ProcReader]

if not self.allow_redirection:
# Don't return since we set some state variables at the end of the function
Expand Down Expand Up @@ -2694,16 +2694,31 @@ def do_help(self, args: argparse.Namespace) -> None:

def _help_menu(self, verbose: bool = False) -> None:
"""Show a list of commands which help can be displayed for"""
cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info()

if len(cmds_cats) == 0:
# No categories found, fall back to standard behavior
self.poutput("{}".format(str(self.doc_leader)))
self._print_topics(self.doc_header, cmds_doc, verbose)
else:
# Categories found, Organize all commands by category
self.poutput('{}'.format(str(self.doc_leader)))
self.poutput('{}'.format(str(self.doc_header)), end="\n\n")
for category in sorted(cmds_cats.keys(), key=self.default_sort_key):
self._print_topics(category, cmds_cats[category], verbose)
self._print_topics(self.default_category, cmds_doc, verbose)

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):
# 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 = []
cmds_undoc = []
cmds_cats = {}

for command in visible_commands:
func = self.cmd_func(command)
has_help_func = False
Expand All @@ -2724,21 +2739,7 @@ def _help_menu(self, verbose: bool = False) -> None:
cmds_doc.append(command)
else:
cmds_undoc.append(command)

if len(cmds_cats) == 0:
# No categories found, fall back to standard behavior
self.poutput("{}".format(str(self.doc_leader)))
self._print_topics(self.doc_header, cmds_doc, verbose)
else:
# Categories found, Organize all commands by category
self.poutput('{}'.format(str(self.doc_leader)))
self.poutput('{}'.format(str(self.doc_header)), end="\n\n")
for category in sorted(cmds_cats.keys(), key=self.default_sort_key):
self._print_topics(category, cmds_cats[category], verbose)
self._print_topics(self.default_category, cmds_doc, verbose)

self.print_topics(self.misc_header, help_topics, 15, 80)
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
return cmds_cats, cmds_doc, cmds_undoc, help_topics

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"""
Expand Down
80 changes: 62 additions & 18 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# coding=utf-8
"""Decorators for ``cmd2`` commands"""
import argparse
from typing import Any, Callable, Dict, List, Optional, Union
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

from . import constants
from .exceptions import Cmd2ArgparseError
Expand Down Expand Up @@ -30,6 +30,42 @@ def cat_decorator(func):
return func
return cat_decorator

##########################
# The _parse_positionals and _swap_args decorators 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) -> 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.
:arg args: The positional arguments to inspect
:return: The cmd2.Cmd reference and the command line statement
"""
for pos, arg in enumerate(args):
from cmd2 import Cmd
if isinstance(arg, Cmd) and len(args) > pos:
next_arg = args[pos + 1]
if isinstance(next_arg, (Statement, str)):
return arg, args[pos + 1]
raise TypeError('Expected arguments: cmd: cmd2.Cmd, statement: Union[Statement, str] Not found')


def _arg_swap(args: Union[Tuple[Any], List[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
:param args: The original positional arguments
:param search_arg: The argument to search for (usually the Statement)
:param replace_arg: The arguments to substitute in
:return: The new set of arguments to pass to the command function
"""
index = args.index(search_arg)
args_list = list(args)
args_list[index:index + 1] = replace_arg
return args_list


def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> Callable[[List], Optional[bool]]:
"""
Expand All @@ -53,20 +89,22 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->

def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str], **kwargs: Dict[str, Any]) -> Optional[bool]:
def cmd_wrapper(*args, **kwargs: Dict[str, Any]) -> Optional[bool]:
"""
Command function wrapper which translates command line into an argument list and calls actual command function

:param cmd2_app: CLI instance passed as self parameter to command function
:param statement: command line string or already generated Statement
:param args: All positional arguments to this function. We're expecting there to be:
cmd2_app, statement: Union[Statement, str]
contiguously somewhere in the list
:param kwargs: any keyword arguments being passed to command function
:return: return value of command function
"""
cmd2_app, statement = _parse_positionals(args)
_, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
statement,
preserve_quotes)

return func(cmd2_app, parsed_arglist, **kwargs)
args_list = _arg_swap(args, statement, parsed_arglist)
return func(*args_list, **kwargs)

command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
cmd_wrapper.__doc__ = func.__doc__
Expand Down Expand Up @@ -159,17 +197,19 @@ def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,

def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str], **kwargs: Dict[str, Any]) -> Optional[bool]:
def cmd_wrapper(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> Optional[bool]:
"""
Command function wrapper which translates command line into argparse Namespace and calls actual
command function

:param cmd2_app: CLI instance passed as self parameter to command function
:param statement: command line string or already generated Statement
:param args: All positional arguments to this function. We're expecting there to be:
cmd2_app, statement: Union[Statement, str]
contiguously somewhere in the list
:param kwargs: any keyword arguments being passed to command function
:return: return value of command function
:raises: Cmd2ArgparseError if argparse has error parsing command line
"""
cmd2_app, statement = _parse_positionals(args)
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
statement,
preserve_quotes)
Expand All @@ -180,12 +220,13 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str], **kwargs: Dict[str,
namespace = ns_provider(cmd2_app)

try:
args, unknown = parser.parse_known_args(parsed_arglist, namespace)
ns, unknown = parser.parse_known_args(parsed_arglist, namespace)
except SystemExit:
raise Cmd2ArgparseError
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args, unknown, **kwargs)
setattr(ns, '__statement__', statement)
args_list = _arg_swap(args, statement, ns, unknown)
return func(*args_list, **kwargs)

# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
Expand Down Expand Up @@ -241,17 +282,19 @@ def with_argparser(parser: argparse.ArgumentParser, *,

def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str], **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

:param cmd2_app: CLI instance passed as self parameter to command function
:param statement: command line string or already generated Statement
:param args: All positional arguments to this function. We're expecting there to be:
cmd2_app, statement: Union[Statement, str]
contiguously somewhere in the list
:param kwargs: any keyword arguments being passed to command function
:return: return value of command function
:raises: Cmd2ArgparseError if argparse has error parsing command line
"""
cmd2_app, statement = _parse_positionals(args)
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
statement,
preserve_quotes)
Expand All @@ -262,12 +305,13 @@ def cmd_wrapper(cmd2_app, statement: Union[Statement, str], **kwargs: Dict[str,
namespace = ns_provider(cmd2_app)

try:
args = parser.parse_args(parsed_arglist, namespace)
ns = parser.parse_args(parsed_arglist, namespace)
except SystemExit:
raise Cmd2ArgparseError
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args, **kwargs)
setattr(ns, '__statement__', statement)
args_list = _arg_swap(args, statement, ns)
return func(*args_list, **kwargs)

# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
Expand Down
5 changes: 5 additions & 0 deletions docs/api/command_definition.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
cmd2_modular_cmds.command_definition
=====================================

.. automodule:: cmd2_modular_cmds.command_definition
:members:
1 change: 1 addition & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This documentation is for ``cmd2`` version |version|.
py_bridge
table_creator
utils
command_definition

**Modules**

Expand Down
1 change: 1 addition & 0 deletions docs/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Plugins
.. toctree::
:maxdepth: 1

modular_commands
external_test
Loading