Argparse: print arguments with non-default value - python

In the argparse module, is it possible (easily) to print/log arguments that have been changed from default values when running a script? For example,
my_script.py
argparse.add_argument("--arg1", default="val1")
argparse.add_argument("--arg2", default="val2")
Running:
python my_script.py --arg2 newval2
Print:
Arguments changed:
arg2 : newval2

Once you have parsed your arguments you can, take a look to this script:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-c", dest="c", default=1)
def main():
options = parser.parse_args()
print(options.c)
if __name__ == "__main__":
main()
If you save this code in a script tmp.py and then call:
$>python3 tmp.py
$>1
$>python3 tmp.py -c 12
$>12

Related

Calling main with sys.argv argparse

I have a main function which takes args from sys.argv using argparse:
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-arg1', '--argument1', type=str, required=True)
parser.add_argument('-arg2', '--argument2', type=str, required=True)
args = parser.parse_args()
The main function is called from another module as mod.main().
When i print what is passed to the main function the args are: ['-arg1', 'val1', '-arg2', 'val2'] which is the correct list of arguments.
However, when i run it it seems that the args are not passed correctly with argparse.
['-arg1', 'val1', '-arg2', 'val2']
usage: -arg1 [-h] -arg2 ARG2 -arg1 ARG1
-arg1: error: the following arguments are required: -arg1
The issue is that i am not running the run_module.py which calls the module_to_call.main() from the command line but passing it as args to spark-submit in order to run it on EMR.
"HadoopJarStep": {
"Args": [
"spark-submit",..., "run_module.py", "module_to_call", "-arg1", "val1", "-arg2", "val2",...
Here is a minimal example for you
#!/usr/bin/env python
# argparse_example.py
import sys
import argparse
def main():
print(sys.argv)
parser = argparse.ArgumentParser()
parser.add_argument('-arg1', '--argument1', type=str, required=True)
parser.add_argument('-arg2', '--argument2', type=str, required=True)
args = parser.parse_args()
print(args)
if __name__ == "__main__":
main()
You can run it with
python argparse_example.py -arg1 val1 -arg2 val2
And get
['argparse_example.py', '-arg1', 'val1', '-arg2', 'val2']
Namespace(argument1='val1', argument2='val2')
Or run it without arguments
python argparse_example.py
to get the help message:
['argg.py']
usage: argg.py [-h] -arg1 ARGUMENT1 -arg2 ARGUMENT2
argg.py: error: the following arguments are required: -arg1/--argument1, -arg2/--argument2
UPDATE
From comments by the OP I realize that sys.argv got mangled on the way to function main() by some enveloping code and the first argument got cut off.
The argument list ['-arg1', 'val1', '-arg2', 'val2'] is not the correct list of arguments as it is missing sys.argv[0] — the name of the python script being called from the command line. As argparse processes sys.argv[1:], it is unable to find '-arg1' and therefore fails with an error.
In this case you can add a dummy element to sys.argv to make sure that -arg1 will be passed to argparse when your code is being run as a module.
#!/usr/bin/env python
# argparse_example.py
import sys
import argparse
def main():
print(sys.argv)
if __name__ != "__main__":
# we are running as a module so add a dummy argument
sys.argv = ['argparse_example.py'] + sys.argv
parser = argparse.ArgumentParser()
parser.add_argument('-arg1', '--argument1', type=str, required=True)
parser.add_argument('-arg2', '--argument2', type=str, required=True)
args = parser.parse_args()
print(args)
if __name__ == "__main__":
main()
You can test it with the following enveloping code
#!/usr/bin/env python
# runner.py
import sys
import argparse_example
if __name__ == "__main__":
sys.argv = sys.argv[1:]
argparse_example.main()
And call it from the command line like this:
python runner.py -arg1 val1 -arg2 val2
The result should be the same as when calling argparse_example directly from the command line, even though sys.argv will be missing the first element:
['-arg1', 'val1', '-arg2', 'val2']
Namespace(argument1='val1', argument2='val2')

argparse subparser exit_on_error

I want to disable exit_on_error for a parser and subparser, to prevent error messages:
import argparse
if __name__ == '__main__':
parser = argparse.ArgumentParser(exit_on_error=False)
subparsers = parser.add_subparsers()
subcommand = subparsers.add_parser('subcommand')
subcommand.add_argument('argument')
try:
arguments = parser.parse_args()
except:
parser.print_usage(std.stderr)
With no arguments (ie: no subparser invoked) it works as intended:
$ python3 main.py
usage: main.py {subcommand} ...
But when I use a subcommand, it doesn't:
$ python3 main.py subcommand
usage: main.py subcommand [-h] argument
main.py subcommand: error: the following arguments are required: argument
usage: main.py {subcommand} ...
In this instance, I'd want it to print this instead:
$ python3 main.py subcommand
usage: main.py {subcommand} ...
I checked, and neither add_subparsers nor add_parser take an exit_on_error argument. How can I suppress printing those error messages?

Call a python script within another python script causes problems with argparse

After trying the solution offered by this thread:
What is the best way to call a Python script from another Python script?
I came along with another problem dealing with arguments.
I have:
test1.py
def some_func():
print 'in test 1, unproductive'
if __name__ == '__main__':
# test1.py executed as script
# do something
some_func()
service.py (with dummy arguments)
import argparse
import test1
actions = ['start', 'remove']
parser = argparse.ArgumentParser()
parser.add_argument("action", help="Possible actions are: "
"'{d[0]}|{d[1]}' ".format(d=actions))
parser.add_argument("-d", "--debug", help="Debug mode", action="store_true")
args = parser.parse_args()
def service_func():
print 'service func'
if __name__ == '__main__':
# service.py executed as script
# do something
service_func()
test1.some_func()
This works:
python service.py start
service func
in test 1, unproductive
However, if I want to add arguments in test1.py as well:
test1.py
import argparse
######## new stuff ##########
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", help="Debug mode", action="store_true")
args = parser.parse_args()
##############################
def some_func():
print 'in test 1, unproductive'
if __name__ == '__main__':
# test1.py executed as script
# do something
some_func()
Now I get:
python service.py start
usage: service.py [-h] [-d]
service.py: error: unrecognized arguments: start
Not sure why...
python service.py start - to argparse 'start' looks like a positional argument. It does not have a '--' to mark it as a flag string.
I'm guessing the parser in service.py is handling it ok, though you don't display or otherwise use the resulting args.
The usage in the error message is consistent with the parser in test1.py. It does not define a positional, just the --debug
usage: service.py [-h] [-d]
test1.py is using the same sys.argv list.
There are several solutions:
add a positional argument to test1
use parse_known_args instead of parse_args.
modify sys.argv in service.py (after parsing) to remove this 'start' string. A change in sys.argv in this script will carry over to test1. Print sys.argv in both to be sure.
use REMAINDER as documented in the docs
argparse.REMAINDER. All the remaining command-line arguments are gathered into a list. This is commonly useful for command line utilities that dispatch to other command line utilities:
Call
args, extras = parser.parse_known_args()
This would set
args to Namespace(debug=False) and extras to ['start']. Otherwise you get a tuple of these two values, (Namespace(debug=False), ['start']).
If you haven't modified sys.argv in the first script, the 2nd one will see the same list. Parsing does not modify this list.

Python optparse from method call?

I have two python scripts with the following structure:
# Script1.py
from optparse import OptionParser
def main():
parser = OptionParser()
parser.add_option("-a", "--add-foobar", action="store_true", help="set foobar true",
dest="foobar", default=False)
options, args = parser.parse_args()
print options.foobar
if __name__ == "__main__":
main()
# Script2.py
from Script1 import main as script1Main
def main():
script1Main()
Is there a way to pass command line arguments from script 2 to script 1? Script 1 in this example is immutable, therefore this must be done only thorough optparse.
If you don't pass any arguments to parse_args, it just uses the value of sys.argv[1:], which is going to be whatever arguments were passed when you called Script2.py. The fact that Script2.py calls Script1.main doesn't change that.
Firstly, maybe use argparse instead. You can process all arguments in script 2, then pass the argument handle to script 1.
# Script1.py
def main(args):
print args
# Script2.py
import argparse
from Script1 import main as script1Main
def main():
parser = argparse.ArgumentParser(
parser.add_option("-a", "--add-foobar", action="store_true", help="set foobar true", default=False)
args = parser.parse_args()
script1Main(args)
if __name__ == "__main__":
main()

Python - unittest trying to call imported custom argparser

I have a unittest that wants to call an imported module to do both parse_os based on the unittest's command-line option but it seems unittest does not recognize the option, any ideas:
./python testParser.py --mac
option --mac not recognized
Usage: testParser.py [options] [test] [...]
Options:
-h, --help Show this message
-v, --verbose Verbose output
-q, --quiet Minimal output
-f, --failfast Stop on first failure
-c, --catch Catch control-C and display results
-b, --buffer Buffer stdout and stderr during test runs
Examples:
testParser.py - run default set of tests
testParser.py MyTestSuite - run suite 'MyTestSuite'
testParser.py MyTestCase.testSomething - run MyTestCase.testSomething
testParser.py MyTestCase - run all 'test*' test methods
in MyTestCase
I want to run my unittest program like this: python testParser.py --mac
EDITTED: Works now by changing 'unittest.main()' to:
runner = unittest.TextTestRunner(stream=stderr_file)
itersuite = unittest.TestLoader().loadTestsFromTestCase(TT28046_ForensicSearchSmokeTest)
runner.run(itersuite)
Unittest program:
import logging
import unittest
from myargparse import *
class MyTest(unittest.TestCase):
def test_parse_os(self):
## Parse the args:
self.install = install_sw(parse_os(arg=""))
print 'Which os? %s' % self.install
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
# get the default logger
logger = logging.getLogger()
# add a file handler
logger.addHandler(logging.FileHandler('stdout.txt', mode='w'))
# set up a stream for all stderr output
stderr_file = open('stderr.txt', 'w')
# attach that stream to the testRunner
unittest.main(testRunner=unittest.TextTestRunner(stream=stderr_file))
My imported module:
import argparse
import os
import sys
def parse_os(arg):
my_os = ''
parser = argparse.ArgumentParser()
parser.add_argument("-m", "--mac",
action="store_true")
parser.add_argument("-w", "--win",
action="store_true")
args = parser.parse_args()
if args.mac:
print 'Mac'
my_os = "Mac"
if args.win:
print 'Windows'
my_os = "Windows"
return my_os
def install_sw(my_os):
installed_os = None
if my_os == 'Mac':
print 'Installing Mac...'
installed_os = 'Mac'
if my_os == 'Windows':
print 'Installing Windows...'
installed_os = 'Windows'
return installed_os
The sys.argv variable is a simple list so you can modify/replace it at your wish.
I'd consider using a context manager in this case, on the lines of:
class SysArgv(object):
def __init__(self, argv):
self._old_argv = sys.argv
sys.argv = argv
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
sys.argv = self._old_argv
return False
And used as:
In [4]: with SysArgv(['a', 'b', 'c']):
...: print(sys.argv)
...:
['a', 'b', 'c']
In your case simple wrap the test code like:
with SysArgv(['the_module_name.py', '--mac']):
# test code goes here
and the argparse module will see the arguments you wants.
As for passing the arguments to the unittest module when running the tests, it's possible passing the argv argument to unittest.main. From the documentation:
The `argv` argument can be a list of options passed to the program, with the first element being the program name. If not specified or `None`, the values of `sys.argv` are used.
However in this case you should modify the sys.argv variable before calling unittest.main:
if __name__ == '__main__':
options = ['name_of_module.py'] + sys.argv[-1:] # last argument as option for the test
with SysArgv(sys.argv[:-1]): # or modify how you want
unittest.main(argv=options)
Have you tried using just '-m' instead of '--mac'?
You may also try:
import optparse
parser = optparse.OptionParser()
parser.add_option("-m", "--mac",
dest="mac",
action="store_true",
help="Run as Mac")
parser.add_option("-w", "--win",
dest="win",
action="store_true",
help="Run as Win")
(options, args) = parser.parse_args()
Thank you all for your suggestions but I decided to go with this to limit the changes to my program.
Instead of calling 'unittest.main()', I just changed to call the following:
runner = unittest.TextTestRunner(stream=stderr_file)
itersuite = unittest.TestLoader().loadTestsFromTestCase(MyTest)
runner.run(itersuite)
Based on all the answers here, I originally did this simple hack, and it worked:
# Change sys.argv before unittest tries to parse our args
sys.argv = [sys.argv[0]] # Replace with only the first arg
unittest.main()
The I realized I could still use all the unitest command line args, and in my case I was just passing a bunch of paths, so anything starting with a "-" could just be passed on and there is no need to hack anything since unittest.main() has an argv argument
# Pass on options, and more importantly, don't pass on ALL args
options = [sys.argv[0]] + [a for a in sys.argv if a.startswith("-")]
unittest.main(argv=options)
Just filter out all argparse and not arparse parameters and send then ones not being argparse arguments to sys.argv which is the one unittest uses:
args, notknownargs = parser.parse_known_args()
sys.argv[1:] = notknownargs

Categories