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