from __future__ import annotations
import argparse
import json
import sys
from typing import IO, Any
from dargs._version import __version__
from dargs.check import check
[docs]
def main_parser() -> argparse.ArgumentParser:
"""Create the main parser for the command line interface.
Returns
-------
argparse.ArgumentParser
The main parser
"""
parser = argparse.ArgumentParser(
description="dargs: Argument checking for Python programs"
)
subparsers = parser.add_subparsers(help="Sub-commands")
parser_check = subparsers.add_parser(
"check",
help="Check a JSON file against an Argument",
epilog="Example: dargs check -f dargs._test.test_arguments test_arguments.json",
)
parser_check.add_argument(
"-f",
"--func",
type=str,
help="Function that returns an Argument object. E.g., `dargs._test.test_arguments`",
required=True,
)
parser_check.add_argument(
"jdata",
type=argparse.FileType("r"),
default=[sys.stdin],
nargs="*",
help="Path to the JSON file. If not given, read from stdin.",
)
parser_check.add_argument(
"--no-strict",
action="store_false",
dest="strict",
help="Do not raise an error if the key is not pre-defined",
)
parser_check.add_argument(
"--trim-pattern",
type=str,
default="_*",
help="Pattern to trim the key",
)
parser_check.add_argument(
"--allow-ref",
action="store_true",
dest="allow_ref",
help="Allow loading from external files via the $ref key",
)
parser_check.set_defaults(entrypoint=check_cli)
# doc subcommand
parser_doc = subparsers.add_parser(
"doc",
help="Print documentation for an Argument",
epilog="Example: dargs doc dargs._test.test_arguments [arg_path]",
)
parser_doc.add_argument(
"func",
type=str,
help="Function that returns an Argument or list of Arguments. E.g., `dargs._test.test_arguments`",
)
parser_doc.add_argument(
"arg",
type=str,
nargs="?",
default=None,
help="Optional argument path (e.g., 'base/sub1'). If not provided, prints all top-level arguments.",
)
parser_doc.set_defaults(entrypoint=doc_cli)
# --version
parser.add_argument("--version", action="version", version=__version__)
return parser
[docs]
def main() -> None:
"""Main entry point for the command line interface."""
parser = main_parser()
args = parser.parse_args()
args.entrypoint(**vars(args))
[docs]
def check_cli(
*,
func: str,
jdata: list[IO],
strict: bool,
allow_ref: bool = False,
**kwargs: Any,
) -> None:
"""Normalize and check input data.
Parameters
----------
func : str
Function that returns an Argument object. E.g., `dargs._test.test_arguments`
jdata : IO
File object that contains the JSON data
strict : bool
If True, raise an error if the key is not pre-defined
allow_ref : bool, optional
If True, allow loading from external files via the ``$ref`` key
Returns
-------
dict
normalized data
"""
module_name, attr_name = func.strip().rsplit(".", 1)
try:
mod = __import__(module_name, globals(), locals(), [attr_name])
except ImportError as e:
raise RuntimeError(
f'Failed to import "{attr_name}" from "{module_name}".\n{sys.exc_info()[1]}'
) from e
if not hasattr(mod, attr_name):
raise RuntimeError(f'Module "{module_name}" has no attribute "{attr_name}"')
func_obj = getattr(mod, attr_name)
arginfo = func_obj()
for jj in jdata:
data = json.load(jj)
check(arginfo, data, strict=strict, allow_ref=allow_ref)
[docs]
def doc_cli(
*,
func: str,
arg: str | None = None,
**kwargs: Any,
) -> None:
"""Print documentation for an Argument.
Parameters
----------
func : str
Function that returns an Argument or list of Arguments. E.g., `dargs._test.test_arguments`
arg : str, optional
Optional argument path (e.g., 'base/sub1'). If not provided, prints all top-level arguments.
"""
try:
module_name, attr_name = func.strip().rsplit(".", 1)
except ValueError as e:
raise RuntimeError(
f'Function must be in format "module.function", got: "{func}"'
) from e
try:
mod = __import__(module_name, globals(), locals(), [attr_name])
except ImportError as e:
raise RuntimeError(
f'Failed to import "{attr_name}" from "{module_name}".\n{sys.exc_info()[1]}'
) from e
if not hasattr(mod, attr_name):
raise RuntimeError(f'Module "{module_name}" has no attribute "{attr_name}"')
func_obj = getattr(mod, attr_name)
arginfo = func_obj()
# Handle both single Argument and iterable of Arguments (list or tuple)
if isinstance(arginfo, (list, tuple)):
args_list = list(arginfo)
else:
args_list = [arginfo]
# Validate that each item looks like an Argument/Variant before using it
for index, argument in enumerate(args_list):
if not (
hasattr(argument, "name")
and hasattr(argument, "sub_fields")
and callable(getattr(argument, "gen_doc", None))
):
raise RuntimeError(
f"Invalid argument object at index {index}: expected an object with "
'"name", "sub_fields", and "gen_doc()" attributes, '
f"got {type(argument)!r}"
)
# If no specific arg path is provided, print all top-level arguments
if arg is None:
for argument in args_list:
print(argument.gen_doc())
print() # Add blank line between arguments
else:
# Navigate to the specific argument by path
path_parts = arg.split("/")
found = False
# First, try to find the argument in the top-level list
for argument in args_list:
if argument.name == path_parts[0]:
# Found the top-level argument
current_arg = argument
# Navigate through sub-fields if path has more parts
for part in path_parts[1:]:
if part in current_arg.sub_fields:
current_arg = current_arg.sub_fields[part]
else:
raise RuntimeError(
f'Argument path "{arg}" not found: "{part}" is not a sub-field of "{current_arg.name}"'
)
# Pass the parent path so gen_doc can render the full argument path
parent_path = path_parts[:-1]
print(current_arg.gen_doc(path=parent_path))
found = True
break
if not found:
raise RuntimeError(
f'Argument path "{arg}" not found: no top-level argument named "{path_parts[0]}"'
)