From 787a31931ed4c4a18ae66a570d396b12b2b7b525 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Fri, 24 Jul 2020 12:03:59 -0400 Subject: Updates the example to remove usage of the now remove ability to register arbitrary functions as commands. Added example that demonstrates use of each of the command decorators with CommandSets. Adds unit test that verifies that CommandSets containing decorators load and process commands correctly. Updated the constructor declaration for Cmd2ArgumentParser to explicitly re-declare argparse constructor parameters. --- cmd2/argparse_custom.py | 31 ++++++-- examples/modular_commands/commandset_basic.py | 31 +------- examples/modular_commands/commandset_complex.py | 53 +++++++++++++ examples/modular_commands/commandset_custominit.py | 14 +--- examples/modular_commands_main.py | 1 + isolated_tests/test_commandset/test_commandset.py | 90 +++++++++++++++++----- 6 files changed, 154 insertions(+), 66 deletions(-) create mode 100644 examples/modular_commands/commandset_complex.py diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 485f65c2..74bddfc7 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -728,11 +728,32 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): 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.""" diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py index 25ba976d..105530e8 100644 --- a/examples/modular_commands/commandset_basic.py +++ b/examples/modular_commands/commandset_basic.py @@ -4,39 +4,10 @@ A simple example demonstrating a loadable command set """ from typing import List -from cmd2 import Cmd, CommandSet, Statement, register_command, with_category, with_default_category +from cmd2 import Cmd, CommandSet, Statement, with_category, with_default_category from cmd2.utils import CompletionError -@register_command -@with_category("AAA") -def do_unbound(cmd: Cmd, statement: Statement): - """This is an example of registering an unbound function - - :param cmd: - :param statement: - :return: - """ - cmd.poutput('Unbound Command: {}'.format(statement.args)) - - -@register_command -@with_category("AAA") -def do_func_with_help(cmd: Cmd, statement: Statement): - """ - This is an example of registering an unbound function - - :param cmd: - :param statement: - :return: - """ - cmd.poutput('Unbound Command: {}'.format(statement.args)) - - -def help_func_with_help(cmd: Cmd): - cmd.poutput('Help for func_with_help') - - @with_default_category('Basic Completion') class BasicCompletionCommandSet(CommandSet): # List of strings used with completion functions diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py new file mode 100644 index 00000000..5a031bd0 --- /dev/null +++ b/examples/modular_commands/commandset_complex.py @@ -0,0 +1,53 @@ +# coding=utf-8 +# flake8: noqa E302 +""" +Test CommandSet +""" + +import argparse +from typing import List + +import cmd2 +from cmd2 import utils + + +@cmd2.with_default_category('Fruits') +class CommandSetA(cmd2.CommandSet): + def do_apple(self, cmd: cmd2.Cmd, statement: cmd2.Statement): + cmd.poutput('Apple!') + + def do_banana(self, cmd: cmd2.Cmd, statement: cmd2.Statement): + """Banana Command""" + cmd.poutput('Banana!!') + + cranberry_parser = cmd2.Cmd2ArgumentParser('cranberry') + cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) + + @cmd2.with_argparser_and_unknown_args(cranberry_parser) + def do_cranberry(self, cmd: cmd2.Cmd, ns: argparse.Namespace, unknown: List[str]): + cmd.poutput('Cranberry {}!!'.format(ns.arg1)) + if unknown and len(unknown): + cmd.poutput('Unknown: ' + ', '.join(['{}']*len(unknown)).format(*unknown)) + cmd.last_result = {'arg1': ns.arg1, + 'unknown': unknown} + + def help_cranberry(self, cmd: cmd2.Cmd): + cmd.stdout.write('This command does diddly squat...\n') + + @cmd2.with_argument_list + @cmd2.with_category('Also Alone') + def do_durian(self, cmd: cmd2.Cmd, args: List[str]): + """Durian Command""" + cmd.poutput('{} Arguments: '.format(len(args))) + cmd.poutput(', '.join(['{}']*len(args)).format(*args)) + + def complete_durian(self, cmd: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: + return utils.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting']) + + elderberry_parser = cmd2.Cmd2ArgumentParser('elderberry') + elderberry_parser.add_argument('arg1') + + @cmd2.with_category('Alone') + @cmd2.with_argparser(elderberry_parser) + def do_elderberry(self, cmd: cmd2.Cmd, ns: argparse.Namespace): + cmd.poutput('Elderberry {}!!'.format(ns.arg1)) diff --git a/examples/modular_commands/commandset_custominit.py b/examples/modular_commands/commandset_custominit.py index d96c5f1c..fa26644b 100644 --- a/examples/modular_commands/commandset_custominit.py +++ b/examples/modular_commands/commandset_custominit.py @@ -2,19 +2,7 @@ """ A simple example demonstrating a loadable command set """ -from cmd2 import Cmd, CommandSet, Statement, register_command, with_category, with_default_category - - -@register_command -@with_category("AAA") -def do_another_command(cmd: Cmd, statement: Statement): - """ - This is an example of registering an unbound function - :param cmd: - :param statement: - :return: - """ - cmd.poutput('Another Unbound Command: {}'.format(statement.args)) +from cmd2 import Cmd, CommandSet, Statement, with_category, with_default_category @with_default_category('Custom Init') diff --git a/examples/modular_commands_main.py b/examples/modular_commands_main.py index 9e7f79cc..fd10d8d3 100644 --- a/examples/modular_commands_main.py +++ b/examples/modular_commands_main.py @@ -10,6 +10,7 @@ from cmd2 import Cmd, Cmd2ArgumentParser, CommandSet, CompletionItem, with_argpa from cmd2.utils import CompletionError, basic_complete from modular_commands.commandset_basic import BasicCompletionCommandSet # noqa: F401 from modular_commands.commandset_custominit import CustomInitCommandSet # noqa: F401 +from modular_commands.commandset_complex import CommandSetA # noqa: F401 # Data source for argparse.choices food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] diff --git a/isolated_tests/test_commandset/test_commandset.py b/isolated_tests/test_commandset/test_commandset.py index 8de2d3b0..023ea30d 100644 --- a/isolated_tests/test_commandset/test_commandset.py +++ b/isolated_tests/test_commandset/test_commandset.py @@ -4,17 +4,17 @@ Test CommandSet """ +import argparse from typing import List import pytest import cmd2 from cmd2 import utils +from cmd2_ext_test import ExternalTestMixin -from .conftest import complete_tester, normalize, run_cmd - -@cmd2.with_default_category('Command Set') +@cmd2.with_default_category('Fruits') class CommandSetA(cmd2.CommandSet): def do_apple(self, cmd: cmd2.Cmd, statement: cmd2.Statement): cmd.poutput('Apple!') @@ -23,28 +23,45 @@ class CommandSetA(cmd2.CommandSet): """Banana Command""" cmd.poutput('Banana!!') - def do_cranberry(self, cmd: cmd2.Cmd, statement: cmd2.Statement): - cmd.poutput('Cranberry!!') + cranberry_parser = cmd2.Cmd2ArgumentParser('cranberry') + cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) + + @cmd2.with_argparser_and_unknown_args(cranberry_parser) + def do_cranberry(self, cmd: cmd2.Cmd, ns: argparse.Namespace, unknown: List[str]): + cmd.poutput('Cranberry {}!!'.format(ns.arg1)) + if unknown and len(unknown): + cmd.poutput('Unknown: ' + ', '.join(['{}']*len(unknown)).format(*unknown)) + cmd.last_result = {'arg1': ns.arg1, + 'unknown': unknown} def help_cranberry(self, cmd: cmd2.Cmd): cmd.stdout.write('This command does diddly squat...\n') - def do_durian(self, cmd: cmd2.Cmd, statement: cmd2.Statement): + @cmd2.with_argument_list + @cmd2.with_category('Also Alone') + def do_durian(self, cmd: cmd2.Cmd, args: List[str]): """Durian Command""" - cmd.poutput('Durian!!') + cmd.poutput('{} Arguments: '.format(len(args))) + cmd.poutput(', '.join(['{}']*len(args)).format(*args)) + cmd.last_result = {'args': args} def complete_durian(self, cmd: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: return utils.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting']) + elderberry_parser = cmd2.Cmd2ArgumentParser('elderberry') + elderberry_parser.add_argument('arg1') + @cmd2.with_category('Alone') - def do_elderberry(self, cmd: cmd2.Cmd, statement: cmd2.Statement): - cmd.poutput('Elderberry!!') + @cmd2.with_argparser(elderberry_parser) + def do_elderberry(self, cmd: cmd2.Cmd, ns: argparse.Namespace): + cmd.poutput('Elderberry {}!!'.format(ns.arg1)) + cmd.last_result = {'arg1': ns.arg1} -class WithCommandSets(cmd2.Cmd): +class WithCommandSets(ExternalTestMixin, cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(WithCommandSets, self).__init__(*args, **kwargs) @cmd2.with_default_category('Command Set B') @@ -84,8 +101,11 @@ def test_autoload_commands(command_sets_app): assert 'Alone' in cmds_cats assert 'elderberry' in cmds_cats['Alone'] - assert 'Command Set' in cmds_cats - assert 'cranberry' in cmds_cats['Command Set'] + assert 'Also Alone' in cmds_cats + assert 'durian' in cmds_cats['Also Alone'] + + assert 'Fruits' in cmds_cats + assert 'cranberry' in cmds_cats['Fruits'] def test_custom_construct_commandsets(): @@ -96,6 +116,7 @@ def test_custom_construct_commandsets(): cmds_cats, cmds_doc, cmds_undoc, help_topics = app._build_command_info() assert 'Command Set B' in cmds_cats + # Verifies that the same CommandSet can not be loaded twice command_set_2 = CommandSetB('bar') with pytest.raises(ValueError): assert app.install_command_set(command_set_2) @@ -112,8 +133,8 @@ def test_load_commands(command_sets_manual): assert 'Alone' in cmds_cats assert 'elderberry' in cmds_cats['Alone'] - assert 'Command Set' in cmds_cats - assert 'cranberry' in cmds_cats['Command Set'] + assert 'Fruits' in cmds_cats + assert 'cranberry' in cmds_cats['Fruits'] # uninstall the command set and verify it is now also no longer accessible command_sets_manual.uninstall_command_set(cmd_set) @@ -121,7 +142,7 @@ def test_load_commands(command_sets_manual): cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info() assert 'Alone' not in cmds_cats - assert 'Command Set' not in cmds_cats + assert 'Fruits' not in cmds_cats # reinstall the command set and verify it is accessible command_sets_manual.install_command_set(cmd_set) @@ -131,8 +152,8 @@ def test_load_commands(command_sets_manual): assert 'Alone' in cmds_cats assert 'elderberry' in cmds_cats['Alone'] - assert 'Command Set' in cmds_cats - assert 'cranberry' in cmds_cats['Command Set'] + assert 'Fruits' in cmds_cats + assert 'cranberry' in cmds_cats['Fruits'] def test_partial_with_passthru(): @@ -158,3 +179,36 @@ def test_partial_with_passthru(): assert hasattr(test_func, 'Bar') assert getattr(test_func, 'Bar', None) == 6 + + +def test_commandset_decorators(command_sets_app): + result = command_sets_app.app_cmd('cranberry juice extra1 extra2') + assert len(result.data['unknown']) == 2 + assert 'extra1' in result.data['unknown'] + assert 'extra2' in result.data['unknown'] + assert result.data['arg1'] == 'juice' + assert result.stderr is None + + result = command_sets_app.app_cmd('durian juice extra1 extra2') + assert len(result.data['args']) == 3 + assert 'juice' in result.data['args'] + assert 'extra1' in result.data['args'] + assert 'extra2' in result.data['args'] + assert result.stderr is None + + result = command_sets_app.app_cmd('durian') + assert len(result.data['args']) == 0 + assert result.stderr is None + + result = command_sets_app.app_cmd('elderberry') + assert result.stderr is not None + assert len(result.stderr) > 0 + assert 'arguments are required' in result.stderr + assert result.data is None + + result = command_sets_app.app_cmd('elderberry a b') + assert result.stderr is not None + assert len(result.stderr) > 0 + assert 'unrecognized arguments' in result.stderr + assert result.data is None + -- cgit v1.2.1