45

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.

YakovL
  • 7,557
  • 12
  • 62
  • 102
f0rd42
  • 1,429
  • 4
  • 19
  • 30
  • You could have a dictionary mapping arguments to functions `{'showtop20': showtop20, ...}` - you then update the dictionary if the functions are moved/renamed. – jonrsharpe Dec 17 '14 at 16:00
  • 1
    I would use `store_const` instead, with a default empty function, and then call all functions in turn – njzk2 Dec 17 '14 at 16:09
  • 1
    https://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 Dec 17 '14 at 17:53
  • 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 Dec 27 '20 at 08:33

7 Answers7

54

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()
Hannes Ovrén
  • 21,229
  • 9
  • 65
  • 75
  • 8
    `choices` works fine with a dictionary. It automatically uses its keys. – hpaulj Dec 17 '14 at 18:55
  • 1
    Good to know. I still find it better to be explicit, but it's not a big issue :) – Hannes Ovrén Dec 18 '14 at 08:59
  • @HannesOvrén sorry, haven't had the time to look into this, but now I have. I do understand the first lines of your suggestion, but not the last two. you set "func" to be the "commend" and then "func()"? Where do my "def showtop20", etc come into the game? – f0rd42 Mar 03 '17 at 17:06
  • function maps will make it very hard to debug. I would recommend against that. Better use docopt! – Nickpick Jun 02 '17 at 12:36
  • 1
    @nitpick: Why? If an exception is raised in any of the functions its name (and offending linenumber) is clearly stated, e.g.: `File "argtest.py", line 4, in my_top20_func`. So I really don't see in what way debugging would be difficult, or how `docopt` would be better in that regard. – Hannes Ovrén Jun 02 '17 at 13:48
  • @HannesOvrén What if i have to pass json argument along with command and use it in function? How can I achieve that ? – udgeet patel Sep 17 '18 at 08:43
22

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)`
Six
  • 5,122
  • 3
  • 29
  • 38
  • The docs for `argparse` give this example using `set_defaults(func=whatever)` as well https://docs.python.org/library/argparse.html#argparse.ArgumentParser.add_subparsers – Boris Verkhovskiy Sep 18 '19 at 14:25
  • I ended up using the [`click`](https://click.palletsprojects.com/) library, where you define functions that act as subcommands and use decorators to specify the arguments. https://click.palletsprojects.com/en/7.x/commands/ – Boris Verkhovskiy Aug 30 '20 at 00:52
14

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.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • @hpaulk, I know this has been a while and it won't specifically answer the original question, but I was looking for a way to pass multiple flags where each flag calls a specific function (rather than just a single function by overriding "dest" variables). I found the `action='append_const'` action which does just that. Would you mind adding that to your answer? – OozeMeister Sep 11 '17 at 18:50
  • Example gist here: https://gist.github.com/frenchtoast747/cdbdba055b649f44d0f86bc88d29b6b8 – OozeMeister Sep 11 '17 at 18:51
11

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
Juan Diego Godoy Robles
  • 14,447
  • 2
  • 38
  • 52
  • 2
    Have you run this code? `store_true` doesn't take any arguments, so `showtop20` shouldn't get executed. It would run with the default 'store' action, but I don't think intertwining parsing and execution is a good idea. – hpaulj Dec 17 '14 at 17:48
  • 1
    You're rigth 'store' action is needed, this method is a kind of hack but maybe the closest thing to the requirement. – Juan Diego Godoy Robles Dec 17 '14 at 18:12
  • 1
    The important difference in your approach is that the function is executed while the parser is still active. So multiple functions could be invoked sequentially. In some cases that would be desirable, in others it could create debugging headaches. – hpaulj Dec 17 '14 at 19:19
0

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/

Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
0
# 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
0

You can evaluate using evalwhether your argument value is callable:

import argparse

def list_showtop20():
    print("Calling from showtop20")
def list_apps():
    print("Calling from listapps")

my_funcs = [x for x in dir() if x.startswith('list_')]

parser = argparse.ArgumentParser()
parser.add_argument("-f", "--function", required=True,
                        choices=my_funcs,
                        help="function to call", metavar="")

args = parser.parse_args()

eval(args.function)()
Bart
  • 264
  • 1
  • 2
  • 11