Skip to content

Instantly share code, notes, and snippets.

@mivade

mivade/cli.py

Last active Sep 24, 2020
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

@alephnull alephnull 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

@mivade mivade 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

@evdcush evdcush 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)
@rforman9

This comment has been minimized.

Copy link

@rforman9 rforman9 commented Sep 24, 2020

Thank you for posting your argparse tutorial . It is helping me to write better command line interfaces with argparse.
One thing I haven't figured out yet with your technique is how to add multiple arguments to the sub-commands.

for instance let's say I want to have an add_contacts subcommand and I want to have arguments for name, address and phone. How would that look?

I am able to add a decorator for the name argument, but if try to add multiple decorators I get the error:

Traceback (most recent call last): File "cli.py", line 50, in <module> @subcommand([argument("-p", "--phone", action="store", type=str, help="Contact phone number", required=True)]) File "cli.py", line 30, in decorator parser = parent.add_parser(func.__name__, description=func.__doc__) AttributeError: 'NoneType' object has no attribute '__name__'

any help would be greatly appreciated!
Thanks!

@rforman9

This comment has been minimized.

Copy link

@rforman9 rforman9 commented Sep 24, 2020

Thank you for posting your argparse tutorial . It is helping me to write better command line interfaces with argparse.
One thing I haven't figured out yet with your technique is how to add multiple arguments to the sub-commands.

for instance let's say I want to have an add_contacts subcommand and I want to have arguments for name, address and phone. How would that look?

I am able to add a decorator for the name argument, but if try to add multiple decorators I get the error:

Traceback (most recent call last): File "cli.py", line 50, in <module> @subcommand([argument("-p", "--phone", action="store", type=str, help="Contact phone number", required=True)]) File "cli.py", line 30, in decorator parser = parent.add_parser(func.__name__, description=func.__doc__) AttributeError: 'NoneType' object has no attribute '__name__'

any help would be greatly appreciated!
Thanks!

nvm, I figured it out, I wasn't paying close enough attention. I wasn't passing in all of the arguments in one list. It's working for me now.
This technique is the pretty awesome!

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.