39
votes

I'm new to python and currently playing with it. I have a script which does some API Calls to an appliance. I would like to extend the functionality and call different functions based on the arguments given when calling the script.

Currently I have the following:

parser = argparse.ArgumentParser()
parser.add_argument("--showtop20", help="list top 20 by app",
                    action="store_true")
parser.add_argument("--listapps", help="list all available apps",
                    action="store_true")
args = parser.parse_args()

I also have a

def showtop20():
    .....

and

def listapps():
....

How can I call the function (and only this) based on the argument given? I don't want to run

if args.showtop20:
   #code here

if args.listapps:
   #code here

as I want to move the different functions to a module later on keeping the main executable file clean and tidy.

6
You could have a dictionary mapping arguments to functions {'showtop20': showtop20, ...} - you then update the dictionary if the functions are moved/renamed. - jonrsharpe
I would use store_const instead, with a default empty function, and then call all functions in turn - njzk2
docs.python.org/3/library/argparse.html#sub-commands - check the example that uses add_subparsers and set_defaults to link command and function. - hpaulj
Look at adding subparsers. The argparse docs explain in detail under that heading how to do what you want by adding func= to the subparser definition. - BoarGules

6 Answers

37
votes

Since it seems like you want to run one, and only one, function depending on the arguments given, I would suggest you use a mandatory positional argument ./prog command, instead of optional arguments (./prog --command1 or ./prog --command2).

so, something like this should do it:

FUNCTION_MAP = {'top20' : my_top20_func,
                'listapps' : my_listapps_func }

parser.add_argument('command', choices=FUNCTION_MAP.keys())

args = parser.parse_args()

func = FUNCTION_MAP[args.command]
func()
17
votes

At least from what you have described, --showtop20 and --listapps sound more like sub-commands than options. Assuming this is the case, we can use subparsers to achieve your desired result. Here is a proof of concept:

import argparse
import sys

def showtop20():
    print('running showtop20')

def listapps():
    print('running listapps')

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

# Create a showtop20 subcommand    
parser_showtop20 = subparsers.add_parser('showtop20', help='list top 20 by app')
parser_showtop20.set_defaults(func=showtop20)

# Create a listapps subcommand       
parser_listapps = subparsers.add_parser('listapps', help='list all available apps')
parser_listapps.set_defaults(func=listapps)

# Print usage message if no args are supplied.

# NOTE: Python 2 will error 'too few arguments' if no subcommand is supplied.
#       No such error occurs in Python 3, which makes it feasible to check
#       whether a subcommand was provided (displaying a help message if not).
#       argparse internals vary significantly over the major versions, so it's
#       much easier to just override the args passed to it.

if len(sys.argv) <= 1:
    sys.argv.append('--help')

options = parser.parse_args()

# Run the appropriate function (in this case showtop20 or listapps)
options.func()

# If you add command-line options, consider passing them to the function,
# e.g. `options.func(options)`
14
votes

There are lots of ways of skinning this cat. Here's one using action='store_const' (inspired by the documented subparser example):

p=argparse.ArgumentParser()
p.add_argument('--cmd1', action='store_const', const=lambda:'cmd1', dest='cmd')
p.add_argument('--cmd2', action='store_const', const=lambda:'cmd2', dest='cmd')

args = p.parse_args(['--cmd1'])
# Out[21]: Namespace(cmd=<function <lambda> at 0x9abf994>)

p.parse_args(['--cmd2']).cmd()
# Out[19]: 'cmd2'
p.parse_args(['--cmd1']).cmd()
# Out[20]: 'cmd1'

With a shared dest, each action puts its function (const) in the same Namespace attribute. The function is invoked by args.cmd().

And as in the documented subparsers example, those functions could be written so as to use other values from Namespace.

args = parse_args()
args.cmd(args)

For sake of comparison, here's the equivalent subparsers case:

p = argparse.ArgumentParser()
sp = p.add_subparsers(dest='cmdstr')
sp1 = sp.add_parser('cmd1')
sp1.set_defaults(cmd=lambda:'cmd1')
sp2 = sp.add_parser('cmd2')
sp2.set_defaults(cmd=lambda:'cmd2')

p.parse_args(['cmd1']).cmd()
# Out[25]: 'cmd1'

As illustrated in the documentation, subparsers lets you define different parameter arguments for each of the commands.

And of course all of these add argument or parser statements could be created in a loop over some list or dictionary that pairs a key with a function.

Another important consideration - what kind of usage and help do you want? The different approaches generate very different help messages.

8
votes

If your functions are "simple enough" take adventage of type parameter https://docs.python.org/2.7/library/argparse.html#type

type= can take any callable that takes a single string argument and returns the converted value:

In your example (even if you don't need a converted value):

parser.add_argument("--listapps", help="list all available apps",
                    type=showtop20,
                    action="store")

This simple script:

import argparse

def showtop20(dummy):
    print "{0}\n".format(dummy) * 5

parser = argparse.ArgumentParser()
parser.add_argument("--listapps", help="list all available apps",
                    type=showtop20,
                    action="store")
args = parser.parse_args()

Will give:

# ./test.py --listapps test
test
test
test
test
test
test
0
votes

Instead of using your code as your_script --showtop20, make it into a sub-command your_script showtop20 and use the click library instead of argparse. You define functions that are the name of your subcommand and use decorators to specify the arguments:

import click

@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
    print(f'Debug mode is {"on" if debug else "off"}')

@cli.command()  # @cli, not @click!
def showtop20():
    # ...

@cli.command()
def listapps():
    # ...

See https://click.palletsprojects.com/en/master/commands/

-1
votes
# based on parser input to invoke either regression/classification plus other params

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--path", type=str)
parser.add_argument("--target", type=str)
parser.add_argument("--type", type=str)
parser.add_argument("--deviceType", type=str)    

args = parser.parse_args()
df = pd.read_csv(args.path)
df = df.loc[:, ~df.columns.str.contains('^Unnamed')]
if args.type == "classification":
    classify = AutoML(df, args.target, args.type, args.deviceType)
    classify.class_dist()
    classify.classification()

elif args.type == "regression":
    reg = AutoML(df, args.target, args.type, args.deviceType)
    reg.regression()

else:
    ValueError("Invalid argument passed")


# Values passed as : python app.py --path C:\Users\Abhishek\Downloads\adult.csv --target income --type classification --deviceType GPU