Skip to content

Instantly share code, notes, and snippets.

@mivade

mivade/cli.py

Last active May 17, 2021
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!

@NSBum

This comment has been minimized.

Copy link

@NSBum NSBum commented Dec 31, 2020

It's a sensible approach.

What's not clear is how to deal with flags that pertain not to a single subcommand but to the entire app. In the example, you've attached the -d debug flag to the subcommand test; but what if one wanted to make the d flag applicable across all subcommands. For example:

cli.add_argument('-d', '--debug', action='store_true', help='Show debug information')

does not work (error: unrecognized arguments: -d)

@mivade

This comment has been minimized.

Copy link
Owner Author

@mivade mivade commented Jan 2, 2021

As written there's currently no way to add an application-wide argument like that, but it shouldn't be too hard to modify things to allow for that. I would think in that case it would be best to define an Application class (or whatever name makes sense) which can be given app-wide options. Then the decorators would be applied with @app.subcommand.

@jamesyip22

This comment has been minimized.

Copy link

@jamesyip22 jamesyip22 commented May 7, 2021

@mivade
could you post the class with the decorators? i'm still learning python and this would be a great example to understand. thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment