diff --git a/teos/cli/teos_cli.py b/teos/cli/teos_cli.py index 402a5424..98e3b55d 100755 --- a/teos/cli/teos_cli.py +++ b/teos/cli/teos_cli.py @@ -147,35 +147,50 @@ def stop(self): class CliCommand: + """ + Base class of each Cli command. + + All the implementations should have an appropriately formatted docstring. See existing commands for an example. + Any implementation _must_ override the ``name`` attribute, and it might override the ``shortopts`` and ``longopts`` + attributes. + """ + name = None shortopts = "" longopts = [] @classmethod def parse_args(cls, args): + """Parses the ``args`` array using ``getopt``, using ``shortopts`` and ``longopts`` as options.""" + return getopt(args, cls.shortopts, cls.longopts) @classmethod def run(rpc_client, opts_args): + """ + Executes the command. Receives as parameters the rpc_client and the output of ``parse_args`` on the command + arguments. + """ + raise NotImplementedError() class Cli: - COMMANDS = {} + """ + This class contains the logic for running all the commands of the command line interface. All the commands must be + subclasses of :class:`CliCommand` and need to be added to this class using the ``_register_command`` decorator. - @classmethod - def _register_command(cls, command_cls): - if not issubclass(command_cls, CliCommand): - raise TypeError(f"{command_cls.__name__} is not a subclass of CliCommand") + Args: + data_dir (:obj:`str`): the path to the data directory where the configuration file may be found. + command_line_conf (:obj"`dict`): the command line settings, parsed in a dictionary. - try: - if not isinstance(command_cls.name, str): - raise TypeError(f'{command_cls.__name__} has a "name" attribute, but it is not a string.') - except AttributeError: - raise TypeError(f'{command_cls.__name__} does not have a "name" attribute.') + Attributes: + rpc_client (:class:`RpcClient`): the rpc client that is passed to the ``run`` method of the commands. + """ - cls.COMMANDS[command_cls.name] = command_cls - return command_cls + # A dictionary mapping each command's name to the corresponding CliCommand subclass. + # It is populated by the ``_register_command`` decorator. + COMMANDS = {} def __init__(self, data_dir, command_line_conf): # Loads config and sets up the data folder and log file @@ -189,15 +204,44 @@ def __init__(self, data_dir, command_line_conf): self.rpc_client = RPCClient(teos_rpc_host, teos_rpc_port) - def run(self, command, raw_args): - if command not in self.COMMANDS: + @classmethod + def _register_command(cls, command_cls): + """ + Register a new command, which must be a subclass of :class:`CliCommand` and must override the ``name`` field + with an appropriate string. + + Raises: + :obj:`TypeError`: ``command_cls`` is not a subclass of :class:`CliCommand`, or its ``name`` field is not a + string. + """ + + if not issubclass(command_cls, CliCommand): + raise TypeError(f"{command_cls.__name__} is not a subclass of CliCommand") + + if not isinstance(command_cls.name, str): + raise TypeError(f'The "name" attribute of {command_cls.__name__} must be a string.') + + cls.COMMANDS[command_cls.name] = command_cls + return command_cls + + def run(self, command_name, raw_args): + """ + Parses ``raw_args`` using the ``parse_args`` method of the command. + Then, executes the command's ``run`` method, passing the ``rpc_client`` and the output of ``parse_args``. + It any error that might happen, showing an appropriate message to console. + + Returns: + The return value of the ``run`` command, or :obj:`None` if an error was raised. + """ + + if command_name not in self.COMMANDS: sys.exit("Unknown command. Use help to check the list of available commands") - cmd = self.COMMANDS[command] + cmd = self.COMMANDS[command_name] try: args = cmd.parse_args(raw_args) - return self.COMMANDS[command].run(self.rpc_client, args) + return self.COMMANDS[command_name].run(self.rpc_client, args) except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAVAILABLE: sys.exit("It was not possible to reach the Eye of Satoshi. Are you sure the tower is running?")