ConfigArgParse ignores abbreviated options? - python

my config.ini:
banana=original_banana
if I run with full argument name, I get the expected result:
python test_configargparse.py --banana new_banana
new_banana
if I run with abbreviated argument name (--ban instead of --banana), I get unexpected behaviour:
python test_configargparse.py --ban new_banana
original_banana
code for test_configargparse.py
import os, configargparse as ap
parser = ap.ArgumentParser(default_config_files=["config.ini"])
parser.add_argument('--banana',dest='banana')
options = parser.parse_args()
print(options.banana)
versions = ConfigArgParse==0.13.0, Python 2.7.10
is this a bug or am I missing something obvious?? it's a very basic feature in a very established module...
NOTE: this feature is explicitly documented in https://docs.python.org/3/library/argparse.html
allows long options to be abbreviated to a prefix, if the abbreviation is unambiguous (the prefix matches a unique option)

It looks like a bug in ConfigArgParse. When it loads options from the config file, it discards any option that is already on the command line.
discard_this_key = already_on_command_line(
args, action.option_strings)
The bug is that already_on_command_line() only checks for complete argument names, not prefixes.
def already_on_command_line(existing_args_list, potential_command_line_args):
"""Utility method for checking if any of the potential_command_line_args is
already present in existing_args.
"""
return any(potential_arg in existing_args_list
for potential_arg in potential_command_line_args)
That leaves two copies of the argument in the list, with the config file's value second. ArgumentParser takes the second value.

Related

How do I suppress an argument when nothing is input on command line?

#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s')
parser.add_argument('--choice', '-c', default = argparse.SUPPRESS)
args = parser.parse_args()
def main(selection, choice):
print(selection)
print(choice)
if __name__=='__main__':
main(args.selection, args.choice)
The example provided is just to provide something simple and short that accurately articulates the actual problem I am facing in my project. My goal is to be able to ignore an argument within the code body when it is NOT typed into the terminal. I would like to be able to do this through passing the argument as a parameter for a function. I based my code off of searching 'suppress' in the following link: https://docs.python.org/3/library/argparse.html
When I run the code as is with the terminal input looking like so: python3 stackquestion.py -s cheese, I receive the following error on the line where the function is called:
AttributeError: 'Namespace' object has no attribute 'choice'
I've tried adding the following parameter into parser like so:
parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
I've also tried the above with
parser.add_argument('--choice', '-c')
But I get the same issue on the same line.
#Barmar answered this question in the comments. Using 'default = None' in parser.add_argument works fine; The code runs without any errors. I selected the anser from #BorrajaX because it's a simple solution to my problem.
According to the docs:
Providing default=argparse.SUPPRESS causes no attribute to be added if the command-line argument was not present:
But you're still assuming it will be there by using it in the call to main:
main(args.selection, args.choice)
A suppressed argument won't be there (i.e. there won't be an args.choice in the arguments) unless the caller specifically called your script adding --choice="something". If this doesn't happen, args.choice doesn't exist.
If you really want to use SUPPRESS, you're going to have to check whether the argument is in the args Namespace by doing if 'choice' in args: and operate accordingly.
Another option (probably more common) can be using a specific... thing (normally the value None, which is what argparse uses by default, anyway) to be used as a default, and if args.choice is None, then assume it hasn't been provided by the user.
Maybe you could look at this the other way around: You want to ensure selection is provided and leave choice as optional?
You can try to set up the arguments like this:
parser = argparse.ArgumentParser()
parser.add_argument('--selection', '-s', required=True)
parser.add_argument('--choice', '-c')
args = parser.parse_args()
if __name__ == '__main__':
if args.choice is None:
print("No choice provided")
else:
print(f"Oh, the user provided choice and it's: {args.choice}")
print(f"And selection HAS TO BE THERE, right? {args.selection}")

Problem using '-' inside an optional argument when using Python Argparse

I am parsing an argument input:
python parser_test.py --p "-999,-99;-9"
I get this error:
parser_test.py: error: argument --p: expected one argument
Is there a particular reason why including '-' in the optional argument
"-999,-99;-9"
throws the error even while within double quotes? I need to be able to include the '-' sign.
Here is the code:
import argparse
def main():
parser = argparse.ArgumentParser(description='Input command line arguments for the averaging program')
parser.add_argument('--p', help='input the missing data filler as an integer')
args = parser.parse_args()
if __name__=='__main__':
main()
The quotes do nothing to alter how argparse treats the -; the only purpose they serve is to prevent the shell from treating the ; as a command terminator.
argparse looks at all the arguments first and identifies which ones might be options, regardless of what options are actually defined, by checking which ones start with -. It makes an exception for things that could be negative numbers (like -999), but only if there are no defined options that look like numbers.
The solution is to prevent argparse from seeing -999,-99;-9 as a separate argument. Make it part of the argument that contains the -p using the --name=value form.
python parser_test.py --p="-999,-99;-9"
You can also use "--p=-999,-99;-9" or --p=-999,-99\;-9, among many other possibilities for writing an argument that will cause the shell to parse your command line as two separate commands, python parser_test.py --p-999,-99 and -9.

Order of the arguments matters in getopt

My application parses the command line arguments:
import sys
import getopt
arguments = sys.argv[1:]
options, remainder = getopt.getopt(arguments, "aa:bb:cc:dd:h", ["aaaa=", "bbbb=", "cccc=", "dddd=", "help"])
print dict(options)
This works great but at the same time odd: if I put the arguments in the different order, they aren't get parsed
python my_app.py --aaaa=value1 --bbbb=value2 --cccc=value3 --dddd=value4 #ok
python my_app.py --dddd=value4 --bbbb=value2 --cccc=value3 --aaaa=value1 # empty
That's disappointing because the order of the arguments shouldn't matter, should it? Is there any way to solve that?
UPDATE:
python my_app.py -aa value1 # odd, empty { "-a" : "" }
python my_app.py -a value1 # even this empty { "-a" : "" }
As stated in the first comment to your question, your main example regarding failed parsing of arguments in a different order works just fine:
~/tmp/so$ python my_app.py --aaaa=value1 --bbbb=value2 --cccc=value3 --dddd=value4
{'--aaaa': 'value1', '--cccc': 'value3', '--dddd': 'value4', '--bbbb': 'value2'}
~/tmp/so$ python my_app.py --dddd=value4 --bbbb=value2 --cccc=value3 --aaaa=value1
{'--cccc': 'value3', '--bbbb': 'value2', '--aaaa': 'value1', '--dddd': 'value4'}
If that's not the case for you, please update the script to print the remainder as well, and show its output.
However, you have still misused the getopt library and that's the reason the latest examples you provided don't work as expected. You can't specify more than a single character as an option, since the second character would count as a new separate option. getopt provides no way to differentiate between two consecutive characters that count as a single option (with the first one carrying no argument value, as it is not followed by a colon) or a single option that is composed of two characters. From getopt.getopt's documentation, with my added emphasis:
options is the string of option letters that the script wants to recognize, with options that require an argument followed by a colon.
Therefore, when getopt parses your arguments, each time it encounters a -a argument, it associates it with the first a option it notices, which in your case is not followed by a colon. Thus, it sets this option, discards its argument value, if there was any (if -aa was passed as an argument to the script, the second a counts as the argument value) and moves on to the next argument.
Finally, regarding getopt and argparse. The documentation clearly advocates argparse:
The getopt module is a parser for command line options whose API is designed to be familiar to users of the C getopt() function. Users who are unfamiliar with the C getopt() function or who would like to write less code and get better help and error messages should consider using the argparse module instead.
More about why argparse is better than both getopt and the deprecated optparse can be read in this PEP and in the answers to this question.
The only functionality that I've found to be supported in getopt while it requires a bit of work in argparse is argument order permutation like that of gnu getopt. However, this question explains how this can be achieved via argparse.

Detecting if any command-line options were specified more than once with optparse or argparse

Python optparse normally allows the user to specify an option more than once and silently ignores all occurrences of the option but the last one. For example, if the action of option --foo is store and the action of option --flag is store_const, store_true or store_false, the following commands will be equivalent:
my-command --foo=bar --foo=another --flag --foo=last --flag
my-command --flag --foo=last
(Update: argparse does just the same thing by default.)
Now, I have a lot of options, and specifying any of them more than once doesn't make sense. If a user specifies the same option more than once I'd like to warn them about the possible error.
What is the most elegant way to detect options that were specified multiple times? Note that the same option can have a short form, a long form and abbreviated long forms (so that -f, --foobar, --foob and --foo are all the same option). It would be even better if it was possible to detect the case when multiple options that have the same destination were specified simultaneously, so that a warning can be given if a user specifies both --quiet and --verbose while both options store a value into the same destination and effectively override each other.
Update: To be more user-friendly, the warning should refer to the exact option names as used on the command line. Using append actions instead of store is possible, but when we detect a conflict, we cannot say which options caused it (was it -q and --verbose or --quiet --quiet?).
Unfortunately I'm stuck with optparse and cannot use argparse because I have to support Python 2.6.
P. S. If you know of a solution that works only with argparse, please post it, too. While I try to minimize the number of external dependencies, using argparse under Python 2.6 is still an option.
I think the correct way would be to "define your action" in some way.
For example, you could use the action callback and implement a function that implement your desired behaviour.
You could write a function that first checks if the destination was already filled, if it is filled then it stores the overlapping options into a list.
When the parsing is finished you should check if these lists are empty, and if they are not raise the appropriate exception.
Another way of doing this could be to define your own action. You can have a look here
A small example that uses the callback:
import sys
import functools
from optparse import OptionParser
bad_option = 'BAD OPTION'
def store(option, opt, value, parser, dest, val):
"""Set option's destination *dest* to *val* if there are no conflicting options."""
list_name = dest + '_options_list'
try:
# if this option is a conflict, save its name and set the value to bad_option
getattr(parser.values, list_name).append(opt)
setattr(parser.values, dest, bad_option)
except AttributeError:
# no conflicts, set the option value and add the options list
setattr(parser.values, dest, val)
setattr(parser.values, list_name, [opt])
store_true = functools.partial(store, val=True)
store_false = functools.partial(store, val=False)
parser = OptionParser()
parser.add_option('-v', '--verbose',
action='callback', callback=store_true,
help='Increase output verbosity',
callback_kwargs={'dest': 'verbose'})
parser.add_option('-q', '--quiet',
action='callback', callback=store_false,
help='Decrease output verbosity',
callback_kwargs={'dest': 'verbose'})
opts, args = parser.parse_args()
# detects all conflicting options for all destinations
found = False
for dest in ('verbose',):
if getattr(opts, dest) == bad_option:
conflicting_opts = ', '.join(getattr(opts, dest + '_options_list'))
print('Conflicting options %s for destination %s'
% (conflicting_opts, dest))
found = True
if found:
parser.print_usage()
sys.exit(2)
And the output:
$ python testing_optparse.py -v -q
Conflicting options -v, -q for destination verbose
Usage: prova_optparse.py [options]
Probably it would be better to raise an OptionValueError when detecting conflicts, even though this would allow to get only couple of conflicting options. If you want to get all conflicting options you have to parse the remaining arguments( in parser.rargs).
You can use action="append" (optparse) and then check the number of appended elements. See http://docs.python.org/library/optparse.html#other-actions

Why isn't getopt working if sys.argv is passed fully?

If I'm using this with getopt:
import getopt
import sys
opts,args = getopt.getopt(sys.argv,"a:bc")
print opts
print args
opts will be empty. No tuples will be created. If however, I'll use sys.argv[1:], everything works as expected. I don't understand why that is. Anyone care to explain?
The first element of sys.argv (sys.argv[0]) is the name of the script currently being executed. Because this script name is (likely) not a valid argument (and probably doesn't begin with a - or -- anyway), getopt does not recognize it as an argument. Due to the nature of how getopt works, when it sees something that is not a command-line flag (something that does not begin with - or --), it stops processing command-line options (and puts the rest of the arguments into args), because it assumes the rest of the arguments are items that will be handled by the program (such as filenames or other "required" arguments).
It's by design. Recall that sys.argv[0] is the running program name, and getopt doesn't want it.
From the docs:
Parses command line options and
parameter list. args is the argument
list to be parsed, without the leading
reference to the running program.
Typically, this means sys.argv[1:].
options is the string of option
letters that the script wants to
recognize, with options that require
an argument followed by a colon (':';
i.e., the same format that Unix
getopt() uses).
http://docs.python.org/library/getopt.html

Categories

Resources