Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1314c97
Initial implementation of modular command loading
anselor Jun 11, 2020
91a7eec
Some minor cleanup of how imports work. Fixed issue with help documen…
anselor Jun 13, 2020
050feee
Added new constructor parameter to flag whether commands should autol…
anselor Jun 13, 2020
97f2135
add ability to remove commands and commandsets
anselor Jun 13, 2020
2964e14
Fixes issue with locating help_ annd complete_ functions when autoloa…
anselor Jun 14, 2020
2a5b7d8
Fixes to sphinx generation
anselor Jun 16, 2020
bd7d247
Added explicit tests for dir and setattr. Minor type hinting changes
anselor Jun 16, 2020
0a564b7
Added more command validation. Moved some common behavior into privat…
anselor Jun 16, 2020
08afb76
Appears to be a type hinting olution that works for flake, sphinx, an…
anselor Jun 16, 2020
706682a
Sort imports using isort
tleonhardt Jun 18, 2020
86c4949
cleanup
anselor Jul 7, 2020
fdcd133
Adjusted decorators to accept variable positional parameters
anselor Jul 17, 2020
546fd97
Added an additional check for isinstance(method, Callable) since ther…
anselor Jul 18, 2020
91e5f9a
added additional documentation for new decorator behavior
anselor Jul 18, 2020
28517e5
Moved commandset tests into an isolated test
anselor Jul 21, 2020
475d53e
Removed support for functions outside of CommandSets
anselor Jul 21, 2020
2abe20a
Updates the example to remove usage of the now remove ability to
anselor Jul 24, 2020
ef8eef4
updated imports
anselor Jul 24, 2020
cc4485a
Adds support for injectable subcommands as part of CommandSet
anselor Jul 27, 2020
540bc17
Adds unit tests for sub-commands and additional commandset edge cases
anselor Jul 28, 2020
a4655f0
Suggested PR Fixes.
anselor Jul 29, 2020
fa85034
Removed sub-class and instead patch argparse._SubParsersAction
anselor Jul 30, 2020
1529800
Fixed typo in documentation
kmvanbrunt Jul 30, 2020
0e6e239
Fixed issue where we attempted to remove CommandSet from a list it wa…
kmvanbrunt Jul 30, 2020
4e673fc
Fixes to how command callables are filtered from CommandSet
anselor Jul 30, 2020
c03a144
Added handling for disabled commands to CommandSet functions
kmvanbrunt Jul 30, 2020
31bbe39
Updated documentation
kmvanbrunt Jul 31, 2020
229a353
Updated documentation
kmvanbrunt Jul 31, 2020
c58fec8
Fix a couple doc8 warnings
tleonhardt Jul 31, 2020
7c97f68
Fix it so py.test by itself doesn't crash
tleonhardt Jul 31, 2020
333096c
Updated Pipfile to do an editable/dev install of cmd2_ext_test so tha…
tleonhardt Jul 31, 2020
b1d7471
Ignore plugins directory when running doc8 by itself outside of invoke
tleonhardt Aug 1, 2020
51a3f76
Now maintains a command->CommandSet mapping and passes the CommandSet
anselor Aug 1, 2020
c0ad7ba
Updated changelog
anselor Aug 3, 2020
b932ad4
Minor formatting fixes. Injecting a function into namespace objects b…
anselor Aug 4, 2020
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.3.0 (August 4, 2020)
* Enchancements
* Added CommandSet - Enables defining a separate loadable module of commands to register/unregister
with your cmd2 application.

## 1.2.1 (July 14, 2020)
* Bug Fixes
* Relax minimum version of `importlib-metadata` to >= 1.6.0 when using Python < 3.8
Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ wcwidth = ">=0.1.7"

[dev-packages]
cmd2 = {editable = true,path = "."}
cmd2_ext_test = {editable = true,path = "plugins/ext_test"}
codecov = "*"
doc8 = "*"
flake8 = "*"
Expand Down
3 changes: 2 additions & 1 deletion cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER
from .argparse_custom import DEFAULT_ARGUMENT_PARSER
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_argparser_and_unknown_args, with_category
from .decorators import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category, as_subcommand_to
from .exceptions import Cmd2ArgparseError, SkipPostcommandHooks
from . import plugin
from .parsing import Statement
Expand Down
22 changes: 17 additions & 5 deletions cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CompletionItem,
generate_range_error,
)
from .command_definition import CommandSet
from .table_creator import Column, SimpleTable
from .utils import CompletionError, basic_complete

Expand Down Expand Up @@ -181,7 +182,8 @@ def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
if isinstance(action, argparse._SubParsersAction):
self._subcommand_action = action

def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int, *,
cmd_set: Optional[CommandSet] = None) -> List[str]:
"""
Complete the command using the argparse metadata and provided argument dictionary
:raises: CompletionError for various types of tab completion errors
Expand Down Expand Up @@ -358,7 +360,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:

completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app,
parent_tokens=parent_tokens)
return completer.complete_command(tokens[token_index:], text, line, begidx, endidx)
return completer.complete_command(tokens[token_index:], text, line, begidx, endidx,
cmd_set=cmd_set)
else:
# Invalid subcommand entered, so no way to complete remaining tokens
return []
Expand Down Expand Up @@ -403,7 +406,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
# Check if we are completing a flag's argument
if flag_arg_state is not None:
completion_results = self._complete_for_arg(flag_arg_state.action, text, line,
begidx, endidx, consumed_arg_values)
begidx, endidx, consumed_arg_values,
cmd_set=cmd_set)

# If we have results, then return them
if completion_results:
Expand All @@ -423,7 +427,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
pos_arg_state = _ArgumentState(action)

completion_results = self._complete_for_arg(pos_arg_state.action, text, line,
begidx, endidx, consumed_arg_values)
begidx, endidx, consumed_arg_values,
cmd_set=cmd_set)

# If we have results, then return them
if completion_results:
Expand Down Expand Up @@ -543,7 +548,8 @@ def format_help(self, tokens: List[str]) -> str:

def _complete_for_arg(self, arg_action: argparse.Action,
text: str, line: str, begidx: int, endidx: int,
consumed_arg_values: Dict[str, List[str]]) -> List[str]:
consumed_arg_values: Dict[str, List[str]], *,
cmd_set: Optional[CommandSet] = None) -> List[str]:
"""
Tab completion routine for an argparse argument
:return: list of completions
Expand All @@ -563,6 +569,12 @@ def _complete_for_arg(self, arg_action: argparse.Action,
kwargs = {}
if isinstance(arg_choices, ChoicesCallable):
if arg_choices.is_method:
cmd_set = getattr(self._parser, constants.PARSER_ATTR_COMMANDSET, cmd_set)
if cmd_set is not None:
if isinstance(cmd_set, CommandSet):
# If command is part of a CommandSet, `self` should be the CommandSet and Cmd will be next
if cmd_set is not None:
args.append(cmd_set)
args.append(self._cmd2_app)

# Check if arg_choices.to_call expects arg_tokens
Expand Down
126 changes: 106 additions & 20 deletions cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,32 @@ def my_choices_function():

parser.add_argument('-o', '--options', choices_function=my_choices_function)

``choices_method`` - this is exactly like choices_function, but the function
needs to be an instance method of a cmd2-based class. When ArgparseCompleter
calls the method, it will pass the app instance as the self argument. This is
good in cases where the list of choices being generated relies on state data of
the cmd2-based app

Example::
``choices_method`` - this is equivalent to choices_function, but the function
needs to be an instance method of a cmd2.Cmd or cmd2.CommandSet subclass. When
ArgparseCompleter calls the method, it well detect whether is is bound to a
CommandSet or Cmd subclass.
If bound to a cmd2.Cmd subclass, it will pass the app instance as the `self`
argument. This is good in cases where the list of choices being generated
relies on state data of the cmd2-based app.
If bound to a cmd2.CommandSet subclass, it will pass the CommandSet instance
as the `self` argument, and the app instance as the positional argument.

Example bound to cmd2.Cmd::

def my_choices_method(self):
...
return my_generated_list

parser.add_argument("arg", choices_method=my_choices_method)

Example bound to cmd2.CommandSEt::

def my_choices_method(self, app: cmd2.Cmd):
...
return my_generated_list

parser.add_argument("arg", choices_method=my_choices_method)

``completer_function`` - pass a tab completion function that does custom
completion. Since custom tab completion operations commonly need to modify
cmd2's instance variables related to tab completion, it will be rare to need a
Expand All @@ -84,10 +98,16 @@ def my_completer_function(text, line, begidx, endidx):
return completions
parser.add_argument('-o', '--options', completer_function=my_completer_function)

``completer_method`` - this is exactly like completer_function, but the
function needs to be an instance method of a cmd2-based class. When
ArgparseCompleter calls the method, it will pass the app instance as the self
argument. cmd2 provides a few completer methods for convenience (e.g.,
``completer_method`` - this is equivalent to completer_function, but the function
needs to be an instance method of a cmd2.Cmd or cmd2.CommandSet subclass. When
ArgparseCompleter calls the method, it well detect whether is is bound to a
CommandSet or Cmd subclass.
If bound to a cmd2.Cmd subclass, it will pass the app instance as the `self`
argument. This is good in cases where the list of choices being generated
relies on state data of the cmd2-based app.
If bound to a cmd2.CommandSet subclass, it will pass the CommandSet instance
as the `self` argument, and the app instance as the positional argument.
cmd2 provides a few completer methods for convenience (e.g.,
path_complete, delimiter_complete)

Example::
Expand Down Expand Up @@ -192,11 +212,15 @@ def my_completer_method(self, text, line, begidx, endidx, arg_tokens)
completion and enables nargs range parsing. See _add_argument_wrapper for
more details on these arguments.

``argparse.ArgumentParser._get_nargs_pattern`` - adds support to for nargs
ranges. See _get_nargs_pattern_wrapper for more details.
``argparse.ArgumentParser._get_nargs_pattern`` - adds support for nargs ranges.
See _get_nargs_pattern_wrapper for more details.

``argparse.ArgumentParser._match_argument`` - adds support to for nargs ranges.
``argparse.ArgumentParser._match_argument`` - adds support for nargs ranges.
See _match_argument_wrapper for more details.

``argparse._SubParsersAction.remove_parser`` - new function which removes a
sub-parser from a sub-parsers group. See _SubParsersAction_remove_parser for
more details.
"""

import argparse
Expand Down Expand Up @@ -528,6 +552,42 @@ def _match_argument_wrapper(self, action, arg_strings_pattern) -> int:
# noinspection PyProtectedMember
argparse.ArgumentParser._match_argument = _match_argument_wrapper


############################################################################################################
# Patch argparse._SubParsersAction to add remove_parser function
############################################################################################################

def _SubParsersAction_remove_parser(self, name: str):
"""
Removes a sub-parser from a sub-parsers group

This is a custom method being added to the argparse._SubParsersAction
class so cmd2 can remove subcommands from a parser.

:param self: instance of the _SubParsersAction being edited
:param name: name of the sub-parser to remove
"""
for choice_action in self._choices_actions:
if choice_action.dest == name:
self._choices_actions.remove(choice_action)
break

subparser = self._name_parser_map[name]
to_remove = []
for name, parser in self._name_parser_map.items():
if parser is subparser:
to_remove.append(name)
for name in to_remove:
del self._name_parser_map[name]

if name in self.choices:
del self.choices[name]


# noinspection PyProtectedMember
setattr(argparse._SubParsersAction, 'remove_parser', _SubParsersAction_remove_parser)


############################################################################################################
# Unless otherwise noted, everything below this point are copied from Python's
# argparse implementation with minor tweaks to adjust output.
Expand Down Expand Up @@ -728,14 +788,40 @@ def _format_args(self, action, default_metavar) -> str:
class Cmd2ArgumentParser(argparse.ArgumentParser):
"""Custom ArgumentParser class that improves error and help output"""

def __init__(self, *args, **kwargs) -> None:
if 'formatter_class' not in kwargs:
kwargs['formatter_class'] = Cmd2HelpFormatter

super().__init__(*args, **kwargs)
def __init__(self,
prog=None,
usage=None,
description=None,
epilog=None,
parents=None,
formatter_class=Cmd2HelpFormatter,
prefix_chars='-',
fromfile_prefix_chars=None,
argument_default=None,
conflict_handler='error',
add_help=True,
allow_abbrev=True) -> None:
super(Cmd2ArgumentParser, self).__init__(
prog=prog,
usage=usage,
description=description,
epilog=epilog,
parents=parents if parents else [],
formatter_class=formatter_class,
prefix_chars=prefix_chars,
fromfile_prefix_chars=fromfile_prefix_chars,
argument_default=argument_default,
conflict_handler=conflict_handler,
add_help=add_help,
allow_abbrev=allow_abbrev)

def add_subparsers(self, **kwargs):
"""Custom override. Sets a default title if one was not given."""
"""
Custom override. Sets a default title if one was not given.

:param kwargs: additional keyword arguments
:return: argparse Subparser Action
"""
if 'title' not in kwargs:
kwargs['title'] = 'subcommands'

Expand Down
Loading