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
30 changes: 28 additions & 2 deletions cecli/commands/utils/base_command.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
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

if not name.endswith("Command"):
raise TypeError(f"Command class must end with 'Command', got '{name}'")

if getattr(cls, "NORM_NAME", None) is None:
raise TypeError("Command class must define NORM_NAME")

if getattr(cls, "DESCRIPTION", None) is None:
raise TypeError("Command class must define DESCRIPTION")

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)
Expand Down
64 changes: 64 additions & 0 deletions tests/basic/test_custom_commands.py
Original file line number Diff line number Diff line change
@@ -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"