pass JSON dictionary and a char with argparse - python

I'm trying to pass two arguments to python, one read-only JSON file and one char together. In fact I want the arguments both positional, but I couldn't do it either, so let the char one optional. At the end I will read the dictionary and the char will be my key.
When I try like below, the second argument is passed as None. I can't understand why it's empty. Do you have any idea?
Thanks a lot!
python3 myProg.py dict.json -a
import json
import argparse
if __name__ == '__main__':
parser = argparse.ArgumentParser(description = 'program')
parser.add_argument('json', nargs = 1, type = argparse.FileType('r'))
parser.add_argument('-args', nargs ="?")
arguments = parser.parse_args()
dictionary = json.load(arguments.json[0])
arg_start = arguments.args
Recieving this error:
Namespace(args=None, json=[<_io.TextIOWrapper name='dict.json' mode='r' encoding='UTF-8'>])

It looks like you got the json file ok. If you'd omitted the nargs=1 you could have used:
dictionary = json.load(arguments.json)
-args is an nargs='?'. That means it it is ok to omit an argument. If not used at all its value will be the Actions default (currently None).
If provided as just -a, the value is the Action's const (also None).
If provided as -a foo, it will set that string.
If you'd define that argument as positional, plain 'args' (without the -) then it would have accepted the next string after the json file name.

Related

How to pass kwargs to argparse

I need to pass a dictionary to a python module via a command line. I have not found a way to get Python to cooperate. The following is a very simplified example of what I'd like to accomplish. This produces the following error TypeError: _get_positional_kwargs() missing 1 required positional argument: 'dest'
. Adding a dest argument to the add_argument line results in TypeError: __init__() got an unexpected keyword argument 'kwargs', indicating the sad possibility that argparse simply may not take a dictionary as an argument. Is a pickle file truly the only option for passing dictionaries back and forth from command line calls?
def run(**kwargs):
for i in kwargs.items():
print(f'{i}: {kwargs[i]}')
# ...some code to manipulate the dictionary
return kwargs
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Test script")
parser.add_argument(kwargs='**', description='A dictionary to be manipulated and returned')
args = parser.parse_args()
x = run(args)
pass
Additional info - I know that I can pass a list to a *nargs argument with a command line that includes space-delimited arguments, but I need to do something similar with a dictionary.
UPDATE - WHAT WORKED:
Based on the helpful input from #chepner, I was able to get it to work with the json solution he mentioned in his answer. On my Windows platform, I did have to remove all spaces from the dictionary string and escape characters are required in the final command line string.
{\"this\":1,\"is\":2,\"a\":5,\"dictionary\":7}
Arguments are strings, period. What you want to pass is a string that contains an encoded dict, for example using JSON.
import argparse
import json
parser = argparse.ArgumentParser()
parser.add_argument('data', type=json.loads)
args = parser.parse_args()
If you run your script with something like
myscript '{"foo": 3, "bar": "hello"}'
then args.data["foo"] == 3 and args.data["bar"] == "hello".

argparse: how to parse a single string argument OR a file listing many arguments?

I have a use case where I'd like the user to be able to provide, as an argument to argparse, EITHER a single string OR a filename where each line has a string.
Assume the user launches ./myscript.py -i foobar
The logical flow I'm looking for is something like this:
The script determines whether the string foobar is a readable file.
IF it is indeed a readable file, we call some function from the script, passing each line in foobar as an argument to that function. If foobar is not a readable file, we call the same function but just use the string foobar as the argument and return.
I have no ability to guarantee that a filename argument will have a specific extension (or even an extension at all).
Is there a more pythonic way to do this OTHER than just coding up the logic exactly as I've described above? I looked through the argparse tutorial and didn't see anything, but it also seems reasonable to think that there would be some specific hooks for filenames as arguments, so I figured I'd ask.
A way would be:
Let's say that you have created a parser like this:
parser.add_argument('-i',
help='...',
type=function)
Where type points to the function which will be an outer function that evaluates the input of the user and decides if it is a string or a filename
More information about type you can find in the documentation.
Here is a minimal example that demonstrates this use of type:
parser.add_argument('-d','--directory',
type=Val_dir,
help='...')
# ....
def Val_dir(dir):
if not os.path.isdir(dir):
raise argparse.ArgumentTypeError('The directory you specified does not seem to exist!')
else:
return dir
The above example shows that with type we can control the input at parsing time. Of course in your case the function would implement another logic - evaluate if the input is a string or a filename.
This doesn't look like an argparse problem, since all you want from it is a string. That string can be a filename or a function argument. To a parser these will look the same. Also argparse isn't normally used to run functions. It is used to parse the commandline. Your code determines what to do with that information.
So here's a script (untested) that I think does your task:
import argparse
def somefunction(*args):
print(args)
if __name__=='__main__':
parser=argparse.ArgumentParser()
parser.add_argument('-i','--input')
args = parser.parse_args()
try:
with open(args.input) as f:
lines = f.read()
somefunction(*lines)
# or
# for line in lines:
# somefuncion(line.strip())
except:
somefunction(arg.input)
argparse just provides the args.input string. It's the try/except block that determines how it is used.
================
Here's a prefix char approach:
parser=argparse.ArgumentParser(fromfile_prefix_chars='#',
description="use <prog -i #filename> to load values from file")
parser.add_argument('-i','--inputs')
args=parser.parse_args()
for arg in args.inputs:
somefunction(arg)
this is supposed to work with a file like:
one
two
three
https://docs.python.org/3/library/argparse.html#fromfile-prefix-chars

Python argparse: Too few arguments

I'm trying to use the argparse library in Python. I want to have the user do something like:
python my_script.py csv_name.csv [--dryrun]
where --dryrun is an optional parameter.
I then have the user enter an API key and secret key. When I run my code, I get past entering the API and secret keys and then I get:
usage: my_script.py [-h] csv dryrun
salesforceImporter.py: error: too few arguments
Here's my code:
def main():
api_key = getpass.getpass(prompt='Enter API Key: ')
secret_key = getpass.getpass(prompt='Enter Secret Key: ')
parser = argparse.ArgumentParser()
parser.add_argument("csv")
parser.add_argument("dryrun")
args = parser.parse_args()
validate_csv_name(args.csv)
is_dry_run = args.dryrun == '--dryrun'
Any idea where I'm going wrong?
Thanks!
When you use the following syntax:
parser.add_argument("csv")
parser.add_argument("dryrun")
You're adding these as positional -- required -- arguments. Only arguments with a leading dash or two are optional.
See the docs here:
The add_argument() method must know whether an optional argument, like -f or --foo, or a positional argument, like a list of filenames, is expected. The first arguments passed to add_argument() must therefore be either a series of flags, or a simple argument name. For example, an optional argument could be created like:
>>> parser.add_argument('-f', '--foo')
To add an optional --dry-run argument, you may use the following snippet:
parser.add_argument('--dry-run', action='store_true')
Calling your script using python my_script.py csv_name.csv --dry-run will result to args.dry_run being True. Not putting the option will result to it being False

argparse key=value parameters

This first link has the same question in the first section, but it is unanswered
(python argparse: parameter=value). And this second question is similar, but I can't seem to get it working for my particular case
( Using argparse to parse arguments of form "arg= val").
So my situation is this -- I am re-writing a Python wrapper which is used by many other scripts (I would prefer not to modify these other scripts). Currently, the Python wrapper is called with command line arguments of the form --key=value for a number of different arguments, but was parsed manually. I would like to parse them with argparse.
N.B. The argument names are unwieldy, so I am renaming using the dest option in add_argument.
parser = argparse.ArgumentParser(description='Wrappin Ronnie Reagan')
parser.add_argument("--veryLongArgName1", nargs=1, dest="arg1", required=True)
parser.add_argument("--veryLongArgName2", nargs=1, dest="arg2")
parser.add_argument("--veryLongArgName3", nargs=1, dest="arg3")
userOpts = vars(parser.parse_args())
Which, while apparently parsing the passed command lines correctly, displays this as the help:
usage: testing_argsparse.py [-h] --veryLongArgName1 ARG1
[--veryLongArgName2 ARG2]
[--veryLongArgName3 ARG3]
testing_argsparse.py: error: argument --veryLongArgName1 is required
But what I want is that all parameters are specified with the --key=value format, not --key value. i.e.
usage: testing_argsparse.py [-h] --veryLongArgName1=ARG1
[--veryLongArgName2=ARG2]
[--veryLongArgName3=ARG3]
testing_argsparse.py: error: argument --veryLongArgName1 is required
testing_argsparse.py --veryLongArgName1=foo
works. argparse module accepts both --veryLongArgName1=foo and --veryLongArgName1 foo formats.
What exact command line arguments are you trying to pass to argparse that's causing it to not work?
A little late but for anyone with a similar request as the OP you could use a custom HelpFormatter.
class ArgFormatter(argparse.HelpFormatter):
def _format_args(self, *args):
result = super(ArgFormatter, self)._format_args(*args)
return result and '%%%' + result
def _format_actions_usage(self, *args):
result = super(ArgFormatter, self)._format_actions_usage(*args)
return result and result.replace(' %%%', '=')
This can then be passed to ArgumentParser to give the wanted behavior.
parser = argparse.ArgumentParser(
description='Wrappin Ronnie Reagan',
formatter_class=ArgFormatter)
This intercepts the args (ARG1, ARG2, ...) and adds a custom prefix which is later replaced (along with the unwanted space) for an = symbol. The and in the return statements makes sure to only modify the result if it's non-empty.

argparse: setting optional argument with value of mandatory argument

With Python's argparse, I would like to add an optional argument that, if not given, gets the value of another (mandatory) argument.
parser.add_argument('filename',
metavar = 'FILE',
type = str,
help = 'input file'
)
parser.add_argument('--extra-file', '-f',
metavar = 'ANOTHER_FILE',
type = str,
default = ,
help = 'complementary file (default: FILE)'
)
I could of course manually check for None after the arguments are parsed, but isn't there a more pythonic way of doing this?
As far as I know, there is no way to do this that is more clean than:
ns = parser.parse_args()
ns.extra_file = ns.extra_file if ns.extra_file else ns.filename
(just like you propose in your question).
You could probably do some custom action gymnastics similar to this, but I really don't think that would be worthwhile (or "pythonic").
This has slightly different semantics than your original set of options, but may work for you:
parse.add_argument('filename', action='append', dest='filenames')
parse.add_argument('--extra-file', '-f', action='append', dest='filenames')
args = parse.parse_args()
This would replace args.filename with a list args.filenames of at least one file, with -f appending its argument to that list. Since it's possible to specify -f on the command line before the positional argument, you couldn't on any particular order of the input files in args.filenames.
Another option would be to dispense with the -f option and make filename a multi-valued positional argument:
parse.add_argument('filenames', nargs='+')
Again, args.filenames would be a list of at least one filename. This would also interfere if you have other positional arguments for your script.

Categories