"""Categorization classes for CLI commands."""
import abc
from typing import Tuple, Set, List, Mapping, Optional, Iterable, Union
from repobee_plug._containers import ImmutableMixin
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)
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))
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}
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)
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)
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,
)