Argparse decorator: arguments via inspection

Motivation

Please see the below argparse.ArgumentParse snippet:

import argparse

parser = argparse.ArgumentParser(prog="demo", description="Run something")
parser.add_argument("--arg", type=int, help="some docs", default=1)


def main(arg: int = 1) -> None:
    """
    Run something.

    Args:
        arg: Some docs
    """


if __name__ == "__main__":
    args: argparse.Namespace = parser.parse_args()
    main(**vars(args))

We see there is lots of duplication between the main signature/docstring and the parser. We have to re-type the type, default, help, double dash arg name, and description.

Idea

It would be cool if we could use argparse.ArgumentParser like a decorator on a function, to automatically infer all of this stuff. It would be a big improvement for DRY code, and quick minimum-viable-product scripts using argparse as an entrypoint.

That’s, you propose to make a decorator that, based on the function signature, will create an argument parser and pass them to the function. The idea is good, if I understand correctly, it will work?

# python main.py --arg1 foo --arg2 89

@ArgumentParser
def main(arg1: str, arg2: int):
    ...

if __name__ == '__main__':
    main()

ideally for this to work too:

main("foo", 89)

But these are the details of the work, and so, I’m for this opportunity.

1 Like

I thought I had seen something like this before. It is not exactly click, fire, or typer, but maybe a mix of those, although typer might be the closest.

It’s fairly close to clize if you want another one to throw into the mix of options.

1 Like

If I’ve understood correctly, I’ve made something similar myself: epmanager. The decorator also adds a callable .invoke attribute to the function which wraps up the actual argparse logic. You can still use the function normally, but if you call the func.invoke and pass a list of strings, it will be equivalent to using .parse_args on that list, dispatching the resulting args to the appropriate places, and calling the function.

Finally, there’s a command-line tool that scans for uses of the decorator and updates pyproject.toml so that those .invoke callables become entry points to the program.

The short version is, you can write (assuming I still remember how it works! It’s been almost 3 years…):

from epmanager import entrypoint

@entrypoint(name="demo", description="Run something", arg="some docs")
def main(arg: int = 1) -> None:
    pass

and you don’t even need a if __name__ == '__main__': block, because the packaging system will create a wrapper script to call main.invoke for you.

Drat… that looks very much like what I wanted when I started writing epmanager, and has been around for much longer. I just… had no idea what to search for x.x

1 Like

Heh. I felt the same way. I built and published a thing called docstringargs and then found out about clize, so I stopped adding features to my one.

There’s also appeal: “ A powerful & Pythonic command-line parsing library. Give your program Appeal!”

2 Likes

@nikdissv-Forever yes, you understood what I was getting at! Thanks for posting the possible implementation.

Per the posed third party libs, I didn’t realize how many already thought about this. Time for me to start using them…

I wonder how many times this sort of thing has been reimplemented?

My personal effort is scription. It’s claim to fame is not using inline-annotations (I find them hard to read); instead it’s something like:

@Command(
    modules=('module', MULTI),
    output=('where graph image will be saved', OPTION),
    complete=('include uninstalled ancestors', FLAG),
    prog=('program to create graph', OPTION),
    )
def ancestors(modules, output, complete, prog='dot'):
    "show modules that depend on MODULE"
    G = nx.DiGraph()
    for m in modules:
        G.add_node('m')
    get_ancestors(G, complete, *modules)
    draw_module_graph(G, output, prog=prog, modules=modules)

Oh, it also doesn’t rely on argparse.

I wonder if “this thing you were asking for is already implemented 100 times” is an argument for or agains including it in the standard library.

In and of itself, neither. There are a few other considerations that can affect such a decision though:

  1. Is it very easy to implement just-plain-wrong, and hard to get right? (Particularly with edge cases.) Increased likelihood.
  2. Are all the variations slightly different in what they can do, such that having all the functionality in one library would make a monster? Decreased likelihood.
  3. Is it something that would be of value to the stdlib itself? For example, the current regular expression module is used by quite a few other stdlib modules; those modules can’t depend on a third-party module, so if they want an uprated regex module, it’d need to be added to the stdlib itself.
  4. Is it a trivially simple part of a larger library? Decreased likelihood - not every two-liner needs to be in the stdlib.

And a few others that I haven’t thought of, too.

2 Likes

As the maintainer of Click, I would strongly recommend not trying to implement a CLI framework in the standard library. optparse is a good base, argparse already adds complexity/ambiguity. I don’t think either are accepting new features, and adding a third one wouldn’t help the situation.

2 Likes

I completely agree.