Skip to content
Snippets Groups Projects
Select Git revision
  • 1bfb2578822e7112732b5f346832298971b11b9d
  • main default
  • pristine-tar
  • upstream
  • debian/2.5.8+ds-1
  • upstream/2.5.8+ds
  • debian/2.5.7+ds-1
  • upstream/2.5.7+ds
  • debian/2.5.4+ds-1
  • upstream/2.5.4+ds
  • debian/2.4.3+ds-2
  • debian/2.4.3+ds-1
  • upstream/2.4.3+ds
  • debian/2.4.2+ds-3
  • debian/2.4.2+ds-2
  • debian/2.4.2+ds-1
  • upstream/2.4.2+ds
  • debian/2.3.3-1
  • upstream/2.3.3
  • debian/2.1.2-1
  • upstream/2.1.2
  • debian/1.5.0-1
  • upstream/1.5.0
  • upstream/1.4.0
24 results

test_commandset.py

Blame
  • test_commandset.py 42.13 KiB
    # coding=utf-8
    # flake8: noqa E302
    """
    Test CommandSet
    """
    
    import argparse
    from typing import (
        List,
    )
    
    import pytest
    
    import cmd2
    from cmd2 import (
        Settable,
    )
    from cmd2.exceptions import (
        CommandSetRegistrationError,
    )
    
    from .conftest import (
        WithCommandSets,
        complete_tester,
        normalize,
        run_cmd,
    )
    
    
    class CommandSetBase(cmd2.CommandSet):
        pass
    
    
    @cmd2.with_default_category('Fruits')
    class CommandSetA(CommandSetBase):
        def on_register(self, cmd) -> None:
            super().on_register(cmd)
            print("in on_register now")
    
        def on_registered(self) -> None:
            super().on_registered()
            print("in on_registered now")
    
        def on_unregister(self) -> None:
            super().on_unregister()
            print("in on_unregister now")
    
        def on_unregistered(self) -> None:
            super().on_unregistered()
            print("in on_unregistered now")
    
        def do_apple(self, statement: cmd2.Statement):
            self._cmd.poutput('Apple!')
    
        def do_banana(self, statement: cmd2.Statement):
            """Banana Command"""
            self._cmd.poutput('Banana!!')
    
        cranberry_parser = cmd2.Cmd2ArgumentParser()
        cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce'])
    
        @cmd2.with_argparser(cranberry_parser, with_unknown_args=True)
        def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]):
            self._cmd.poutput('Cranberry {}!!'.format(ns.arg1))
            if unknown and len(unknown):
                self._cmd.poutput('Unknown: ' + ', '.join(['{}'] * len(unknown)).format(*unknown))
            self._cmd.last_result = {'arg1': ns.arg1, 'unknown': unknown}
    
        def help_cranberry(self):
            self._cmd.stdout.write('This command does diddly squat...\n')
    
        @cmd2.with_argument_list
        @cmd2.with_category('Also Alone')
        def do_durian(self, args: List[str]):
            """Durian Command"""
            self._cmd.poutput('{} Arguments: '.format(len(args)))
            self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args))
            self._cmd.last_result = {'args': args}
    
        def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
            return self._cmd.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting'])
    
        elderberry_parser = cmd2.Cmd2ArgumentParser()
        elderberry_parser.add_argument('arg1')
    
        @cmd2.with_category('Alone')
        @cmd2.with_argparser(elderberry_parser)
        def do_elderberry(self, ns: argparse.Namespace):
            self._cmd.poutput('Elderberry {}!!'.format(ns.arg1))
            self._cmd.last_result = {'arg1': ns.arg1}
    
        # Test that CommandSet with as_subcommand_to decorator successfully loads
        # during `cmd2.Cmd.__init__()`.
        main_parser = cmd2.Cmd2ArgumentParser(description="Main Command")
        main_subparsers = main_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
        main_subparsers.required = True
    
        @cmd2.with_category('Alone')
        @cmd2.with_argparser(main_parser)
        def do_main(self, args: argparse.Namespace) -> None:
            # Call handler for whatever subcommand was selected
            handler = args.cmd2_handler.get()
            handler(args)
    
        # main -> sub
        subcmd_parser = cmd2.Cmd2ArgumentParser(description="Sub Command")
    
        @cmd2.as_subcommand_to('main', 'sub', subcmd_parser, help="sub command")
        def subcmd_func(self, args: argparse.Namespace) -> None:
            self._cmd.poutput("Subcommand Ran")
    
    
    @cmd2.with_default_category('Command Set B')
    class CommandSetB(CommandSetBase):
        def __init__(self, arg1):
            super().__init__()
            self._arg1 = arg1
    
        def do_aardvark(self, statement: cmd2.Statement):
            self._cmd.poutput('Aardvark!')
    
        def do_bat(self, statement: cmd2.Statement):
            """Banana Command"""
            self._cmd.poutput('Bat!!')
    
        def do_crocodile(self, statement: cmd2.Statement):
            self._cmd.poutput('Crocodile!!')
    
    
    def test_autoload_commands(command_sets_app):
        # verifies that, when autoload is enabled, CommandSets and registered functions all show up
    
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_app._build_command_info()
    
        assert 'Alone' in cmds_cats
        assert 'elderberry' in cmds_cats['Alone']
        assert 'main' in cmds_cats['Alone']
    
        # Test subcommand was autoloaded
        result = command_sets_app.app_cmd('main sub')
        assert 'Subcommand Ran' in result.stdout
    
        assert 'Also Alone' in cmds_cats
        assert 'durian' in cmds_cats['Also Alone']
    
        assert 'Fruits' in cmds_cats
        assert 'cranberry' in cmds_cats['Fruits']
    
        assert 'Command Set B' not in cmds_cats
    
    
    def test_custom_construct_commandsets():
        # Verifies that a custom initialized CommandSet loads correctly when passed into the constructor
        command_set_b = CommandSetB('foo')
        app = WithCommandSets(command_sets=[command_set_b])
    
        cmds_cats, cmds_doc, cmds_undoc, help_topics = app._build_command_info()
        assert 'Command Set B' in cmds_cats
    
        # Verifies that the same CommandSet cannot be loaded twice
        command_set_2 = CommandSetB('bar')
        with pytest.raises(CommandSetRegistrationError):
            assert app.register_command_set(command_set_2)
    
        # Verify that autoload doesn't conflict with a manually loaded CommandSet that could be autoloaded.
        command_set_a = CommandSetA()
        app2 = WithCommandSets(command_sets=[command_set_a])
    
        with pytest.raises(CommandSetRegistrationError):
            app2.register_command_set(command_set_b)
    
        app.unregister_command_set(command_set_b)
    
        app2.register_command_set(command_set_b)
    
        assert hasattr(app2, 'do_apple')
        assert hasattr(app2, 'do_aardvark')
    
        assert app2.find_commandset_for_command('aardvark') is command_set_b
        assert app2.find_commandset_for_command('apple') is command_set_a
    
        matches = app2.find_commandsets(CommandSetBase, subclass_match=True)
        assert command_set_a in matches
        assert command_set_b in matches
        assert command_set_2 not in matches
    
    
    def test_load_commands(command_sets_manual, capsys):
    
        # now install a command set and verify the commands are now present
        cmd_set = CommandSetA()
    
        assert command_sets_manual.find_commandset_for_command('elderberry') is None
        assert not command_sets_manual.find_commandsets(CommandSetA)
    
        command_sets_manual.register_command_set(cmd_set)
    
        assert command_sets_manual.find_commandsets(CommandSetA)[0] is cmd_set
        assert command_sets_manual.find_commandset_for_command('elderberry') is cmd_set
    
        # Make sure registration callbacks ran
        out, err = capsys.readouterr()
        assert "in on_register now" in out
        assert "in on_registered now" in out
    
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
    
        assert 'Alone' in cmds_cats
        assert 'elderberry' in cmds_cats['Alone']
        assert 'main' in cmds_cats['Alone']
    
        # Test subcommand was loaded
        result = command_sets_manual.app_cmd('main sub')
        assert 'Subcommand Ran' in result.stdout
    
        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.unregister_command_set(cmd_set)
    
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
    
        assert 'Alone' not in cmds_cats
        assert 'Fruits' not in cmds_cats
    
        # Make sure unregistration callbacks ran
        out, err = capsys.readouterr()
        assert "in on_unregister now" in out
        assert "in on_unregistered now" in out
    
        # uninstall a second time and verify no errors happen
        command_sets_manual.unregister_command_set(cmd_set)
    
        # reinstall the command set and verify it is accessible
        command_sets_manual.register_command_set(cmd_set)
    
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
    
        assert 'Alone' in cmds_cats
        assert 'elderberry' in cmds_cats['Alone']
        assert 'main' in cmds_cats['Alone']
    
        # Test subcommand was loaded
        result = command_sets_manual.app_cmd('main sub')
        assert 'Subcommand Ran' in result.stdout
    
        assert 'Fruits' in cmds_cats
        assert 'cranberry' in cmds_cats['Fruits']
    
    
    def test_commandset_decorators(command_sets_app):
        result = command_sets_app.app_cmd('cranberry juice extra1 extra2')
        assert result is not None
        assert result.data is not None
        assert len(result.data['unknown']) == 2
        assert 'extra1' in result.data['unknown']
        assert 'extra2' in result.data['unknown']
        assert result.data['arg1'] == 'juice'
        assert not result.stderr
    
        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 not result.stderr
    
        result = command_sets_app.app_cmd('durian')
        assert len(result.data['args']) == 0
        assert not result.stderr
    
        result = command_sets_app.app_cmd('elderberry')
        assert 'arguments are required' in result.stderr
        assert result.data is None
    
        result = command_sets_app.app_cmd('elderberry a b')
        assert 'unrecognized arguments' in result.stderr
        assert result.data is None
    
    
    def test_load_commandset_errors(command_sets_manual, capsys):
        cmd_set = CommandSetA()
    
        # create a conflicting command before installing CommandSet to verify rollback behavior
        command_sets_manual._install_command_function('durian', cmd_set.do_durian)
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual.register_command_set(cmd_set)
    
        # verify that the commands weren't installed
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
    
        assert 'Alone' not in cmds_cats
        assert 'Fruits' not in cmds_cats
        assert not command_sets_manual._installed_command_sets
    
        delattr(command_sets_manual, 'do_durian')
    
        # pre-create intentionally conflicting macro and alias names
        command_sets_manual.app_cmd('macro create apple run_pyscript')
        command_sets_manual.app_cmd('alias create banana run_pyscript')
    
        # now install a command set and verify the commands are now present
        command_sets_manual.register_command_set(cmd_set)
        out, err = capsys.readouterr()
    
        # verify aliases and macros are deleted with warning if they conflict with a command
        assert "Deleting alias 'banana'" in err
        assert "Deleting macro 'apple'" in err
    
        # verify duplicate commands are detected
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual._install_command_function('banana', cmd_set.do_banana)
    
        # verify bad command names are detected
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual._install_command_function('bad command', cmd_set.do_banana)
    
        # verify error conflict with existing completer function
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual._install_completer_function('durian', cmd_set.complete_durian)
    
        # verify error conflict with existing help function
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual._install_help_function('cranberry', cmd_set.help_cranberry)
    
    
    class LoadableBase(cmd2.CommandSet):
        def __init__(self, dummy):
            super(LoadableBase, self).__init__()
            self._dummy = dummy  # prevents autoload
            self._cut_called = False
    
        cut_parser = cmd2.Cmd2ArgumentParser()
        cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
    
        def namespace_provider(self) -> argparse.Namespace:
            ns = argparse.Namespace()
            ns.cut_called = self._cut_called
            return ns
    
        @cmd2.with_argparser(cut_parser)
        def do_cut(self, ns: argparse.Namespace):
            """Cut something"""
            handler = ns.cmd2_handler.get()
            if handler is not None:
                # Call whatever subcommand function was selected
                handler(ns)
                self._cut_called = True
            else:
                # No subcommand was provided, so call help
                self._cmd.pwarning('This command does nothing without sub-parsers registered')
                self._cmd.do_help('cut')
    
        stir_parser = cmd2.Cmd2ArgumentParser()
        stir_subparsers = stir_parser.add_subparsers(title='item', help='what to stir')
    
        @cmd2.with_argparser(stir_parser, ns_provider=namespace_provider)
        def do_stir(self, ns: argparse.Namespace):
            """Stir something"""
            if not ns.cut_called:
                self._cmd.poutput('Need to cut before stirring')
                return
    
            handler = ns.cmd2_handler.get()
            if handler is not None:
                # Call whatever subcommand function was selected
                handler(ns)
            else:
                # No subcommand was provided, so call help
                self._cmd.pwarning('This command does nothing without sub-parsers registered')
                self._cmd.do_help('stir')
    
        stir_pasta_parser = cmd2.Cmd2ArgumentParser()
        stir_pasta_parser.add_argument('--option', '-o')
        stir_pasta_parser.add_subparsers(title='style', help='Stir style')
    
        @cmd2.as_subcommand_to('stir', 'pasta', stir_pasta_parser)
        def stir_pasta(self, ns: argparse.Namespace):
            handler = ns.cmd2_handler.get()
            if handler is not None:
                # Call whatever subcommand function was selected
                handler(ns)
            else:
                self._cmd.poutput('Stir pasta haphazardly')
    
    
    class LoadableBadBase(cmd2.CommandSet):
        def __init__(self, dummy):
            super(LoadableBadBase, self).__init__()
            self._dummy = dummy  # prevents autoload
    
        def do_cut(self, ns: argparse.Namespace):
            """Cut something"""
            handler = ns.cmd2_handler.get()
            if handler is not None:
                # Call whatever subcommand function was selected
                handler(ns)
            else:
                # No subcommand was provided, so call help
                self._cmd.poutput('This command does nothing without sub-parsers registered')
                self._cmd.do_help('cut')
    
    
    @cmd2.with_default_category('Fruits')
    class LoadableFruits(cmd2.CommandSet):
        def __init__(self, dummy):
            super(LoadableFruits, self).__init__()
            self._dummy = dummy  # prevents autoload
    
        def do_apple(self, _: cmd2.Statement):
            self._cmd.poutput('Apple')
    
        banana_parser = cmd2.Cmd2ArgumentParser()
        banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
    
        @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer'])
        def cut_banana(self, ns: argparse.Namespace):
            """Cut banana"""
            self._cmd.poutput('cutting banana: ' + ns.direction)
    
    
    class LoadablePastaStir(cmd2.CommandSet):
        def __init__(self, dummy):
            super(LoadablePastaStir, self).__init__()
            self._dummy = dummy  # prevents autoload
    
        stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser()
        stir_pasta_vigor_parser.add_argument('frequency')
    
        @cmd2.as_subcommand_to('stir pasta', 'vigorously', stir_pasta_vigor_parser)
        def stir_pasta_vigorously(self, ns: argparse.Namespace):
            self._cmd.poutput('stir the pasta vigorously')
    
    
    @cmd2.with_default_category('Vegetables')
    class LoadableVegetables(cmd2.CommandSet):
        def __init__(self, dummy):
            super(LoadableVegetables, self).__init__()
            self._dummy = dummy  # prevents autoload
    
        def do_arugula(self, _: cmd2.Statement):
            self._cmd.poutput('Arugula')
    
        def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
            return ['quartered', 'diced']
    
        bokchoy_parser = cmd2.Cmd2ArgumentParser()
        bokchoy_parser.add_argument('style', completer=complete_style_arg)
    
        @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser)
        def cut_bokchoy(self, ns: argparse.Namespace):
            self._cmd.poutput('Bok Choy: ' + ns.style)
    
    
    def test_subcommands(command_sets_manual):
    
        base_cmds = LoadableBase(1)
        badbase_cmds = LoadableBadBase(1)
        fruit_cmds = LoadableFruits(1)
        veg_cmds = LoadableVegetables(1)
    
        # installing subcommands without base command present raises exception
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual.register_command_set(fruit_cmds)
    
        # if the base command is present but isn't an argparse command, expect exception
        command_sets_manual.register_command_set(badbase_cmds)
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual.register_command_set(fruit_cmds)
    
        # verify that the commands weren't installed
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
        assert 'cut' in cmds_doc
        assert 'Fruits' not in cmds_cats
    
        # Now install the good base commands
        command_sets_manual.unregister_command_set(badbase_cmds)
        command_sets_manual.register_command_set(base_cmds)
    
        # verify that we catch an attempt to register subcommands when the commandset isn't installed
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual._register_subcommands(fruit_cmds)
    
        cmd_result = command_sets_manual.app_cmd('cut')
        assert 'This command does nothing without sub-parsers registered' in cmd_result.stderr
    
        # verify that command set install without problems
        command_sets_manual.register_command_set(fruit_cmds)
        command_sets_manual.register_command_set(veg_cmds)
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
        assert 'Fruits' in cmds_cats
    
        text = ''
        line = 'cut {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
    
        assert first_match is not None
        # check that the alias shows up correctly
        assert ['banana', 'bananer', 'bokchoy'] == command_sets_manual.completion_matches
    
        cmd_result = command_sets_manual.app_cmd('cut banana discs')
        assert 'cutting banana: discs' in cmd_result.stdout
    
        text = ''
        line = 'cut bokchoy {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
    
        assert first_match is not None
        # verify that argparse completer in commandset functions correctly
        assert ['diced', 'quartered'] == command_sets_manual.completion_matches
    
        # verify that command set uninstalls without problems
        command_sets_manual.unregister_command_set(fruit_cmds)
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
        assert 'Fruits' not in cmds_cats
    
        # verify a double-unregister raises exception
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual._unregister_subcommands(fruit_cmds)
        command_sets_manual.unregister_command_set(veg_cmds)
    
        # Disable command and verify subcommands still load and unload
        command_sets_manual.disable_command('cut', 'disabled for test')
    
        # verify that command set install without problems
        command_sets_manual.register_command_set(fruit_cmds)
        command_sets_manual.register_command_set(veg_cmds)
    
        command_sets_manual.enable_command('cut')
    
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
        assert 'Fruits' in cmds_cats
    
        text = ''
        line = 'cut {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
    
        assert first_match is not None
        # check that the alias shows up correctly
        assert ['banana', 'bananer', 'bokchoy'] == command_sets_manual.completion_matches
    
        text = ''
        line = 'cut bokchoy {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
    
        assert first_match is not None
        # verify that argparse completer in commandset functions correctly
        assert ['diced', 'quartered'] == command_sets_manual.completion_matches
    
        # disable again and verify can still uninstnall
        command_sets_manual.disable_command('cut', 'disabled for test')
    
        # verify that command set uninstalls without problems
        command_sets_manual.unregister_command_set(fruit_cmds)
        cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_manual._build_command_info()
        assert 'Fruits' not in cmds_cats
    
        # verify a double-unregister raises exception
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual._unregister_subcommands(fruit_cmds)
    
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual.unregister_command_set(base_cmds)
    
        command_sets_manual.unregister_command_set(veg_cmds)
        command_sets_manual.unregister_command_set(base_cmds)
    
    
    def test_nested_subcommands(command_sets_manual):
        base_cmds = LoadableBase(1)
        pasta_cmds = LoadablePastaStir(1)
    
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual.register_command_set(pasta_cmds)
    
        command_sets_manual.register_command_set(base_cmds)
    
        command_sets_manual.register_command_set(pasta_cmds)
    
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual.unregister_command_set(base_cmds)
    
        class BadNestedSubcommands(cmd2.CommandSet):
            def __init__(self, dummy):
                super(BadNestedSubcommands, self).__init__()
                self._dummy = dummy  # prevents autoload
    
            stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser()
            stir_pasta_vigor_parser.add_argument('frequency')
    
            # stir sauce doesn't exist anywhere, this should fail
            @cmd2.as_subcommand_to('stir sauce', 'vigorously', stir_pasta_vigor_parser)
            def stir_pasta_vigorously(self, ns: argparse.Namespace):
                self._cmd.poutput('stir the pasta vigorously')
    
        with pytest.raises(CommandSetRegistrationError):
            command_sets_manual.register_command_set(BadNestedSubcommands(1))
    
        fruit_cmds = LoadableFruits(1)
        command_sets_manual.register_command_set(fruit_cmds)
    
        # validates custom namespace provider works correctly. Stir command will fail until
        # the cut command is called
        result = command_sets_manual.app_cmd('stir pasta vigorously everyminute')
        assert 'Need to cut before stirring' in result.stdout
    
        result = command_sets_manual.app_cmd('cut banana discs')
        assert 'cutting banana: discs' in result.stdout
    
        result = command_sets_manual.app_cmd('stir pasta vigorously everyminute')
        assert 'stir the pasta vigorously' in result.stdout
    
    
    class AppWithSubCommands(cmd2.Cmd):
        """Class for testing usage of `as_subcommand_to` decorator directly in a Cmd2 subclass."""
    
        def __init__(self, *args, **kwargs):
            super(AppWithSubCommands, self).__init__(*args, **kwargs)
    
        cut_parser = cmd2.Cmd2ArgumentParser()
        cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
    
        @cmd2.with_argparser(cut_parser)
        def do_cut(self, ns: argparse.Namespace):
            """Cut something"""
            handler = ns.cmd2_handler.get()
            if handler is not None:
                # Call whatever subcommand function was selected
                handler(ns)
            else:
                # No subcommand was provided, so call help
                self.poutput('This command does nothing without sub-parsers registered')
                self.do_help('cut')
    
        banana_parser = cmd2.Cmd2ArgumentParser()
        banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
    
        @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer'])
        def cut_banana(self, ns: argparse.Namespace):
            """Cut banana"""
            self.poutput('cutting banana: ' + ns.direction)
    
        def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
            return ['quartered', 'diced']
    
        bokchoy_parser = cmd2.Cmd2ArgumentParser()
        bokchoy_parser.add_argument('style', completer=complete_style_arg)
    
        @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser)
        def cut_bokchoy(self, _: argparse.Namespace):
            self.poutput('Bok Choy')
    
    
    @pytest.fixture
    def static_subcommands_app():
        app = AppWithSubCommands()
        return app
    
    
    def test_static_subcommands(static_subcommands_app):
        cmds_cats, cmds_doc, cmds_undoc, help_topics = static_subcommands_app._build_command_info()
        assert 'Fruits' in cmds_cats
    
        text = ''
        line = 'cut {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, static_subcommands_app)
    
        assert first_match is not None
        # check that the alias shows up correctly
        assert ['banana', 'bananer', 'bokchoy'] == static_subcommands_app.completion_matches
    
        text = ''
        line = 'cut bokchoy {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, static_subcommands_app)
    
        assert first_match is not None
        # verify that argparse completer in commandset functions correctly
        assert ['diced', 'quartered'] == static_subcommands_app.completion_matches
    
    
    complete_states_expected_self = None
    
    
    @cmd2.with_default_category('With Completer')
    class SupportFuncProvider(cmd2.CommandSet):
        """CommandSet which provides a support function (complete_states) to other CommandSets"""
    
        states = ['alabama', 'alaska', 'arizona', 'arkansas', 'california', 'colorado', 'connecticut', 'delaware']
    
        def __init__(self, dummy):
            """dummy variable prevents this from being autoloaded in other tests"""
            super(SupportFuncProvider, self).__init__()
    
        def complete_states(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
            assert self is complete_states_expected_self
            return self._cmd.basic_complete(text, line, begidx, endidx, self.states)
    
    
    class SupportFuncUserSubclass1(SupportFuncProvider):
        """A sub-class of SupportFuncProvider which uses its support function"""
    
        parser = cmd2.Cmd2ArgumentParser()
        parser.add_argument('state', type=str, completer=SupportFuncProvider.complete_states)
    
        @cmd2.with_argparser(parser)
        def do_user_sub1(self, ns: argparse.Namespace):
            self._cmd.poutput('something {}'.format(ns.state))
    
    
    class SupportFuncUserSubclass2(SupportFuncProvider):
        """A second sub-class of SupportFuncProvider which uses its support function"""
    
        parser = cmd2.Cmd2ArgumentParser()
        parser.add_argument('state', type=str, completer=SupportFuncProvider.complete_states)
    
        @cmd2.with_argparser(parser)
        def do_user_sub2(self, ns: argparse.Namespace):
            self._cmd.poutput('something {}'.format(ns.state))
    
    
    class SupportFuncUserUnrelated(cmd2.CommandSet):
        """A CommandSet that isn't related to SupportFuncProvider which uses its support function"""
    
        def __init__(self, dummy):
            """dummy variable prevents this from being autoloaded in other tests"""
            super(SupportFuncUserUnrelated, self).__init__()
    
        parser = cmd2.Cmd2ArgumentParser()
        parser.add_argument('state', type=str, completer=SupportFuncProvider.complete_states)
    
        @cmd2.with_argparser(parser)
        def do_user_unrelated(self, ns: argparse.Namespace):
            self._cmd.poutput('something {}'.format(ns.state))
    
    
    def test_cross_commandset_completer(command_sets_manual, capsys):
        global complete_states_expected_self
        # This tests the different ways to locate the matching CommandSet when completing an argparse argument.
        # Exercises the 3 cases in cmd2.Cmd._resolve_func_self() which is called during argparse tab completion.
    
        # Create all the CommandSets for these tests
        func_provider = SupportFuncProvider(1)
        user_sub1 = SupportFuncUserSubclass1(2)
        user_sub2 = SupportFuncUserSubclass2(3)
        user_unrelated = SupportFuncUserUnrelated(4)
    
        ####################################################################################################################
        # This exercises Case 1
        # If the CommandSet holding a command is a sub-class of the class that defines the completer function, then use that
        # CommandSet instance as self when calling the completer
    
        # Create instances of two different sub-class types to ensure no one removes the case 1 check in Cmd._resolve_func_self().
        # If that check is removed, testing with only 1 sub-class type will still pass. Testing it with two sub-class types
        # will fail and show that the case 1 check cannot be removed.
        command_sets_manual.register_command_set(user_sub1)
        command_sets_manual.register_command_set(user_sub2)
    
        text = ''
        line = 'user_sub1 {}'.format(text)
        endidx = len(line)
        begidx = endidx
        complete_states_expected_self = user_sub1
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
        complete_states_expected_self = None
    
        assert first_match == 'alabama'
        assert command_sets_manual.completion_matches == SupportFuncProvider.states
    
        assert (
            getattr(command_sets_manual.cmd_func('user_sub1').__func__, cmd2.constants.CMD_ATTR_HELP_CATEGORY) == 'With Completer'
        )
    
        command_sets_manual.unregister_command_set(user_sub2)
        command_sets_manual.unregister_command_set(user_sub1)
    
        ####################################################################################################################
        # This exercises Case 2
        # If the CommandSet holding a command is unrelated to the CommandSet holding the completer function, then search
        # all installed CommandSet instances for one that is an exact type match
    
        command_sets_manual.register_command_set(func_provider)
        command_sets_manual.register_command_set(user_unrelated)
    
        text = ''
        line = 'user_unrelated {}'.format(text)
        endidx = len(line)
        begidx = endidx
        complete_states_expected_self = func_provider
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
        complete_states_expected_self = None
    
        assert first_match == 'alabama'
        assert command_sets_manual.completion_matches == SupportFuncProvider.states
    
        command_sets_manual.unregister_command_set(user_unrelated)
        command_sets_manual.unregister_command_set(func_provider)
    
        ####################################################################################################################
        # This exercises Case 3
        # If the CommandSet holding a command is unrelated to the CommandSet holding the completer function,
        # and no exact type match can be found, but sub-class matches can be found and there is only a single
        # sub-class match, then use the lone sub-class match as the parent CommandSet.
    
        command_sets_manual.register_command_set(user_sub1)
        command_sets_manual.register_command_set(user_unrelated)
    
        text = ''
        line = 'user_unrelated {}'.format(text)
        endidx = len(line)
        begidx = endidx
        complete_states_expected_self = user_sub1
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
        complete_states_expected_self = None
    
        assert first_match == 'alabama'
        assert command_sets_manual.completion_matches == SupportFuncProvider.states
    
        command_sets_manual.unregister_command_set(user_unrelated)
        command_sets_manual.unregister_command_set(user_sub1)
    
        ####################################################################################################################
        # Error Case 1
        # If the CommandSet holding a command is unrelated to the CommandSet holding the completer function, then search
        # all installed CommandSet instances for one that is an exact type match, none are found
        # search for sub-class matches, also none are found.
    
        command_sets_manual.register_command_set(user_unrelated)
    
        text = ''
        line = 'user_unrelated {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
        out, err = capsys.readouterr()
    
        assert first_match is None
        assert command_sets_manual.completion_matches == []
        assert "Could not find CommandSet instance" in out
    
        command_sets_manual.unregister_command_set(user_unrelated)
    
        ####################################################################################################################
        # Error Case 2
        # If the CommandSet holding a command is unrelated to the CommandSet holding the completer function, then search
        # all installed CommandSet instances for one that is an exact type match, none are found
        # search for sub-class matches, more than 1 is found.
    
        command_sets_manual.register_command_set(user_sub1)
        command_sets_manual.register_command_set(user_sub2)
        command_sets_manual.register_command_set(user_unrelated)
    
        text = ''
        line = 'user_unrelated {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
        out, err = capsys.readouterr()
    
        assert first_match is None
        assert command_sets_manual.completion_matches == []
        assert "Could not find CommandSet instance" in out
    
        command_sets_manual.unregister_command_set(user_unrelated)
        command_sets_manual.unregister_command_set(user_sub2)
        command_sets_manual.unregister_command_set(user_sub1)
    
    
    class CommandSetWithPathComplete(cmd2.CommandSet):
        def __init__(self, dummy):
            """dummy variable prevents this from being autoloaded in other tests"""
            super(CommandSetWithPathComplete, self).__init__()
    
        parser = cmd2.Cmd2ArgumentParser()
        parser.add_argument('path', nargs='+', help='paths', completer=cmd2.Cmd.path_complete)
    
        @cmd2.with_argparser(parser)
        def do_path(self, app: cmd2.Cmd, args):
            app.poutput(args.path)
    
    
    def test_path_complete(command_sets_manual):
        test_set = CommandSetWithPathComplete(1)
    
        command_sets_manual.register_command_set(test_set)
    
        text = ''
        line = 'path {}'.format(text)
        endidx = len(line)
        begidx = endidx
        first_match = complete_tester(text, line, begidx, endidx, command_sets_manual)
    
        assert first_match is not None
    
    
    def test_bad_subcommand():
        class BadSubcommandApp(cmd2.Cmd):
            """Class for testing usage of `as_subcommand_to` decorator directly in a Cmd2 subclass."""
    
            def __init__(self, *args, **kwargs):
                super(BadSubcommandApp, self).__init__(*args, **kwargs)
    
            cut_parser = cmd2.Cmd2ArgumentParser()
            cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut')
    
            @cmd2.with_argparser(cut_parser)
            def do_cut(self, ns: argparse.Namespace):
                """Cut something"""
                pass
    
            banana_parser = cmd2.Cmd2ArgumentParser()
            banana_parser.add_argument('direction', choices=['discs', 'lengthwise'])
    
            @cmd2.as_subcommand_to('cut', 'bad name', banana_parser, help='This should fail')
            def cut_banana(self, ns: argparse.Namespace):
                """Cut banana"""
                self.poutput('cutting banana: ' + ns.direction)
    
        with pytest.raises(CommandSetRegistrationError):
            app = BadSubcommandApp()
    
    
    def test_commandset_settables():
        # Define an arbitrary class with some attribute
        class Arbitrary:
            def __init__(self):
                self.some_value = 5
    
        # Declare a CommandSet with a settable of some arbitrary property
        class WithSettablesA(CommandSetBase):
            def __init__(self):
                super(WithSettablesA, self).__init__()
    
                self._arbitrary = Arbitrary()
                self._settable_prefix = 'addon'
                self.my_int = 11
    
                self.add_settable(
                    Settable(
                        'arbitrary_value',
                        int,
                        'Some settable value',
                        settable_object=self._arbitrary,
                        settable_attrib_name='some_value',
                    )
                )
    
        # Declare a CommandSet with an empty settable prefix
        class WithSettablesNoPrefix(CommandSetBase):
            def __init__(self):
                super(WithSettablesNoPrefix, self).__init__()
    
                self._arbitrary = Arbitrary()
                self._settable_prefix = ''
                self.my_int = 11
    
                self.add_settable(
                    Settable(
                        'another_value',
                        float,
                        'Some settable value',
                        settable_object=self._arbitrary,
                        settable_attrib_name='some_value',
                    )
                )
    
        # Declare a commandset with duplicate settable name
        class WithSettablesB(CommandSetBase):
            def __init__(self):
                super(WithSettablesB, self).__init__()
    
                self._arbitrary = Arbitrary()
                self._settable_prefix = 'some'
                self.my_int = 11
    
                self.add_settable(
                    Settable(
                        'arbitrary_value',
                        int,
                        'Some settable value',
                        settable_object=self._arbitrary,
                        settable_attrib_name='some_value',
                    )
                )
    
        # create the command set and cmd2
        cmdset = WithSettablesA()
        arbitrary2 = Arbitrary()
        app = cmd2.Cmd(command_sets=[cmdset], auto_load_commands=False)
        setattr(app, 'str_value', '')
        app.add_settable(Settable('always_prefix_settables', bool, 'Prefix settables', app))
        app._settables['str_value'] = Settable('str_value', str, 'String value', app)
    
        assert 'arbitrary_value' in app.settables.keys()
        assert 'always_prefix_settables' in app.settables.keys()
        assert 'str_value' in app.settables.keys()
    
        # verify the settable shows up
        out, err = run_cmd(app, 'set')
        any(['arbitrary_value' in line and '5' in line for line in out])
    
        out, err = run_cmd(app, 'set arbitrary_value')
        any(['arbitrary_value' in line and '5' in line for line in out])
    
        # change the value and verify the value changed
        out, err = run_cmd(app, 'set arbitrary_value 10')
        expected = """
    arbitrary_value - was: 5
    now: 10
    """
        assert out == normalize(expected)
        out, err = run_cmd(app, 'set arbitrary_value')
        any(['arbitrary_value' in line and '10' in line for line in out])
    
        # can't add to cmd2 now because commandset already has this settable
        with pytest.raises(KeyError):
            app.add_settable(Settable('arbitrary_value', int, 'This should fail', app))
    
        cmdset.add_settable(
            Settable('arbitrary_value', int, 'Replaced settable', settable_object=arbitrary2, settable_attrib_name='some_value')
        )
    
        # Can't add a settable to the commandset that already exists in cmd2
        with pytest.raises(KeyError):
            cmdset.add_settable(Settable('always_prefix_settables', int, 'This should also fail', cmdset))
    
        # Can't remove a settable from the CommandSet if it is elsewhere and not in the CommandSet
        with pytest.raises(KeyError):
            cmdset.remove_settable('always_prefix_settables')
    
        # verify registering a commandset with duplicate settable names fails
        cmdset_dupname = WithSettablesB()
        with pytest.raises(CommandSetRegistrationError):
            app.register_command_set(cmdset_dupname)
    
        # unregister the CommandSet and verify the settable is now gone
        app.unregister_command_set(cmdset)
        out, err = run_cmd(app, 'set')
        assert 'arbitrary_value' not in out
        out, err = run_cmd(app, 'set arbitrary_value')
        expected = """
    Parameter 'arbitrary_value' not supported (type 'set' for list of parameters).
    """
        assert err == normalize(expected)
    
        # Add a commandset with no prefix
        cmdset_nopfx = WithSettablesNoPrefix()
        app.register_command_set(cmdset_nopfx)
    
        with pytest.raises(ValueError):
            app.always_prefix_settables = True
    
        app.unregister_command_set(cmdset_nopfx)
    
        # turn on prefixes and add the commandset back
        app.always_prefix_settables = True
    
        with pytest.raises(CommandSetRegistrationError):
            app.register_command_set(cmdset_nopfx)
    
        app.register_command_set(cmdset)
    
        # Verify the settable is back with the defined prefix.
        assert 'addon.arbitrary_value' in app.settables.keys()
    
        # rename the prefix and verify that the prefix changes everywhere
        cmdset._settable_prefix = 'some'
        assert 'addon.arbitrary_value' not in app.settables.keys()
        assert 'some.arbitrary_value' in app.settables.keys()
    
        out, err = run_cmd(app, 'set')
        any(['some.arbitrary_value' in line and '5' in line for line in out])
    
        out, err = run_cmd(app, 'set some.arbitrary_value')
        any(['some.arbitrary_value' in line and '5' in line for line in out])
    
        # verify registering a commandset with duplicate prefix and settable names fails
        with pytest.raises(CommandSetRegistrationError):
            app.register_command_set(cmdset_dupname)
    
        cmdset_dupname.remove_settable('arbitrary_value')
    
        app.register_command_set(cmdset_dupname)
    
        with pytest.raises(KeyError):
            cmdset_dupname.add_settable(
                Settable(
                    'arbitrary_value',
                    int,
                    'Some settable value',
                    settable_object=cmdset_dupname._arbitrary,
                    settable_attrib_name='some_value',
                )
            )
    
    
    class NsProviderSet(cmd2.CommandSet):
        # CommandSet which implements a namespace provider
        def __init__(self, dummy):
            # Use dummy argument so this won't be autoloaded by other tests
            super(NsProviderSet, self).__init__()
    
        def ns_provider(self) -> argparse.Namespace:
            ns = argparse.Namespace()
            # Save what was passed as self from with_argparser().
            ns.self = self
            return ns
    
    
    class NsProviderApp(cmd2.Cmd):
        # Used to test namespace providers in CommandSets
        def __init__(self, *args, **kwargs) -> None:
            super().__init__(*args, **kwargs)
            super(NsProviderApp, self).__init__(*args, **kwargs)
    
        @cmd2.with_argparser(cmd2.Cmd2ArgumentParser(), ns_provider=NsProviderSet.ns_provider)
        def do_test_ns(self, args: argparse.Namespace) -> None:
            # Save args.self so the unit tests can read it.
            self.last_result = args.self
    
    
    def test_ns_provider():
        """This exercises code in with_argparser() decorator that calls namespace providers"""
        ns_provider_set = NsProviderSet(1)
        app = NsProviderApp(auto_load_commands=False)
    
        # First test the case in which a namespace provider function resides in a CommandSet class which is registered.
        # with_argparser() will pass the CommandSet instance to the ns_provider() function.
        app.register_command_set(ns_provider_set)
        run_cmd(app, "test_ns")
        assert app.last_result == ns_provider_set
    
        # Now test the case in which a namespace provider function resides in a CommandSet class which is not registered.
        # with_argparser() will receive None from cmd2.Cmd._resolve_func_self() and therefore pass app as self to ns_provider().
        app.unregister_command_set(ns_provider_set)
        run_cmd(app, "test_ns")
        assert app.last_result == app