Evidlo
Evidlo

Reputation: 334

Make functions importable when using argparse

I have the following CLI program which adds two numbers:

import argparse

def foo(args):
    print('X + Y:', args.x + args.y)
    return args.x + args.y


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers()

    foo_parser = subparsers.add_parser('foo')
    foo_parser.add_argument('-x', type=int, default=1)
    foo_parser.add_argument('-y', type=int, default=2)
    foo_parser.set_defaults(func=foo)

    parser.add_argument('--debug', action='store_true', default=False)


    args = parser.parse_args()
    args.func(args)

Suppose now I want my users to also be able to import foo and call it directly with arguments x and y. I.e. I want foo to look like this:

def foo(x, y):
    print('X + Y:', x + y)
    return x + y

How can I adapt args.func(args) to handle this new foo?

Upvotes: 0

Views: 98

Answers (2)

Evidlo
Evidlo

Reputation: 334

This is the cleanest way I've found so far:

import argparse

def foo(*, x, y, **kwargs):
    print('X + Y:', x + y)
    return x + y

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers()

    foo_parser = subparsers.add_parser('foo')
    foo_parser.add_argument('-x', metavar='PATH', type=int, default=1)
    foo_parser.add_argument('-y', metavar='PATH', type=int, default=2)
    foo_parser.set_defaults(func=foo)

    parser.add_argument('--debug', action='store_true', default=False)

    args = parser.parse_args()
    args.func(**vars(args))

Maybe someone else can find something better.

Upvotes: 0

pvpks
pvpks

Reputation: 371

The use of args.func(**vars(args)) is not a best fit for use-cases which require import as well as a CLI view

  • When users import a function and call it, they expect a return value for further processing (nothing printed on a console. The caller can decide to print based on the result obtained)
  • When they use a CLI, they expect to see an output printed on the console and a proper exit code (0 or 1 based on the return value)

The ideal way is to separate the parsing/CLI management/sum-function into separate functions and delegate processing once the parsing is complete (below is a sample example)

from __future__ import print_function # for python2
import argparse

def mysum(x, y=5):
    return x+y

def delegator(input_args):
    func_map = {
        "mysum" : {
            "func" : mysum,
            "args": (input_args.get("x"),),
            "kwargs" : {
                "y" : input_args.get("y")
            },
            "result_renderer": print
         }
    }

    func_data = func_map.get(input_args.get("action_to_perform"))

    func = func_data.get("func")
    args = func_data.get("args")
    kwargs = func_data.get("kwargs")
    renderer = func_data.get("result_renderer")

    renderer(func(*args, **kwargs))

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="action_to_perform")

    foo_parser = subparsers.add_parser('mysum')
    foo_parser.add_argument('-x', metavar='PATH', type=int, default=1)
    foo_parser.add_argument('-y', metavar='PATH', type=int, default=3)
    delegator(vars(parser.parse_args()))

Above example would also remove *args, **kwargs from your original function and lets the delegator send only what is needed by the function based on the command

You can extend it to support multiple commands
Hope this helps!

Upvotes: 1

Related Questions