Source code for hazwaz.command

import argparse
import logging
import typing

try:
    import coloredlogs  # type: ignore
except ModuleNotFoundError:
    coloredlogs = None


def _get_first_docstring_line(obj):
    try:
        return obj.__doc__.split('\n')[1].strip()
    except (AttributeError, IndexError):
        return None


def _get_remaining_docstring_lines(obj):
    try:
        return "\n".join(obj.__doc__.split('\n')[2:]).strip()
    except (AttributeError, IndexError):
        return None


[docs]class MainCommand: ''' The main class for a command line command. Your script will have to subclass this once, instantiate and run its :py:meth:`run()` e.g. as:: class MyCommand(MainCommand): """ A description that will be used in the help. """ if __name__ == "__main__": MyCommand().run() ''' commands: typing.Iterable["Command"] = () """ The subcommands: a tuple of :py:class:`Command` subclasses. """ logformat: str = "%(levelname)s:%(name)s: %(message)s" """ The format passed to logging.Formatter. """ coloredlogs: bool = True """ Whether coloredlogs is used (if available) """ def __init__(self): desc = _get_first_docstring_line(self) epilog = _get_remaining_docstring_lines(self) self.parser = argparse.ArgumentParser( description=desc, epilog=epilog, ) self.add_arguments(self.parser) self.parser.set_defaults(subcommand=self) self.subparsers = self.parser.add_subparsers() for sub in self.commands: sub_help = _get_first_docstring_line(sub) sub_epilog = _get_remaining_docstring_lines(sub) sub_parser = self.subparsers.add_parser( sub.name, description=sub_help, epilog=sub_epilog, ) sub.add_arguments(sub_parser) sub_parser.set_defaults(subcommand=sub)
[docs] def main(self): """ The main function for a command with no subcommands. This default implementation that simply prints the help is good for most cases when there are subcommands and running the bare command doesn't do anything. """ self.parser.print_help()
[docs] def add_arguments(self, parser: argparse.ArgumentParser): """ Add argparse arguments to an existing parser. If you need to override this method, you probably want to call super().add_arguments(parser) to add the default arguments. """ group = parser.add_mutually_exclusive_group() group.add_argument( '--verbose', '-v', action='store_true', help="Show more details", ) group.add_argument( '--debug', action='store_true', help="Show debug messages", )
[docs] def setup_logging(self): if getattr(self.args, "debug", False): level = logging.DEBUG elif getattr(self.args, "verbose", False): level = logging.INFO else: level = logging.WARNING if self.coloredlogs and coloredlogs: coloredlogs.install(level=level, fmt=self.logformat) else: logger = logging.getLogger() handler = logging.StreamHandler() formatter = logging.Formatter(self.logformat) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(level)
[docs] def run(self): """ Run the command. This is the method called to start running the command. """ self.args = self.parser.parse_args() self.setup_logging() self.args.subcommand.args = self.args self.args.subcommand.main()
[docs]class Command: """ A subcommand to a MainCommand. Every subcommand of your script will be a subclass of this, added to the :py:attr:`MainCommand.subcommands`. """ name: typing.Optional[str] = None """ The name used to call this subcommand from the command line. If this property is none, the default is the name of the class set to lowercase. """ def __init__(self): if self.name is None: self.name = self.__class__.__name__.lower()
[docs] def add_arguments(self, parser: argparse.ArgumentParser): """ Add argparse arguments to an existing parser. Override this method to add arguments to a subcommand. """ pass
[docs] def main(self): """ Main code of this subcommand. Override this method to implement the actual program. """ raise NotImplementedError