Skip to content

Instantly share code, notes, and snippets.

@mivade mivade/cli.py
Last active Mar 15, 2019

Embed
What would you like to do?
Using a decorator to simplify subcommand creation with argparse
from argparse import ArgumentParser
cli = ArgumentParser()
subparsers = cli.add_subparsers(dest="subcommand")
def argument(*name_or_flags, **kwargs):
"""Convenience function to properly format arguments to pass to the
subcommand decorator.
"""
return (list(name_or_flags), kwargs)
def subcommand(args=[], parent=subparsers):
"""Decorator to define a new subcommand in a sanity-preserving way.
The function will be stored in the ``func`` variable when the parser
parses arguments so that it can be called directly like so::
args = cli.parse_args()
args.func(args)
Usage example::
@subcommand([argument("-d", help="Enable debug mode", action="store_true")])
def subcommand(args):
print(args)
Then on the command line::
$ python cli.py subcommand -d
"""
def decorator(func):
parser = parent.add_parser(func.__name__, description=func.__doc__)
for arg in args:
parser.add_argument(*arg[0], **arg[1])
parser.set_defaults(func=func)
return decorator
@subcommand()
def nothing(args):
print("Nothing special!")
@subcommand([argument("-d", help="Debug mode", action="store_true")])
def test(args):
print(args)
@subcommand([argument("-f", "--filename", help="A thing with a filename")])
def filename(args):
print(args.filename)
@subcommand([argument("name", help="Name")])
def name(args):
print(args.name)
if __name__ == "__main__":
args = cli.parse_args()
if args.subcommand is None:
cli.print_help()
else:
args.func(args)
@alephnull

This comment has been minimized.

Copy link

commented Jun 15, 2018

[ *name_or_flags ] seems to be invalid syntax now. I replaced it with list(name_or_flags).

This is a nice way of dealing with argparse. Makes it more like click. I spent half a day futzing with docopt for a subcommand driven CLI before implementing it your way.

@mivade

This comment has been minimized.

Copy link
Owner Author

commented Aug 14, 2018

@alephnull: thanks! I've updated this so that it works with modern Python versions.

@evdcush

This comment has been minimized.

Copy link

commented Mar 15, 2019

This is excellent! Thank you for your post on subparsers; keep finding there is so much more functionality in argparse than what can be gleaned from the docs. It'd be great to see this recipe like this in the docs.

Minor edit from my own usage: subcommand receives argument as-is, and a little simpler unpacking.

def argument(*names_or_flags, **kwargs):
    return names_or_flags, kwargs

def subcommand(*subparser_args, parent=subparsers):
    def decorator(func):
        parser = parent.add_parser(func.__name__, description=func.__doc__)
        for args, kwargs in subparser_args:
            parser.add_argument(*args, **kwargs)
        parser.set_defaults(func=func)
    return decorator

@subcommand(argument('-f', '--filename', help="A thing with a filename"))
def filename(args):
    print(args.filename)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.