I have a CLI written with argparse
and I was wondering if there was a way to produce a JSON schema from the ArgumentParser
? The thought behind this being to distribute the JSON schema to extensions interfacing with the application, thus removing the need for each extension to write and maintain their own schema.
My idea was to
argparse.ArgumentParser
to Python dictionary or JSON fileimport argparse
from genson import SchemaBuilder
parser = argparse.ArgumentParser(
description="Some description", prog="myprog", usage="myprog [options]"
)
parser.add_argument(
"-v",
"--version",
action="store_true",
help="Print server version number and exit",
)
parser.add_argument(
"-c",
"--config",
type=str,
default=".fortls",
help="Configuration options file (default file name: %(default)s)",
)
args = vars(parser.parse_args(""))
# Generate schema
builder = SchemaBuilder()
builder.add_schema({"type": "object", "properties": {}})
for k, v in args.items():
builder.add_object({k: v})
print(builder.to_json(indent=2))
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"version": {
"type": "boolean"
},
"config": {
"type": "string"
}
}
}
However, I quickly realised that calling vars(parser().parse_args(""))
to convert the CLI into a dictionary resulted into a lot of information being lost, like descriptions and required.
Is there another way of doing this? I am open to swappingargparse
with some other CLI if it would make generating a schema easier.
The solution that I came up with was to access the private variable _actions
from ArgumentParser
and convert that to a schema using pydantic
. In my specific case it was quite easy to do since all the arguments in argparse
were optional. If not, a bit more thought has to be put when creating the model with pydantic
from __future__ import annotations
from pydantic import Field, create_model
import argparse
parser = argparse.ArgumentParser(
description="Some description", prog="myprog", usage="myprog [options]"
)
parser.add_argument(
"-v",
"--version",
action="store_true",
help="Print server version number and exit",
)
parser.add_argument(
"-c",
"--config",
type=str,
default=".fortls",
help="Configuration options file (default file name: %(default)s)",
)
schema_vals = {}
for arg in parser._actions:
# if condition for arguments to exclude:
# continue
val = arg.default
desc: str = arg.help.replace("%(default)s", str(val)) # type: ignore
schema_vals[arg.dest] = Field(val, description=desc) # type: ignore
m = create_model("MySchema", **schema_vals)
m.__doc__ = "Some description"
with open("schema.json", "w") as f:
print(m.schema_json(indent=2), file=f)
Output
{
"title": "MySchema",
"description": "Some description",
"type": "object",
"properties": {
"help": {
"title": "Help",
"description": "show this help message and exit",
"default": "==SUPPRESS==",
"type": "string"
},
"version": {
"title": "Version",
"description": "Print server version number and exit",
"default": false,
"type": "boolean"
},
"config": {
"title": "Config",
"description": "Configuration options file (default file name: .fortls)",
"default": ".fortls",
"type": "string"
}
}
}