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
1 change: 1 addition & 0 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before merging in this would need a CHANGELOG entry for version 1.2.0 or whatever.

from .argparse_custom import DEFAULT_ARGUMENT_PARSER
from .cmd2 import Cmd
from .command_definition import CommandSet, with_default_category, register_command
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
Expand Down
257 changes: 230 additions & 27 deletions cmd2/cmd2.py

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions cmd2/command_definition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# coding=utf-8
"""
Supports the definition of commands in separate classes to be composed into cmd2.Cmd
"""
import functools
from typing import Callable, Dict, Iterable, Optional, Type

from .constants import COMMAND_FUNC_PREFIX

# Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues
try: # pragma: no cover
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import cmd2

except ImportError: # pragma: no cover
pass

_REGISTERED_COMMANDS = {} # type: Dict[str, Callable]
"""
Registered command tuples. (command, ``do_`` function)
"""


def _partial_passthru(func: Callable, *args, **kwargs) -> functools.partial:
"""
Constructs a partial function that passes arguments through to the wrapped function.
Must construct a new type every time so that each wrapped function's __doc__ can be copied correctly.

:param func: wrapped function
:param args: positional arguments
:param kwargs: keyword arguments
:return: partial function that exposes attributes of wrapped function
"""
def __getattr__(self, item):
return getattr(self.func, item)

def __setattr__(self, key, value):
return setattr(self.func, key, value)

def __dir__(self) -> Iterable[str]:
return dir(self.func)

passthru_type = type('PassthruPartial' + func.__name__,
(functools.partial,),
{
'__getattr__': __getattr__,
'__setattr__': __setattr__,
'__dir__': __dir__,
})
passthru_type.__doc__ = func.__doc__
return passthru_type(func, *args, **kwargs)


def register_command(cmd_func: Callable):
"""
Decorator that allows an arbitrary function to be automatically registered as a command.
If there is a ``help_`` or ``complete_`` function that matches this command, that will also be registered.

:param cmd_func: Function to register as a cmd2 command
:type cmd_func: Callable[[cmd2.Cmd, Union[Statement, argparse.Namespace]], None]
:return:
"""
assert cmd_func.__name__.startswith(COMMAND_FUNC_PREFIX), 'Command functions must start with `do_`'

cmd_name = cmd_func.__name__[len(COMMAND_FUNC_PREFIX):]

if cmd_name not in _REGISTERED_COMMANDS:
_REGISTERED_COMMANDS[cmd_name] = cmd_func
else:
raise KeyError('Command ' + cmd_name + ' is already registered')
return cmd_func


def with_default_category(category: str):
"""
Decorator that applies a category to all ``do_*`` command methods in a class that do not already
have a category specified.

:param category: category to put all uncategorized commands in
:return: decorator function
"""

def decorate_class(cls: Type[CommandSet]):
from .constants import CMD_ATTR_HELP_CATEGORY
import inspect
from .decorators import with_category
methods = inspect.getmembers(
cls,
predicate=lambda meth: inspect.isfunction(meth) and meth.__name__.startswith(COMMAND_FUNC_PREFIX))
category_decorator = with_category(category)
for method in methods:
if not hasattr(method[1], CMD_ATTR_HELP_CATEGORY):
setattr(cls, method[0], category_decorator(method[1]))
return cls
return decorate_class


class CommandSet(object):
"""
Base class for defining sets of commands to load in cmd2.

``with_default_category`` can be used to apply a default category to all commands in the CommandSet.

``do_``, ``help_``, and ``complete_`` functions differ only in that they're now required to accept
a reference to ``cmd2.Cmd`` as the first argument after self.
"""

def __init__(self):
self._cmd = None # type: Optional[cmd2.Cmd]

def on_register(self, cmd):
"""
Called by cmd2.Cmd when a CommandSet is registered. Subclasses can override this
to perform an initialization requiring access to the Cmd object.

:param cmd: The cmd2 main application
:type cmd: cmd2.Cmd
"""
self._cmd = cmd

def on_unregister(self, cmd):
"""
Called by ``cmd2.Cmd`` when a CommandSet is unregistered and removed.

:param cmd:
:type cmd: cmd2.Cmd
"""
self._cmd = None
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.command_definition
=======================

.. automodule:: cmd2.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 @@ -23,6 +23,7 @@ This documentation is for ``cmd2`` version |version|.
argparse_completer
argparse_custom
constants
command_definition
decorators
exceptions
history
Expand Down
Empty file.
Loading