Argparser subcommands function as a feature, not a workaround

I’ve been developing some CLI tools with argparser and stumbled in a piece of code that I’ve found a little ugly:

# Setting up argparse instance
# ...
# Then:
parser_foo.set_defaults(func=foo)
parser_bar.set_defaults(func=bar)

# parse the args and call whatever function was selected
args = parser.parse_args('foo 1 -x 2'.split())
args.func(args)   # Ugly
>>> 2.0
# parse the args and call whatever function was selected
args = parser.parse_args('bar XYZYX'.split())
args.func(args)  # Eeew
>>> ((XYZYX))

This code is supposed to call a function for the correct subparser and it’s in the argparse documentation just before this section.

In this example, its shown that one can add a function as a property to Namespace and then call this function according to the correct subparer.
Quoting the documentation:

One particularly effective way of handling sub-commands is to combine the use of the add_subparsers() method with calls to set_defaults() so that each subparser knows which Python function it should execute. For example:

I feel that calling func seems a little like a gambiarra.

args.func(args)

I’d like to give a couple of options that might be better than the current suggestion:

  1. Bind the function as a Namespace method
def foo(args: Namespace):
    print("Called foo!")

subparsers.add_parser('foo')
parser_foo.set_defaults(func=foo)  # Some new magic needed here. A little weird still
args = parser.parse_args()
args.func()
  1. Add an optional argument to add_subparsers and a run method to ArgumentParser:
def foo(args: Namespace):
    print("Called foo!")

subparsers.add_parser('foo', run=foo)
parser.run() # Runs the correct function

Does this make sense? Any other suggestions?

My understanding is that argparse is feature frozen at this point. It’s maintained to fix serious bugs, but new features are not being added. There are a myriad of other CLI frameworks that have various design goals and features that you should look to instead if argparse is not sufficient.

5 Likes

That’s basically true. It would be better to contribute to or use any of the various 3rd-party argument parsing libraries for advanced uses.

2 Likes

Makes a lot of sense! I would recommend looking at clize which works broadly in this way, but goes further: the function’s parameters come from the subcommand’s arguments and are used to define them. I’ve used it in several projects and it’s quite convenient.

1 Like

You can easily make your own module which extends argparse with this functionality:

import argparse

class Namespace(argparse.Namespace):

    def run(self):
        run = self._run
        del self._run
        return run(self)

class ArgumentParser(argparse.ArgumentParser):

    def parse_known_args(self, args=None, namespace=None):
        if namespace is None:
            namespace = Namespace()
        return super().parse_known_args(args, namespace)

    def add_subparsers(self, **kwargs):
        class RunAwareSubParsers:
            def add_parser(self, name, run=None, **kwargs):
                parser = subparsers.add_parser(name, **kwargs)
                parser.set_defaults(_run=run)
                return parser
        subparsers = super().add_subparsers(**kwargs)
        return RunAwareSubParsers()

    def run(self, args=None):
        return self.parse_args(args).run()

Import ArgumentParser from this module, and it will work as desired …

def foo(args):
    print("Foo args:", args)

parser = ArgumentParser()
subparsers = parser.add_subparsers()

foo_parser = subparsers.add_parser('foo', run=foo)
foo_parser.add_argument('x', type=int)

args = parser.parse_args('foo 10'.split())
args.run()

# or, if sys.argv[] is properly set
parser.run()
1 Like