Python argparse order for sub-command and defaults values - python

I have been struggling with it for a long time, so I would like to do sub-commands for another command which will be ignore order opposite to subparsers = parser.add_subparsers(), because a subparser can be executed at the end of arguments or if it is alone.
For example test.py is connecting to the device via SSH and execute some command, I can see output in the terminal but like I'd like to save it e.g. as filename_test_1 (default name is command_output) as .txt or .csv (default format is txt).
I wish I could make --save in various possible ways (keeping the -n and -f executed only after --save --> for example error: argument -f/--format: can't be used before --save):
python3 test.py --save
python3 test.py --save -n file_name
python3 test.py --save -n file_name -f csv
python3 test.py --save -n file_name --command "cat /etc/passwd"
python3 test.py --command "cat /etc/passwd" --save -f csv
I am basing on this code, but I can't rewrite it into my own needs. One of the problem is --save is not store_true argument:
#!/usr/bin/python3
import argparse
from collections import OrderedDict
def open_gui():
print("GUI has been opened.")
return 0
def test_information():
print("Some information.")
return 0
class ParentAction(argparse.Action):
def __init__(self, *args, **kwargs):
super().__init__(*args, default=OrderedDict(), **kwargs)
self.children = []
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest)
nspace = type(namespace)()
for child in self.children:
if child.default is not None:
setattr(nspace, child.name, child.default)
items[values] = nspace
class ChildAction(argparse.Action):
def __init__(self, *args, parent, sub_action='store', **kwargs):
super().__init__(*args, **kwargs)
self.dest, self.name = parent.dest, self.dest
self.action = sub_action
self._action = None
self.parent = parent
parent.children.append(self)
def get_action(self, parser):
if self._action is None:
action_cls = parser._registry_get('action', self.action, self.action)
self._action = action_cls(self.option_strings, self.name)
return self._action
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest)
try:
last_item = next(reversed(items.values()))
except StopIteration:
raise argparse.ArgumentError(self, "can't be used before {}".format(self.parent.option_strings[0]))
action = self.get_action(parser)
action(parser, last_item, values, option_string)
def main(command_line=None):
print('')
parser = argparse.ArgumentParser(prog='SSHtest',
description='It is example description',
add_help=False)
single_group = parser.add_argument_group('single use arguments')
single_group_exception = parser.add_argument_group('exception for single use arguments')
multi_group = parser.add_argument_group('multiple use arguments')
single_group.add_argument('--help', '-h', action='help', help='show this help message and exit')
single_group.add_argument('--version', '-v', action='version', version='%(prog)s 1.0.1a201009')
single_group.add_argument('--information', '-i', action='store_true', help='show alias information and exit')
single_group.add_argument('--gui', action='store_true', help='open a GUI application from default web browser and exit')
multi_group.add_argument('--debug', action='store_true', help='print debug info')
multi_group.add_argument('--command', type=str, help='execute custom command and exit')
parent = parser.add_argument('-s', '--save', action=ParentAction)
parser.add_argument('-n', '--name', action=ChildAction, parent=parent, default='command_output', metavar='NAME', help='set file name (default: %(default)s)')
parser.add_argument('-f', '--format', choices=['txt', 'csv', 'xlsx'], default='txt', action=ChildAction, parent=parent, metavar='FORMAT', help='set format of file, default is %(default)s (%(choices)s)')
args = parser.parse_args(command_line)
if args.information:
test_information()
parser.exit()
if args.debug:
print("debug: " + str(args))
if args.gui:
open_gui()
if __name__ == '__main__':
main()
maybe there is a simple way? I would also like to keep "good" formatting of --help
Note: I would like to use basic libs so I'll stay with argparse :v

Related

How to pass arguments to Unit test

I am trying to write an unit test for a function that deals with the argparsers from the users.
My function:
def __init_parser() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description='Parsing arguments...',
)
parser.add_argument('--task_id', '-t', dest='task_id', action="store", type=str, required=True)
return parser.parse_args()
Test
#pytest.mark.parametrize('task_id', ('--task_id', '-t'))
def test__init_parser_with_job_id(capsys, task_id):
args = __init_parser()
The error _jb_pytest_runner.py: error: the following arguments are required: --task_id/-t
How can I achieve this passing the number of task_id in the body of the test function?
As the comment of #hpaulj, the function looks for the argv, so in this case it is necessary to mock it and then proceed with the asserts.
def test__init_parser(monkeypatch):
task_id = '258'
with mock.patch.object(sys, 'argv', ['startup.py', '--task_id', '258'):
args = __init_parser()
job_id = getattr(args, 'task_id')
assert task_id == '258'

Use argparse to send selective arguments to another python script

How do I use argparse to send selective arguments to other scripts. The scripts invoked are imported as modules and the folder structure is as below:
Directory Structure - hello.py
- cloud_module
- script1
- script2
In hello.py script, I am trying to invoke scripts based on argument conditions and pass selective remainder arguments -
hello.py
from cloud_module import script1,script2
import argparse
def parse_arguments(parser):
parser.add_argument('--name', type=str, required=True)
parser.add_argument('--cloud', type=str, required=True)
parser.add_argument('--service', type=str, required=True)
parser.add_argument('--zone', type=str, required=True)
parser.add_argument('--billing', type=str, required=True)
def parse_command_line_arguments():
parser = argparse.ArgumentParser()
parse_arguments(parser)
args = parser.parse_args()
arguments = args.__dict__
return args
def output(args):
if args.name == 'script1':
**// Pass values to script1.py: cloud & service**
elif args.name == 'script2':
**// Pass values to script2.py: zone & billing**
if __name__ == "__main__":
arguments = parse_command_line_arguments()
output(arguments)
script1.py
import argparse
def parse_arguments(parser):
parser.add_argument('--cloud', type=str, required=True)
parser.add_argument('--service', type=str, required=True)
def parse_command_line_arguments():
parser = argparse.ArgumentParser()
parse_arguments(parser)
args = parser.parse_args()
arguments = args.__dict__
return args
def func1(arguments):
print('this is script1')
if __name__ == "__main__":
arguments = parse_command_line_arguments()
func1(arguments)
You can use subparsers for parsing different scripts with different arguments. Here is a simple example for the same
subparser_example.py
import argparse
def script_1_run(a, b):
print( f'Running script 1 with {a}, {b}')
def script_2_run(x, y):
print( f'Running script 2 with {x}, {y}')
def add_parser_for_script1( parser: argparse.ArgumentParser ):
parser.add_argument('-a', '--argument_a', type=str, dest='a')
parser.add_argument('-b', '--argument_b', type=str, dest='b')
def add_parser_for_script2( parser: argparse.ArgumentParser ):
parser.add_argument('-x', '--argument_y', type=str, dest='x')
parser.add_argument('-y', '--argument_x', type=str, dest='y')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
subparser = parser.add_subparsers(title = 'Script select', dest='script_type')
subparser.required = True
add_parser_for_script1(subparser.add_parser('script1'))
add_parser_for_script2(subparser.add_parser('script2'))
args = parser.parse_args()
if args.script_type == 'script1':
script_1_run(args.a, args.b)
elif args.script_type == 'script2':
script_2_run(args.x, args.y)
Usage:
for script 1
py subparser_example.py script1 -a argument_a -b argument_b
for script 2
py subparser_example.py script2 -x argument_x -y argument_y

How can I remove CLI arguments using argparse so unittest will accept arg list

I'd like to pass my own arguments into files that are setup for unittest. So calling it from the command line like this should work:
python Test.py --c keith.ini SomeTests.test_one
Currently I'm running into two issues.
1) Arg parse doesn't allow unknown arguments
usage: Test.py [-h] [--c CONFILE]
Test.py: error: unrecognized arguments: SomeTests.test_one
2) Unit test doesn't allow unknown arguments. So --c fileName is not accepted by unittest and returns:
AttributeError: 'module' object has no attribute 'keith'
So the idea is to collect my arguments and remove them before calling unittest runner.
import unittest
import argparse
myArgs = None
def getArgs( allArgs ):
parser = argparse.ArgumentParser( )
parser.add_argument('--c', dest='conFile', type=str, default=None, help='Config file')
args = parser.parse_args()
if ( args.conFile == None ):
parser.print_help()
return args
class SomeTests(unittest.TestCase):
def test_one(self):
theTest( 'keith' )
def test_two(self):
otherTest( 'keith' )
if __name__ == '__main__':
myArgs = getArgs( sys.argv )
print 'Config File: ' + myArgs.conFile
unittest.main( argv=sys.argv, testRunner = unittest.TextTestRunner(verbosity=2))
Interesting I just found parse_known_args() so I changed the parse line to:
args = parser.parse_known_args(['--c']).
I thought this would solve my issue and give me something to pass to unittest. Unfortunately I get:
Test.py: error: argument --c: expected one argument.
Shouldn't this work?
OK took a bit of effort but figured it out. This is totally possible. The documentation for argparse is not correct. The function parse_known_args() should not include a list of known arguments. Also argparse removes arg[0] which is important to return so other commands see a valid argument list. I'd consider this removal a bug. I have included the final example code.
import unittest
import argparse
import sys
myArgs = None
def getArgs( allArgs ):
parser = argparse.ArgumentParser( )
parser.add_argument('--c', dest='conFile', type=str, default=None, help='Configuration file. (Required)')
args, addArgs = parser.parse_known_args( )
if ( args.conFile == None ):
parser.print_help()
sys.exit(2)
# argparse strips argv[0] so prepend it
return args, [ sys.argv[0]] + addArgs
def verify( expected, actual ):
assert expected == actual, 'Test Failed: '
# Reusable Test
def theTest( exp ):
print 'myargs: ' + str( myArgs )
verify( exp, 'keith' )
def otherTest( exp ):
theTest( exp )
class SomeTests(unittest.TestCase):
def test_one(self):
theTest( 'keith' )
def test_two(self):
otherTest( 'keith2' )
if __name__ == '__main__':
myArgs, addArgs = getArgs( sys.argv )
unittest.main( argv=addArgs, testRunner = unittest.TextTestRunner(verbosity=2))
Once you save this to a file you can call it like the examples below and it will all work.
python Test.py # Requires config file
python Test.py --c keith.ini # Runs all tests
python Test.py --c keith.ini SomeTests # Runs Class
python Test.py --c keith.ini SomeTests.test_one # Runs test
HTH, Enjoy

Command line interface with multiple commands using click: add unspecified options for command as dictionary

I have a command line interface build with click which implements multiple commands.
Now I want to pass unspecified named options into one command which is here named command1 e.g. the number of options and their names should be able to vary flexibly.
import click
#click.group(chain=True)
#click.pass_context
def cli(ctx, **kwargs):
return True
#cli.command()
#click.option('--command1-option1', type=str)
#click.option('--command1-option2', type=str)
#click.pass_context
def command1(ctx, **kwargs):
"""Add command1."""
ctx.obj['command1_args'] = {}
for k, v in kwargs.items():
ctx.obj['command1_args'][k] = v
return True
#cli.command()
#click.argument('command2-argument1', type=str)
#click.pass_context
def command2(ctx, **kwargs):
"""Add command2."""
print(ctx.obj)
print(kwargs)
return True
if __name__ == '__main__':
cli(obj={})
I have already looked into forwarding unknown options like in this question but the problem is that I have to call (chanin) other commands after the first one which have to be asserted e.g. this call has to work but with arbitrary options for command1:
$python cli.py command1 --command1-option1 foo --command1-option2 bar command2 'hello'
So how can I add unspecified named options to a single command and call (chain) another one at the same time (after it)?
The custom class found here, can be adapted to your case.
Using the Custom Class:
To use the custom class, just use the cls parameter to the click.command() decorator like:
#cli.command(cls=AcceptAllCommand)
#click.pass_context
def command1(ctx, **kwargs):
"""Add command1."""
...
Test Code:
import click
class AcceptAllCommand(click.Command):
def make_parser(self, ctx):
"""Hook 'make_parser' and allow the opt dict to find any option"""
parser = super(AcceptAllCommand, self).make_parser(ctx)
command = self
class AcceptAllDict(dict):
def __contains__(self, item):
"""If the parser does no know this option, add it"""
if not super(AcceptAllDict, self).__contains__(item):
# create an option name
name = item.lstrip('-')
# add the option to our command
click.option(item)(command)
# get the option instance from the command
option = command.params[-1]
# add the option instance to the parser
parser.add_option(
[item], name.replace('-', '_'), obj=option)
return True
# set the parser options to our dict
parser._short_opt = AcceptAllDict(parser._short_opt)
parser._long_opt = AcceptAllDict(parser._long_opt)
return parser
#click.group(chain=True)
#click.pass_context
def cli(ctx, **kwargs):
""""""
#cli.command(cls=AcceptAllCommand)
#click.pass_context
def command1(ctx, **kwargs):
"""Add command1."""
ctx.obj['command1_args'] = {}
for k, v in kwargs.items():
ctx.obj['command1_args'][k] = v
#cli.command()
#click.argument('command2-argument1', type=str)
#click.pass_context
def command2(ctx, **kwargs):
"""Add command2."""
print(ctx.obj)
print(kwargs)
if __name__ == "__main__":
commands = (
"command1 --cmd1-opt1 foo --cmd1-opt2 bar command2 hello",
'--help',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + cmd)
time.sleep(0.1)
cli(cmd.split(), obj={})
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Results:
Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> command1 --cmd1-opt1 foo --cmd1-opt2 bar command2 hello
{'command1_args': {'cmd1_opt1': 'foo', 'cmd1_opt2': 'bar'}}
{'command2_argument1': 'hello'}
-----------
> --help
Usage: test.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--help Show this message and exit.
Commands:
command1 Add command1.
command2 Add command2.

Passing down extra arguments via ArgumentParser

So I have a script in which my argument parsing functions are separated for a cleaner design. Ultimately, I wish to run a single command and have all the arguments parsed by those 3 functions. The command would look like:
python3 rhize_refactored.py -l <str>, -sa, [-cr], -si <int>, -i <input_path>, -o <output_path>, [-r], [-c]
In order for all arguments to be recognized, I've set up the script so that any extra arguments ignored by the first argument parsing function get passed on to the second argument parsing function, and again with the third argument parsing function. That part looks like this:
#Argument parsing functions#
def parse_args_language():
parser=ArgumentParser(prog= 'rhize.py')
parser.add_argument('-l', dest='language', choices= ['bash', 'python'], type=str, default='bash') #required
args, extras1= parser.parse_known_args() #pass extras down to parse_args_bash()
return args
return extras1
def parse_args_bash(extras1):
parser=ArgumentParser()
parser=parser.add_argument('-sa', action='store_true') #required
parser=parser.add_argument('-cr', action='store_true') #optional
parser=parser.add_argument('-si', type=int) #required
parser=parser.add_argument('-i') #required
parser=parser.add_argument('-o') #required
args=parser.parse_args(argv =extras1)
extras2= parser.parse_known_args() #pass extras down to parse_args_repo
return args
return extras2
def parse_args_repo(extras2):
parser= ArgumentParser()
parser.add_argument('-r', action= 'store_true') #optional
parser.add_argument('-c', action= 'store_true') #optional
args=parser.parser_args(argv=extras2)
return args
##############################################################
def rhize_bash():
args, extras1= parse_args_language()
parse_args_bash(extras1)
make_templates()
....
def make_templates():
args, extras2= parse_args_bash()
parse_args_repo(extras2)
...
def main():
language= parse_args_language()
if language == "bash":
rhize_bash()
if language == "python":
rhize_python() #omitted from this post
print("Completed the run.")
main()
Have I set this up the right way? Because when I try running the full script, it appears to run through it fully, even though I know it shouldn't.
Here's an attempt to make the code flow correctly. I haven't tested it.
#Argument parsing functions#
def parse_args_language():
parser=ArgumentParser(prog= 'rhize.py')
parser.add_argument('-l', dest='language', choices= ['bash', 'python'], default='bash')
args, extras1 = parser.parse_known_args() #pass extras down to parse_args_bash()
return args, extras1 # return a tuple of items
def parse_args_bash(extras1):
parser=ArgumentParser()
parser=parser.add_argument('--sa', action='store_true')
# store_true actions are always optional
parser=parser.add_argument('--cr', action='store_true')
parser=parser.add_argument('--si', type=int)
parser=parser.add_argument('-i')
parser=parser.add_argument('-o')
args, extras2= parser.parse_known_args() #pass extras down to parse_args_repo
return args, extras2
def parse_args_repo(extras2):
parser= ArgumentParser()
parser.add_argument('-r', action= 'store_true')
parser.add_argument('-c', action= 'store_true')
args=parser.parser_args(argv=extras2)
return args
##############################################################
def rhize_bash(extras1):
args1, extras2 = parse_args_bash(extras1)
make_templates(extras2)
....
def make_templates(extras2):
args2 = parse_args_repo(extras2)
...
def main():
args, extras1 = parse_args_language()
if args.language == "bash":
rhize_bash(extras1)
elif args.language == "python":
rhize_python(extras1) #omitted from this post
print("Completed the run.")
if __name__ == "__main__":
main()
The parse_args_bash and parse_args_repo return separate args namespace objects. We could pass the args to parse_args_repo, and have it add its values to that. But I'll skip that step for now.
Your code called parse_args_language a couple of times, once to get the args.language value, and once to get extras1 to pass to on. No harm in doing that, but rewrote it so it is called just once.
Since it is using parse_known_args, parse_args_bash should work using the default sys.argv, since it would just ignore the -l argument. But it's also ok to work with extras which strips that out.

Categories