From 15aba14a9a1691e24583898d4dcdb04915b02d94 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 3 Feb 2026 13:00:23 -0500 Subject: [PATCH 1/2] Fixed issue where CommandSet registration did not respect disabled categories --- CHANGELOG.md | 5 +++++ cmd2/cmd2.py | 31 ++++++++++++++++++++++++++----- tests/test_cmd2.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 172967281..30d79fd0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.1.3 (TBD) + +- Bug Fixes + - Fixed issue where `CommandSet` registration did not respect disabled categories + ## 3.1.2 (January 26, 2026) - Bug Fixes diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 9000d7411..bf38b0551 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) @@ -5805,7 +5814,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 @@ -5837,11 +5846,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. @@ -5852,7 +5867,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 @@ -5891,6 +5906,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: @@ -5898,6 +5917,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 a46018904..ed0cfbd40 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() @@ -3251,6 +3262,25 @@ 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 + + @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__) From 818293dad480cbe17065130fd23a7887e62fb471 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 3 Feb 2026 13:53:40 -0500 Subject: [PATCH 2/2] Added unit tests --- tests/test_cmd2.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index ed0cfbd40..62c1569b1 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -3220,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) @@ -3234,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 @@ -3281,6 +3281,31 @@ def test_register_command_in_disabled_category(disable_commands_app) -> None: 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__)