From 5f5d1d19dbd3266d255a0eed38a35797009b204a Mon Sep 17 00:00:00 2001 From: Gopar Date: Tue, 6 Jan 2026 12:06:16 -0800 Subject: [PATCH 1/2] Add metaclass to enforce custom command rules --- cecli/commands/utils/base_command.py | 34 ++++++++++++++- tests/basic/test_custom_commands.py | 64 ++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/basic/test_custom_commands.py diff --git a/cecli/commands/utils/base_command.py b/cecli/commands/utils/base_command.py index c94206ee45c..760e373328b 100644 --- a/cecli/commands/utils/base_command.py +++ b/cecli/commands/utils/base_command.py @@ -1,8 +1,38 @@ -from abc import ABC, abstractmethod +from abc import ABC, ABCMeta, abstractmethod from typing import List -class BaseCommand(ABC): +class CommandMeta(ABCMeta): + """Metaclass for validating command classes at definition time.""" + + def __new__(mcs, name, bases, namespace): + # Create the class first + cls = super().__new__(mcs, name, bases, namespace) + + # Skip validation for BaseCommand itself + if name == "BaseCommand": + return cls + + # Validate class name + if not name.endswith("Command"): + raise TypeError(f"Command class must end with 'Command', got '{name}'") + + # Validate NORM_NAME + if getattr(cls, "NORM_NAME", None) is None: + raise TypeError("Command class must define NORM_NAME") + + # Validate DESCRIPTION + if getattr(cls, "DESCRIPTION", None) is None: + raise TypeError("Command class must define DESCRIPTION") + + # Validate execute method is implemented + if "execute" not in namespace: + raise TypeError("Command class must implement execute method") + + return cls + + +class BaseCommand(ABC, metaclass=CommandMeta): """Abstract base class for all commands.""" # Class properties (similar to BaseTool) diff --git a/tests/basic/test_custom_commands.py b/tests/basic/test_custom_commands.py new file mode 100644 index 00000000000..88c0b537400 --- /dev/null +++ b/tests/basic/test_custom_commands.py @@ -0,0 +1,64 @@ +import pytest + +from cecli.commands.utils.base_command import BaseCommand + + +class TestCommandMeta: + """Tests for the CommandMeta metaclass validation.""" + + def test_valid_custom_command_is_accepted(self): + """Test that a valid custom command class is accepted.""" + + class CustomCommand(BaseCommand): + NORM_NAME = "custom" + DESCRIPTION = "A valid custom command" + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + pass + + # If we get here without exception, the test passes + assert CustomCommand.NORM_NAME == "custom" + assert CustomCommand.DESCRIPTION == "A valid custom command" + + def test_class_name_must_end_with_command(self): + """Test that class name must end with 'Command'.""" + with pytest.raises(TypeError, match="Command class must end with 'Command'"): + + class Custom(BaseCommand): + NORM_NAME = "custom" + DESCRIPTION = "An invalid custom command" + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + pass + + def test_must_define_norm_name(self): + """Test that NORM_NAME must be defined.""" + with pytest.raises(TypeError, match="Command class must define NORM_NAME"): + + class CustomCommand(BaseCommand): + DESCRIPTION = "Missing NORM_NAME" + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + pass + + def test_must_define_description(self): + """Test that DESCRIPTION must be defined.""" + with pytest.raises(TypeError, match="Command class must define DESCRIPTION"): + + class CustomCommand(BaseCommand): + NORM_NAME = "custom" + + @classmethod + async def execute(cls, io, coder, args, **kwargs): + pass + + def test_must_implement_execute_method(self): + """Test that execute method must be implemented.""" + with pytest.raises(TypeError, match="Command class must implement execute method"): + + class CustomCommand(BaseCommand): + NORM_NAME = "custom" + DESCRIPTION = "Missing execute method" From e25b6ac88efc1ca92999cac0beaa11fca785313d Mon Sep 17 00:00:00 2001 From: Gopar Date: Tue, 6 Jan 2026 12:07:53 -0800 Subject: [PATCH 2/2] Remove comments --- cecli/commands/utils/base_command.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cecli/commands/utils/base_command.py b/cecli/commands/utils/base_command.py index 760e373328b..68eba79780f 100644 --- a/cecli/commands/utils/base_command.py +++ b/cecli/commands/utils/base_command.py @@ -13,19 +13,15 @@ def __new__(mcs, name, bases, namespace): if name == "BaseCommand": return cls - # Validate class name if not name.endswith("Command"): raise TypeError(f"Command class must end with 'Command', got '{name}'") - # Validate NORM_NAME if getattr(cls, "NORM_NAME", None) is None: raise TypeError("Command class must define NORM_NAME") - # Validate DESCRIPTION if getattr(cls, "DESCRIPTION", None) is None: raise TypeError("Command class must define DESCRIPTION") - # Validate execute method is implemented if "execute" not in namespace: raise TypeError("Command class must implement execute method")