54 lines
1.6 KiB
Python
54 lines
1.6 KiB
Python
"""Base command dataclass for composable CLI trees."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from argparse import ArgumentParser
|
|
from dataclasses import dataclass, fields
|
|
from typing import Any, ClassVar, Dict, List, Optional, Type
|
|
|
|
|
|
@dataclass
|
|
class CLICommand:
|
|
"""Base class for CLI commands.
|
|
|
|
Subclasses define their identity (name, help, description) as
|
|
dataclass fields. These are passed as kwargs to
|
|
``subparsers.add_parser()``.
|
|
|
|
Override ``add_arguments`` to register flags and positionals.
|
|
Override ``execute`` to implement the command's logic.
|
|
|
|
Nest subcommands by setting ``_subcommands`` as a class variable.
|
|
"""
|
|
|
|
name: str = ""
|
|
help: str = ""
|
|
description: str = ""
|
|
|
|
_subcommands: ClassVar[List[Type[Command]]] = []
|
|
|
|
def add_arguments(self, parser: ArgumentParser) -> None:
|
|
"""Add arguments to the parser. Override in subclasses."""
|
|
|
|
def execute(self, args: Any) -> int:
|
|
"""Run the command. Override in subclasses.
|
|
|
|
Returns an exit code (0 = success).
|
|
"""
|
|
return 0
|
|
|
|
def parser_kwargs(self) -> Dict[str, Any]:
|
|
"""Return the dataclass fields as kwargs for add_parser.
|
|
|
|
Excludes ``name`` (used as the positional parser name) and
|
|
any empty-string fields so argparse defaults apply.
|
|
"""
|
|
skip = {"name"}
|
|
kwargs = {}
|
|
for f in fields(self):
|
|
if f.name in skip or f.name.startswith("_"):
|
|
continue
|
|
val = getattr(self, f.name)
|
|
if val != "":
|
|
kwargs[f.name] = val
|
|
return kwargs
|