Skip to content

Instantly share code, notes, and snippets.

@charlesreid1
Created September 10, 2022 18:10
Show Gist options
  • Save charlesreid1/f47b6c247c829864d3ed88835891ee8f to your computer and use it in GitHub Desktop.
Save charlesreid1/f47b6c247c829864d3ed88835891ee8f to your computer and use it in GitHub Desktop.
Dynamically turn all class methods into CLI commands

src_cli.py goes in src/cli.py

then, the states_cli module is going to be at src/states/cli.py

it is going to define a client class, and a bunch of methods that each do something,

and the docstring will provide descriptions of each parameter

that will be automatically parsed by above

"""Console script"""
import sys
import os
import json
import argparse
import logging
import platform
import traceback
import argcomplete
from .states import cli as states_cli
from . import __version__
class SupercommandArgumentParser(argparse.ArgumentParser):
"""
Super command argument parser.
A super command is an argparse command with sub-commands
that can be attached to it.
This is implemented as an extension of the argparse.ArgParser
class, but it also stores a list of subparsers in a
private attribute self._subparsers.
argument parsers can be registered as subparsers of this
parent argument parser by registering with the add_parser_func()
method.
"""
def __init__(self, *args, **kwargs):
super(*args, **kwargs)
self._subparsers = None
def add_parser_func(self, func, **kwargs):
if self._subparsers is None:
self._subparsers = self.add_subparsers()
# we were passed a function, add a subparser action corresponding to it.
# auto-generate a safe name for this parser based on the function name.
# func.__name__ is the magic here.
subparser = self._subparsers.add_parser(func.__name__.replace("_", "-"), **kwargs)
# set the entry point for this command line option to execute the function
subparser.set_defaults(entry_point=func)
# trust fall
command = subparser.prog[len(self.prog) + 1 :].replace("-", "_").replace(" ", "_")
subparser.set_defaults({})
# If the subparser has a description, use that.
# Otherwise, if the "help" keyword arg was provided, use that.
# Otherwise, use the docstring.
if subparser.description is None:
subparser.description = kwargs.get("help", func.__doc__)
# Not sure why this is necessary...
self._defaults.pop("entry_point", None)
return subparser
def print_help(self, file=None):
formatted_help = self.format_help()
formatted_help = formatted_help.replace("positional arguments:", "Positional Arguments:")
formatted_help = formatted_help.replace("optional arguments:", "Optional Arguments:")
print(formatted_help)
self.exit()
def get_parser(help_menu=False):
parser = SupercommandArgumentParser(
description=__doc__, formatter_class=argparse.RawTextHelpFormatter
)
version_string = "%(prog)s {version} ({python_impl} {python_version} {platform})".format(
version=__version__,
python_impl=platform.python_implementation(),
python_version=platform.python_version(),
platform=platform.platform(),
)
parser.add_argument("--version", action="version", version=version_string)
# Add help options for this parser
def halp(args):
"""Print help message"""
parser.print_help()
parser.add_parser_func(halp)
# Add subparser
states_cli.add_commands(parser._subparsers, help_menu=help_menu)
argcomplete.autocomplete(parser)
return parser
def main(args=None):
if not args:
args = sys.argv[1:]
if "--help" in args or "-h" in args:
parser = get_parser(help_menu=True)
else:
parser = get_parser()
if len(args) < 1:
parser.print_help()
parser.exit(1)
# Parse the arguments provided to main() function
parsed_args = parser.parse_args(args=args)
# Set logging levels for external libraries
logging.basicConfig(level=logging.ERROR)
try:
# Call the entry point function, passing the parsed arguments
result = parsed_args.entry_point(parsed_args)
except Exception as e:
# O noes!!1
err_msg = traceback.format_exc()
print(err_msg, file=sys.stderr)
exit(os.EX_SOFTWARE)
else:
if isinstance(result, SystemExit):
raise result
elif result is not None:
if isinstance(result, bytes):
sys.stdout.buffer.write(result)
else:
# The default arg here ensures smooth handling of datetime
print(json.dumps(result, indent=4, default=lambda x: str(x)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment