Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
31 changes: 26 additions & 5 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down Expand Up @@ -5905,13 +5920,19 @@ 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:
func = self.cmd_func(cmd_name)
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.

Expand Down
59 changes: 57 additions & 2 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
COMMAND_NAME,
Cmd2Style,
Color,
CommandSet,
RichPrintKwargs,
clipboard,
constants,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)


Expand All @@ -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
Expand Down Expand Up @@ -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__)
Expand Down
Loading