Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dargs/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from __future__ import annotations

from dargs.cli import main

if __name__ == "__main__":
main()
25 changes: 25 additions & 0 deletions dargs/_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from typing import List

from dargs.dargs import Argument


def test_arguments() -> list[Argument]:
"""Returns a list of arguments."""
return [
Argument(name="test1", dtype=int, doc="Argument 1"),
Argument(name="test2", dtype=[float, None], doc="Argument 2"),
Argument(
name="test3",
dtype=List[str],
default=["test"],
optional=True,
doc="Argument 3",
),
]


__all__ = [
"test_arguments",
]
35 changes: 35 additions & 0 deletions dargs/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations

from dargs.dargs import Argument


def check(
arginfo: Argument | list[Argument] | tuple[Argument, ...],
data: dict,
strict: bool = True,
trim_pattern: str = "_*",
) -> dict:
"""Check and normalize input data.

Parameters
----------
arginfo : Union[Argument, List[Argument], Tuple[Argument, ...]]
Argument object
data : dict
data to check
strict : bool, optional
If True, raise an error if the key is not pre-defined, by default True
trim_pattern : str, optional
Pattern to trim the key, by default "_*"

Returns
-------
dict
normalized data
"""
if isinstance(arginfo, (list, tuple)):
arginfo = Argument("base", dtype=dict, sub_fields=arginfo)

data = arginfo.normalize_value(data, trim_pattern=trim_pattern)
arginfo.check_value(data, strict=strict)
return data
107 changes: 107 additions & 0 deletions dargs/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from __future__ import annotations

import argparse
import json
import sys
from typing import IO

from dargs._version import __version__
from dargs.check import check


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.set_defaults(entrypoint=check_cli)

# --version
parser.add_argument("--version", action="version", version=__version__)
return parser


def main():
"""Main entry point for the command line interface."""
parser = main_parser()
args = parser.parse_args()

args.entrypoint(**vars(args))


def check_cli(
*,
func: str,
jdata: list[IO],
strict: bool,
**kwargs,
) -> 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

Returns
-------
dict
normalized data
"""
module_name, attr_name = func.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)
19 changes: 2 additions & 17 deletions dargs/sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ def run(self):

try:
mod = __import__(module_name, globals(), locals(), [attr_name])
except ImportError:
except ImportError as e:
raise self.error(
f'Failed to import "{attr_name}" from "{module_name}".\n{sys.exc_info()[1]}'
)
) from e

if not hasattr(mod, attr_name):
raise self.error(
Expand Down Expand Up @@ -217,18 +217,3 @@ def _test_argument() -> Argument:
),
],
)


def _test_arguments() -> list[Argument]:
"""Returns a list of arguments."""
return [
Argument(name="test1", dtype=int, doc="Argument 1"),
Argument(name="test2", dtype=[float, None], doc="Argument 2"),
Argument(
name="test3",
dtype=List[str],
default=["test"],
optional=True,
doc="Argument 3",
),
]
9 changes: 9 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. _cli:

Command line interface
======================

.. argparse::
:module: dargs.cli
:func: main_parser
:prog: dargs
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"numpydoc",
"myst_nb",
"dargs.sphinx",
"sphinxarg.ext",
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Welcome to dargs's documentation!
:caption: Contents:

intro
cli
sphinx
dpgui
nb
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ numpydoc
deepmodeling_sphinx>=0.1.1
myst-nb
sphinx_rtd_theme
sphinx-argparse
14 changes: 7 additions & 7 deletions docs/sphinx.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ Then `dargs` directive will be enabled:
.. code-block:: rst

.. dargs::
:module: dargs.sphinx
:func: _test_argument
:module: dargs._test
:func: test_argument

where `_test_argument` returns an :class:`Argument <dargs.Argument>`. The documentation will be rendered as:
where `test_argument` returns an :class:`Argument <dargs.Argument>`. The documentation will be rendered as:

.. dargs::
:module: dargs.sphinx
:func: _test_argument
:module: dargs._test
:func: test_argument

A :class:`list` of :class:`Argument <dargs.Argument>` is also accepted.

.. dargs::
:module: dargs.sphinx
:func: _test_arguments
:module: dargs._test
:func: test_arguments

Cross-referencing Arguments
---------------------------
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ typecheck = [
"ipython",
]

[project.scripts]
dargs = "dargs.cli:main"

[tool.setuptools.packages.find]
include = ["dargs*"]

Expand All @@ -54,6 +57,7 @@ select = [
"RUF", # ruff
"I", # isort
"TCH", # flake8-type-checking
"B904", # raise-without-from-inside-except
]

ignore = [
Expand Down
4 changes: 4 additions & 0 deletions tests/test_arguments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"test1": 1,
"test2": 2
}
44 changes: 44 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

import subprocess
import sys
import unittest
from pathlib import Path

this_directory = Path(__file__).parent


class TestCli(unittest.TestCase):
def test_check(self):
subprocess.check_call(
[
"dargs",
"check",
"-f",
"dargs._test.test_arguments",
str(this_directory / "test_arguments.json"),
str(this_directory / "test_arguments.json"),
]
)
subprocess.check_call(
[
sys.executable,
"-m",
"dargs",
"check",
"-f",
"dargs._test.test_arguments",
str(this_directory / "test_arguments.json"),
str(this_directory / "test_arguments.json"),
]
)
with (this_directory / "test_arguments.json").open() as f:
subprocess.check_call(
[
"dargs",
"check",
"-f",
"dargs._test.test_arguments",
],
stdin=f,
)