1

I am using argparse to specify some arguments as follows:

my_parser = argparse.ArgumentParser()
my_parser.add_argument("--script_path", nargs='?', type=str, const='', default='', help="Path of the script to pull.")
my_parser.add_argument("--script", nargs='?', type=str, const='', default='', help="Name of the script to get pulled, without script extension.")
my_parser.add_argument("--project", nargs='?', type=str, const='', default='', help="Project.")
my_args = my_parser.parse_args()
my_script_path = my_args.script_path
my_script = my_args.script
my_project = my_args.project

Now I am trying to do the same but instead to have the above arguments defined via a .json file that I would load. I chose .json because it seemed right, feel free to suggest something better.

What I have tried is having a .json file like this:

[
{
    "name_or_flags": ["-sp", "--script_path"],
    "nargs": "?",
    "const": "",
    "default": "",
    "type": "str",
    "help": "The absolute path of the script to run."
},
...
]

After I loaded the file, I tried and failed in the below:

my_parser.add_argument(<combination of all keys, values from .json as a dictionary>)

my_parser.add_argument(<*unnamed_tup, **named_dict>) 
    #unnamed tuple since name_or_flags isn't supposed to be used
    #unnamed tuple is only made from name_or_flags

No matter what I do it doesn't work.

Has anyone done something similar?

I am not looking to add values via the external file, like in: Using Argparse and Json together

Just to define the arguments.

Thanks!

Lev
  • 673
  • 1
  • 12
  • 29
  • since you've only supplied pseudocode for your add_argument code, you'll need to specify what error message you receive back, there could be myriad things going wrong – Lachlan Aug 16 '21 at 15:09

3 Answers3

1

You need to pop("name_or_flags"), with the attention of providing always a list; furthermore, you need to exclude type, because it raises an error (being a string instead of a class or function).

import argparse
import json

args = json.loads("""
[{
    "name_or_flags": "-sp", "--script_path"],
    "nargs": "?",
    "const": "",
    "default": "",
    "type": "str",
    "help": "The absolute path of the script to run."
}]
""")
parser = argparse.ArgumentParser()

for arg in args:
    arg.pop("type", None)  # will raise ValueError: 'str' is not callable
    parser.add_argument(*arg.pop("name_or_flags"), **arg)

# If the type is important to keep, you can always create a dictionary to map a string to a value, that is a builtin class.
mapping = dict(str=str, bool=bool, int=int)  # this will map strings to classes
for arg in args:
    thetype_str = arg.pop("type", "str")
    arg["type"] = mapping.get(thetype_str, str)  # if missing or wrong, will give plain string
    parser.add_argument(*arg.pop("name_or_flags"), **arg)
crissal
  • 2,547
  • 7
  • 25
  • Removing the type would result in that part of the arg parser validation not working. – KillerKode Aug 16 '21 at 15:25
  • Why it shouldn't work? You can always `eval("str")` to obtain `str` class, but that's a really bad idea - and by default argparse works with only strings as argument parsed. – crissal Aug 16 '21 at 15:26
  • Because if he had a "type": "bool" and you disregard that, it would convert it to a string incorrectly, so just disregarding the type is probably not what he wants. – KillerKode Aug 16 '21 at 15:33
  • You're right, I've added a safe way to do this. – crissal Aug 16 '21 at 15:46
1

This is how you would do it from a file:

import argparse

jdata = [
    {
        "args": ["--script_path"],
        "kwargs": {
            "nargs": "?",
            "const": "",
            "default": "",
            "type": "str",
            "help": "The absolute path of the script to run.",
        },
    } ]

my_parser = argparse.ArgumentParser()

for i in jdata:
    i["kwargs"]["type"] = eval(i["kwargs"]["type"])
    my_parser.add_argument(*tuple(i["args"]), **i["kwargs"])

my_args = my_parser.parse_args() my_script_path = my_args.script_path

print(my_script_path)

Note: Some of the data coming from the JSON file needs to be converted, for example the type needs to be converted to a Python type, before it is passed to the method.

KillerKode
  • 957
  • 1
  • 12
  • 31
  • Note that `type("int") is str`; anything will evaluate to string. – crissal Aug 16 '21 at 15:22
  • Note that [using `eval` is a bad practice, and should not be enforced](https://stackoverflow.com/questions/1832940/why-is-using-eval-a-bad-practice). Try `eval`-uating `"bool"` first and `"bool and print('should not be printed')"` after. – crissal Aug 16 '21 at 15:37
0

Hope this helps (you should find your way through from my comments).

import argparse
import json
from pydoc import locate

# Load the json
f = open("args.json")
args = json.load(f)
# Pre-processing that converts "str" -> <class "str">, "int" -> <class "int">, etc.
for arg in args:
    if "type" in arg.keys():
        arg["type"] = locate(arg["type"])

my_parser = argparse.ArgumentParser()

# Normal method of arg-parse for comparision
my_parser.add_argument(
    "-sp-normal",
    "--script-path-normal",
    nargs="?",
    const="",
    default="",
    type=str,
    help="The absolute path of the script to run.",
)
# Loading from the JSON
for arg in args:
    my_parser.add_argument(*arg.pop("name_or_flags"), **arg)

# Load the args
my_args = my_parser.parse_args()

# Check
script_path = my_args.script_path
script_path_1 = my_args.script_path_1
script_path_normal = my_args.script_path_normal
print(script_path, script_path_1, script_path_normal)

The JSON I used:

[
  {
    "name_or_flags": [
      "-sp",
      "--script-path"
    ],
    "nargs": "?",
    "const": "",
    "default": "",
    "type": "str",
    "help": "The absolute path of the script to run."
  },
  {
    "name_or_flags": [
      "-sp-1",
      "--script-path-1"
    ],
    "nargs": "?",
    "const": "",
    "default": "",
    "type": "str",
    "help": "The absolute path of the script to run."
  }
]
S P Sharan
  • 1,101
  • 9
  • 18