How to create conditonal arguments based on arguments using python's argparse? - python

I'm trying to create small command line tool. My approach was to start off with a list of commands that I would like to run and then create a parser accommodate those commands. Rather than set up the parser and then have the dictate what the input should be.
I'm struggling to figure out how to set up arguments based on previous inputs. Below are a few examples of the commands I am aiming for.
cgen create test-runner
cgen create config --branch branch_name
cgen create guide --branch branch_name
I currently have it set up so that create has set choices as an argument. Then, depending on the inputted argument, I would like to have branch be a required argument if config or guide is run, but not if test-runner is inputted.
I keep tearing things down and trying different approaches so what I have is really basic at the moment but look something like this...
def main():
def run_create(args):
print('run_create')
if args.create_type == 'test-runner':
create_test_runner(args)
if args.create_type == 'config':
print(f'Creating config')
if args.create_type == 'guide':
print(f'Creating guide')
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# create the parser for the "create" command
parser_create = subparsers.add_parser('create')
parser_create.add_argument(choices=['test-runner', 'config', 'guide'], dest='create_type')
parser_create.set_defaults(func=run_create)
args = parser.parse_args()
args.func(args)

When you add arguments you can specify if you want them to be required or not link.
So you can test on the fly and make an argument obligatory.
def arguments_is_given(*args: str) -> bool:
return len(set(args) & set(sys.argv)) > 0
def main():
def run_create(args):
print('run_create')
if args.create_type == 'test-runner':
create_test_runner(args)
if args.create_type == 'config':
print(f'Creating config')
if args.create_type == 'guide':
print(f'Creating guide')
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# create the parser for the "create" command
parser_create = subparsers.add_parser('create')
parser_create.add_argument("--branch",required=not arguments_is_given("config", "guide"))
parser_create.add_argument(choices=['test-runner', 'config', 'guide'], dest='create_type')
parser_create.set_defaults(func=run_create)
args = parser.parse_args()
args.func(args)

Related

How to add arguments to Python script entrypoint based on conditions

I have an entry point like so:
ENTRYPOINT [ "python" ,"/usr/local/bin/script.py" ]
I want to be able to add multiple arguments to script.py if a particular container has "MY_ENV_VAR" set to TRUE.
So for instance if:
MY_ENV_VAR = true
MY_ENV_VAR2 = true
MY_ENV_VAR3 = false (or anything other than true)
I want to run
/usr/local/bin/script.py --my-env-var --my-env-var2
I can't find a good working example of how to accomplish this
You will not be able to conditionally do this within the Dockerfile, but you can achieve the desired behavior by configuring the Python script script.py as follows:
import argparse
import os
def main():
parser = argparse.ArgumentParser("Arguments")
if os.environ.get("MY_ENV_VAR") is not None:
parser.add_argument("--my-env-var", required=True, action="store_true")
if os.environ.get("MY_ENV_VAR2") is not None:
parser.add_argument("--my-env-var2", required=True, action="store_true")
parser.parse_args()
if __name__ == "__main__":
main()
The logic is that if MY_ENV_VAR or MY_ENV_VAR2, then a required argparse argument is set.

How to build a 'cascading' CLI tool using system arguments?

Here is my sample code:
def function1():
parser = argparse.ArgumentParser(description='Test Cascading Utility')
parser.add_argument('--number', type=str, help='Enter number')
args = parser.parse_args()
x = str(args.number)
squares = float(x)**2
def function2():
parser = argparse.ArgumentParser(description='Test Cascading Utility')
parser.add_argument('--number1', type=str, help='Enter number')
parser.add_argument('--number2', type=str, help='Enter number')
args = parser.parse_args()
x = str(args.number1)
y = str(args.number2)
div = float(x)/float(y)
def main():
choice = sys.argv[1]
if choice == 'Y':
function1()
elif choice == 'N':
function2()
else:
print("Come on, choose a Y or N option.")
if __name__ == '__main__':
main()
I am trying to create a cascading cli tool where based on one option I enter, it runs a particular method. This method will in turn have its own set of arguments.
This particular code throws an error: error: unrecognized arguments: Y
This leads me to think 'choice' system argument is being overridden by the argument parser, so how can I implement this cascading effect where based on the choice I run the method.
This is my first time delving into argparse and hence please bear with me if the question is silly. But it is something I really would like to implement.
I would recommend you to use click. It makes these things very simple
http://click.pocoo.org/5/
You need to use groups and maybe multicommand chaining
http://click.pocoo.org/5/commands/#group-invocation-without-command
http://click.pocoo.org/5/commands/#multi-command-chaining
You can create groups and subcommands.
Then in each subcommand call the original functions that you are integrating with.

How to make argparse work only with one argument, even if many are passed?

Structure of my parser:
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--search')
parser.add_argument('-t', '--status' action='store_true')
args = parser.parse_args()
if args.search:
func(args.search)
if args.status:
func1()
Right now the parser can accept both options, -s query -t is valid.
I have two questions:
How to take actions on the first argument only if multiple are passed.
-s query -t have to result only
if args.search:
func(args.search)
to be done.
How to throw an error if multiple args are passed?
The appropriate tool here is a subparser; idiomatic usage is different than what your currently proposed command line looks like.
def your_search_function(options):
pass # do a search here
def your_status_function(options):
pass # collect status here
def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparser(dest='action')
search_parser = subparsers.add_parser('search')
search_parser.set_defaults(func=your_search_function)
status_parser = subparsers.add_parser('status')
status_parser.set_defaults(func=your_status_function)
results = parser.parse_args()
results.func(options=results)
if __name__ == '__main__':
main()
Usage then looks like:
./yourcommand <global options> search <search options>
or
./yourcommand <global options> status <status options>
...which, since it takes the subcommand to run as an argument rather than an option, does not allow multiple subcommands to be passed, thus mooting the parts of your question only applicable when usage is ambiguous.
Global options can be added to parser as usual; search-specific options to search_parser, and status-specific options to status_parser.

Call function based on argparse

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.
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()
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)`
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.
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
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/
# 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
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)()

Argparse - How to Specify a Default Subcommand

I am using the argparse package of Python 2.7 to write some option-parsing logic for a command-line tool. The tool should accept one of the following arguments:
"ON": Turn a function on.
"OFF": Turn a function off.
[No arguments provided]: Echo the current state of the function.
Looking at the argparse documentation led me to believe that I wanted two--possibly three--subcommands to be defined, since these three states are mutually exclusive and represent different conceptual activities. This is my current attempt at the code:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser.set_defaults(func=print_state) # I think this line is wrong.
parser_on = subparsers.add_parser('ON')
parser_on.set_defaults(func=set_state, newstate='ON')
parser_off = subparsers.add_parser('OFF')
parser_off.set_defaults(func=set_state, newstate='OFF')
args = parser.parse_args()
if(args.func == set_state):
set_state(args.newstate)
elif(args.func == print_state):
print_state()
else:
args.func() # Catchall in case I add more functions later
I was under the impression that if I provided 0 arguments, the main parser would set func=print_state, and if I provided 1 argument, the main parser would use the appropriate subcommand's defaults and call func=set_state. Instead, I get the following error with 0 arguments:
usage: cvsSecure.py [-h] {ON,OFF} ...
cvsSecure.py: error: too few arguments
And if I provide "OFF" or "ON", print_state gets called instead of set_state. If I comment out the parser.set_defaults line, set_state is called correctly.
I'm a journeyman-level programmer, but a rank beginner to Python. Any suggestions about how I can get this working?
Edit: Another reason I was looking at subcommands was a potential fourth function that I am considering for the future:
"FORCE txtval": Set the function's state to txtval.
The defaults of the top-level parser override the defaults on the sub-parsers, so setting the default value of func on the sub-parsers is ignored, but the value of newstate from the sub-parser defaults is correct.
I don't think you want to use subcommands. Subcommands are used when the available options and positional arguments change depending on which subcommand is chosen. However, you have no other options or positional arguments.
The following code seems to do what you require:
import argparse
def print_state():
print "Print state"
def set_state(s):
print "Setting state to " + s
parser = argparse.ArgumentParser()
parser.add_argument('state', choices = ['ON', 'OFF'], nargs='?')
args = parser.parse_args()
if args.state is None:
print_state()
elif args.state in ('ON', 'OFF'):
set_state(args.state)
Note the optional parameters to parser.add_argument. The "choices" parameter specifies the allowable options, while setting "nargs" to "?" specifies that 1 argument should be consumed if available, otherwise none should be consumed.
Edit: If you want to add a FORCE command with an argument and have separate help text for the ON and OFF command then you do need to use subcommands. Unfortunately there doesn't seem to be a way of specifying a default subcommand. However, you can work around the problem by checking for an empty argument list and supplying your own. Here's some sample code illustrating what I mean:
import argparse
import sys
def print_state(ignored):
print "Print state"
def set_state(s):
print "Setting state to " + s
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
on = subparsers.add_parser('ON', help = 'On help here.')
on.set_defaults(func = set_state, newstate = 'ON')
off = subparsers.add_parser('OFF', help = 'Off help here.')
off.set_defaults(func = set_state, newstate = 'OFF')
prt = subparsers.add_parser('PRINT')
prt.set_defaults(func = print_state, newstate = 'N/A')
force = subparsers.add_parser('FORCE' , help = 'Force help here.')
force.add_argument('newstate', choices = [ 'ON', 'OFF' ])
force.set_defaults(func = set_state)
if (len(sys.argv) < 2):
args = parser.parse_args(['PRINT'])
else:
args = parser.parse_args(sys.argv[1:])
args.func(args.newstate)
There are two problems with your approach.
First you probably already noticed that newstate is not some sub_value of the sub parser and needs to be addressed at the top level of args as args.newstate. That should explain that assigning a default to newstate twice will result in the first value being overwritten. Whether you call your programm with 'ON' or 'OFF' as a parameter, each time set_state() will be called with OFF. If you just want to be able to do python cvsSecure ON and
python cvsSecure OFF the following would work:
from __future__ import print_function
import sys
import argparse
def set_state(state):
print("set_state", state)
def do_on(args):
set_state('ON')
def do_off(args):
set_state('OFF')
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser_on = subparsers.add_parser('ON')
parser_on.set_defaults(func=do_on)
parser_on.add_argument('--fast', action='store_true')
parser_off = subparsers.add_parser('OFF')
parser_off.set_defaults(func=do_off)
args = parser.parse_args()
args.func(args)
The second problem is that argparse does handle subparsers as single value arguments, so you have to specify one before invoking parser.parse_args(). You can automate insertion of a lacking argument by adding a extra subparser 'PRINT' and automatically inserting
that using set_default_subparser added to argparse.ArgumentParser() (that code is part
of the package ruamel.std.argparse
from __future__ import print_function
import sys
import argparse
def set_default_subparser(self, name, args=None):
"""default subparser selection. Call after setup, just before parse_args()
name: is the name of the subparser to call by default
args: if set is the argument list handed to parse_args()
, tested with 2.7, 3.2, 3.3, 3.4
it works with 2.6 assuming argparse is installed
"""
subparser_found = False
for arg in sys.argv[1:]:
if arg in ['-h', '--help']: # global help if no subparser
break
else:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if not subparser_found:
# insert default in first position, this implies no
# global options without a sub_parsers specified
if args is None:
sys.argv.insert(1, name)
else:
args.insert(0, name)
argparse.ArgumentParser.set_default_subparser = set_default_subparser
def print_state(args):
print("print_state")
def set_state(state):
print("set_state", state)
def do_on(args):
set_state('ON')
def do_off(args):
set_state('OFF')
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser_print = subparsers.add_parser('PRINT', help='default action')
parser_print.set_defaults(func=print_state)
parser_on = subparsers.add_parser('ON')
parser_on.set_defaults(func=do_on)
parser_on.add_argument('--fast', action='store_true')
parser_off = subparsers.add_parser('OFF')
parser_off.set_defaults(func=do_off)
parser.set_default_subparser('PRINT')
args = parser.parse_args()
args.func(args)
You don't need to handle in args to do_on(), etc., but it comes in handy if you start specifying options to the different subparsers.

Categories

Resources