diff --git a/CHANGELOG.md b/CHANGELOG.md index b6cc4080..599d4505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Bug Fixes - Fixed issue where `delimiter_complete()` could cause more matches than display matches + - Fixed issue where `CommandSet` registration did not respect disabled categories ## 3.1.2 (January 26, 2026) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 20c00cc2..c9de5ce4 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -593,11 +593,14 @@ def __init__( # being printed by a command. self.terminal_lock = threading.RLock() - # 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. + # Commands disabled during specific application states + # Key: Command name | Value: DisabledCommand object self.disabled_commands: dict[str, DisabledCommand] = {} + # Categories of commands to be disabled + # Key: Category name | Value: Message to display + self.disabled_categories: dict[str, str] = {} + # The default key for sorting string results. Its default value performs a case-insensitive alphabetical sort. # If natural sorting is preferred, then set this to NATURAL_SORT_KEY. # cmd2 uses this key for sorting: @@ -788,6 +791,12 @@ def register_command_set(self, cmdset: CommandSet) -> None: if default_category and not hasattr(command_method, constants.CMD_ATTR_HELP_CATEGORY): utils.categorize(command_method, default_category) + # If this command is in a disabled category, then disable it + command_category = getattr(command_method, constants.CMD_ATTR_HELP_CATEGORY, None) + if command_category in self.disabled_categories: + message_to_print = self.disabled_categories[command_category] + self.disable_command(command, message_to_print) + self._installed_command_sets.add(cmdset) self._register_subcommands(cmdset) @@ -5819,7 +5828,7 @@ def enable_command(self, command: str) -> None: :param command: the command being enabled """ - # If the commands is already enabled, then return + # If the command is already enabled, then return if command not in self.disabled_commands: return @@ -5851,11 +5860,17 @@ def enable_category(self, category: str) -> None: :param category: the category to enable """ + # If the category is already enabled, then return + if category not in self.disabled_categories: + return + for cmd_name in list(self.disabled_commands): func = self.disabled_commands[cmd_name].command_function if getattr(func, constants.CMD_ATTR_HELP_CATEGORY, None) == category: self.enable_command(cmd_name) + del self.disabled_categories[category] + def disable_command(self, command: str, message_to_print: str) -> None: """Disable a command and overwrite its functions. @@ -5866,7 +5881,7 @@ def disable_command(self, command: str, message_to_print: str) -> None: command being disabled. ex: message_to_print = f"{cmd2.COMMAND_NAME} is currently disabled" """ - # If the commands is already disabled, then return + # If the command is already disabled, then return if command in self.disabled_commands: return @@ -5905,6 +5920,10 @@ def disable_category(self, category: str, message_to_print: str) -> None: of the command being disabled. ex: message_to_print = f"{cmd2.COMMAND_NAME} is currently disabled" """ + # If the category is already disabled, then return + if category in self.disabled_categories: + return + all_commands = self.get_all_commands() for cmd_name in all_commands: @@ -5912,6 +5931,8 @@ def disable_category(self, category: str, message_to_print: str) -> None: if getattr(func, constants.CMD_ATTR_HELP_CATEGORY, None) == category: self.disable_command(cmd_name, message_to_print) + self.disabled_categories[category] = message_to_print + def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_kwargs: Any) -> None: """Report when a disabled command has been run or had help called on it. diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index a4601890..62c1569b 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -20,6 +20,7 @@ COMMAND_NAME, Cmd2Style, Color, + CommandSet, RichPrintKwargs, clipboard, constants, @@ -3106,6 +3107,16 @@ def do_has_no_helper_funcs(self, arg) -> None: self.poutput("The real has_no_helper_funcs") +class DisableCommandSet(CommandSet): + """Test registering a command which is in a disabled category""" + + category_name = "CommandSet Test Category" + + @cmd2.with_category(category_name) + def do_new_command(self, arg) -> None: + self._cmd.poutput("CommandSet function is enabled") + + @pytest.fixture def disable_commands_app(): return DisableCommandsApp() @@ -3209,7 +3220,7 @@ def test_enable_enabled_command(disable_commands_app) -> None: saved_len = len(disable_commands_app.disabled_commands) disable_commands_app.enable_command('has_helper_funcs') - # The number of disabled_commands should not have changed + # The number of disabled commands should not have changed assert saved_len == len(disable_commands_app.disabled_commands) @@ -3223,7 +3234,7 @@ def test_disable_command_twice(disable_commands_app) -> None: message_to_print = 'These commands are currently disabled' disable_commands_app.disable_command('has_helper_funcs', message_to_print) - # The length of disabled_commands should have increased one + # The number of disabled commands should have increased one new_len = len(disable_commands_app.disabled_commands) assert saved_len == new_len - 1 saved_len = new_len @@ -3251,6 +3262,50 @@ def test_disabled_message_command_name(disable_commands_app) -> None: assert err[0].startswith('has_helper_funcs is currently disabled') +def test_register_command_in_enabled_category(disable_commands_app) -> None: + disable_commands_app.enable_category(DisableCommandSet.category_name) + cs = DisableCommandSet() + disable_commands_app.register_command_set(cs) + + out, _err = run_cmd(disable_commands_app, 'new_command') + assert out[0] == "CommandSet function is enabled" + + +def test_register_command_in_disabled_category(disable_commands_app) -> None: + message_to_print = "CommandSet function is disabled" + disable_commands_app.disable_category(DisableCommandSet.category_name, message_to_print) + cs = DisableCommandSet() + disable_commands_app.register_command_set(cs) + + _out, err = run_cmd(disable_commands_app, 'new_command') + assert err[0] == message_to_print + + +def test_enable_enabled_category(disable_commands_app) -> None: + # Test enabling a category that is not disabled + saved_len = len(disable_commands_app.disabled_categories) + disable_commands_app.enable_category('Test Category') + + # The number of disabled categories should not have changed + assert saved_len == len(disable_commands_app.disabled_categories) + + +def test_disable_category_twice(disable_commands_app) -> None: + saved_len = len(disable_commands_app.disabled_categories) + message_to_print = 'These commands are currently disabled' + disable_commands_app.disable_category('Test Category', message_to_print) + + # The number of disabled categories should have increased one + new_len = len(disable_commands_app.disabled_categories) + assert saved_len == new_len - 1 + saved_len = new_len + + # Disable again and the length should not change + disable_commands_app.disable_category('Test Category', message_to_print) + new_len = len(disable_commands_app.disabled_categories) + assert saved_len == new_len + + @pytest.mark.parametrize('silence_startup_script', [True, False]) def test_startup_script(request, capsys, silence_startup_script) -> None: test_dir = os.path.dirname(request.module.__file__)