Source code for repobee_plug.cli.categorization

"""Categorization classes for CLI commands."""
import abc
from typing import Tuple, Set, List, Mapping, Optional, Iterable, Union

from repobee_plug._immutable import ImmutableMixin


[docs]class Category(ImmutableMixin, abc.ABC): """Class describing a command category for RepoBee's CLI. The purpose of this class is to make it easy to programmatically access the different commands in RepoBee. A full command in RepoBee typically takes the following form: .. code-block:: bash $ repobee <category> <action> [options ...] For example, the command ``repobee issues list`` has category ``issues`` and action ``list``. Actions are unique only within their category. """ help: str = "" description: str = "" name: str actions: Tuple["Action"] action_names: Set[str] _action_table: Mapping[str, "Action"] def __init__( self, name: Optional[str] = None, action_names: Optional[Set[str]] = None, help: Optional[str] = None, description: Optional[str] = None, ): # determine the name of this category based on the runtime type of the # inheriting class name = name or self.__class__.__name__.lower().strip("_") # determine the action names based on type annotations in the # inheriting class action_names = (action_names or set()) | { name for name, tpe in self.__annotations__.items() if isinstance(tpe, type) and issubclass(tpe, Action) } object.__setattr__(self, "help", help or self.help) object.__setattr__( self, "description", description or self.description ) object.__setattr__(self, "name", name) object.__setattr__(self, "action_names", set(action_names)) # This is just to reserve the name 'actions' object.__setattr__(self, "actions", None) for key in self.__dict__.keys(): if key in action_names: raise ValueError(f"Illegal action name: {key}") actions = [] for action_name in action_names: action = Action(action_name.replace("_", "-"), self) object.__setattr__(self, action_name.replace("-", "_"), action) actions.append(action) object.__setattr__(self, "actions", tuple(actions)) object.__setattr__(self, "_action_table", {a.name: a for a in actions}) def get(self, key: str) -> Optional["Action"]: return self._action_table.get(key) def __getitem__(self, key: str) -> "Action": return self._action_table[key] def __iter__(self) -> Iterable["Action"]: return iter(self.actions) def __len__(self): return len(self.actions) def __repr__(self): return f"Category(name={self.name}, actions={self.action_names})" def __str__(self): return self.name def __eq__(self, other): if not isinstance(other, self.__class__): return False return self.name == other.name def __hash__(self): return hash(repr(self)) def __getattr__(self, key): """We implement getattr such that linters won't complain about dynamically added members. """ return object.__getattribute__(self, key)
[docs]class Action(ImmutableMixin): """Class describing a RepoBee CLI action. Attributes: name: Name of this action. category: The category this action belongs to. """ name: str category: Category def __init__(self, name: str, category: Category): object.__setattr__(self, "name", name) object.__setattr__(self, "category", category) def __repr__(self): return f"<Action(name={self.name},category={self.category})>" def __str__(self): return f"{self.category.name} {self.name}" def __eq__(self, other): return ( isinstance(other, self.__class__) and self.name == other.name and self.category == other.category ) def __hash__(self): return hash(str(self))
[docs] def as_name_dict(self) -> Mapping[str, str]: """This is a convenience method for testing that returns a dictionary on the following form: .. code-block:: python {"category": self.category.name "action": self.name} Returns: A dictionary with the name of this action and its category. """ return {"category": self.category.name, "action": self.name}
[docs] def as_name_tuple(self) -> Tuple[str, str]: """This is a convenience method for testing that returns a tuple on the following form: .. code-block:: python (self.category.name, self.name) Returns: A dictionary with the name of this action and its category. """ return (self.category.name, self.name)
[docs] def astuple(self) -> Tuple["Category", "Action"]: """Same as :py:meth:`Action.as_name_tuple`, but with the proper :py:class:`Category` and :py:class:`Action` objects instead of strings. Returns: A tuple with the category and action. """ return (self.category, self)
[docs] def asdict(self) -> Mapping[str, Union["Category", "Action"]]: """Same as :py:meth:`Action.as_name_dict`, but with the proper :py:class:`Category` and :py:class:`Action` objects instead of strings. Returns: A dictionary with the category and action. """ return {"category": self.category, "action": self}
[docs]def category( name: str, action_names: List[str], help: str = "", description: str = "" ) -> "Category": """Create a category for CLI actions. Args: name: Name of the category. action_names: The actions of this category. Returns: A CLI category. """ return Category( name=name, action_names=set(action_names), help=help, description=description, )