Updated commands.py
- Added global variable for logging. - Removed `class@RegisterCommand` metaclass. - Replaced with decorator function. - Added `class@CommandHelpStruct`. - Basic struct to define rigid structure of the help message of a command. Uses `__slots__` over instance dictionary. - Changed type annotations for removal of metaclass. - Added logging. - Created decorator function `register_command()`. - Used to register a new command class (replaces metaclass) - Guarantees a name and doc dunder attribute is defined. - Registry now ensures that a command isn't already defined with the same registry name. - Must be invoked with parens
This commit is contained in:
parent
cb7ea6c347
commit
c8abba3165
|
|
@ -1,36 +1,26 @@
|
|||
from collections.abc import Collection
|
||||
import sys
|
||||
import clog
|
||||
import soypak.cli.parser as parser
|
||||
|
||||
|
||||
class RegisterCommand(type):
|
||||
"""Register a new command to soypak and add help information to the parser.
|
||||
# partial load the logger
|
||||
log = lambda: clog.Logger.get("runtime_logger")
|
||||
|
||||
This class is to be used as metaclass for commands.
|
||||
"""
|
||||
def __init__(cls, name, bases, _dict):
|
||||
super().__init__(name, bases, _dict)
|
||||
# get the name of the command
|
||||
name = getattr(cls, "name", None)
|
||||
if name is None:
|
||||
raise Exception("Command has no registered name.")
|
||||
|
||||
# check if the command already exists (means we have duplicate registered name)
|
||||
if name in Command.registered_cmds:
|
||||
raise Exception("Duplicate command, already exists.")
|
||||
|
||||
# add the command to the registered list, with `name` and class object
|
||||
Command.registered_cmds[name] = cls
|
||||
if not cls.__doc__:
|
||||
raise Exception("Command does not provide a __doc__ attribute for usage.")
|
||||
# add the command to the argparse._action_groups list (will appear as a positional in help message)
|
||||
parser.SoypakParser.add_action_command(cmd=name, help=cls.__doc__.split('\n')[0])
|
||||
class CommandHelpStruct:
|
||||
__slots__ = ('synopsis', 'usage', 'summary')
|
||||
def __init__(self, *, synopsis: str, usage: str, summary: Collection = '') -> None:
|
||||
self.synopsis = synopsis
|
||||
self.usage = usage
|
||||
self.summary: tuple = tuple(summary) if summary is tuple else (summary,)
|
||||
|
||||
|
||||
class Command:
|
||||
"""Class for creating a new type of command. Keep record of the registered commands for soypak."""
|
||||
# store all registered commands. Those are commands which soypak implements
|
||||
registered_cmds: dict[str, RegisterCommand] = {}
|
||||
registered_cmds: dict[str, type] = {}
|
||||
registry_names: list[str] = []
|
||||
|
||||
def __init__(self) -> None:
|
||||
# get/create the parser instance
|
||||
|
|
@ -38,14 +28,14 @@ class Command:
|
|||
|
||||
|
||||
@staticmethod
|
||||
def get_command(name, *, error: bool = False, args: list[str] | None = None) -> RegisterCommand | None:
|
||||
def get_command(name, *, error: bool = False, args: list[str] | None = None) -> type | None:
|
||||
"""Fetch and initialise a registered command, or return `None` is command cannot be found."""
|
||||
if name in Command.registered_cmds:
|
||||
return Command.registered_cmds[name](data=parser.SoypakParser().namespace, args=args)
|
||||
|
||||
if error:
|
||||
# Just get the logger for this one statement (:@Note: if more logging occurs, global variable for logger)
|
||||
clog.Logger.get("runtime_logger").error("Unrecognised command: %s" % name).withConsole()
|
||||
log().error("Unrecognised command: %s" % name).withConsole()
|
||||
Command.die()
|
||||
|
||||
return None
|
||||
|
|
@ -60,3 +50,54 @@ class Command:
|
|||
def run(self):
|
||||
"""Virtual method which must be overridden by a new implementation in the child class."""
|
||||
raise RuntimeError("Virtual method must be overridden by child class.")
|
||||
|
||||
|
||||
|
||||
# This template is inspired by how Cpython does `@dataclass`s
|
||||
def register_command(_cls=None, *, name: str, alias=None, help: CommandHelpStruct):
|
||||
""" Register a command to soypak. Used as a decorator `@register_command` on a command class. """
|
||||
def init(cls):
|
||||
# check if the command has already been given a registry name (only one should exist)
|
||||
if hasattr(cls, 'name'):
|
||||
msg = f"Command {cls.__name__!r} has already been registered with the given name: {getattr(cls, 'name')!r}"
|
||||
log().error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
# check if that registry name is already taken
|
||||
if name in Command.registry_names:
|
||||
msg = f"Command {cls.__name__!r} cannot use registry name {name!r} because it is in use."
|
||||
log().error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
# set the name of the command as given
|
||||
setattr(cls, 'name', name)
|
||||
|
||||
# This is deliberate to make aware that `alias` isn't being used so pointless to pass now.
|
||||
if alias is not None:
|
||||
log().warn(f"'alias' argument was passed as something other than None. This is intended for future use.")
|
||||
raise Warning("The 'alias' argument is not intended for use. Please exclude for now.")
|
||||
|
||||
# ensure that the `help` given is of the correct type
|
||||
if not isinstance(help, CommandHelpStruct):
|
||||
msg = f"The argument 'help' must be of type CommandHelpStruct, not {type(help)!r}"
|
||||
log().error(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# construct the help message
|
||||
doc = "{0}\n\nUsage: {1}\n\n{2}".format(help.synopsis, help.usage, "".join(help.summary))
|
||||
setattr(cls, '__doc__', doc)
|
||||
|
||||
# add the command to the registered list, with `name` and class object
|
||||
Command.registered_cmds[name] = cls
|
||||
Command.registry_names.insert(-1, name)
|
||||
# add the command to the argparse._action_groups list (will appear as a positional in help message)
|
||||
parser.SoypakParser.add_action_command(cmd=name, help=help.synopsis)
|
||||
|
||||
return cls
|
||||
|
||||
# if somehow this is ever not None, raise RuntimeError
|
||||
if _cls is not None:
|
||||
raise RuntimeError("Decorator must be called with at least 1 position argument.")
|
||||
|
||||
|
||||
return init
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user