Skip to content

Commit 17e7d61

Browse files
feat: add command line interface (#72)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a command-line interface (CLI) for argument checking in Python programs. - Added a script entry point for the CLI in project configuration. - Enhanced documentation with a new section on CLI usage. - **Bug Fixes** - Updated type annotations and added default arguments to improve the robustness of the `check` function. - **Documentation** - Added CLI module documentation. - Updated module references for better documentation rendering. - **Tests** - Added test cases to validate the new CLI functionality. - Introduced new unit tests for CLI command verification. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Jinzhe Zeng <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 01e37f5 commit 17e7d61

File tree

13 files changed

+246
-24
lines changed

13 files changed

+246
-24
lines changed

dargs/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from __future__ import annotations
2+
3+
from dargs.cli import main
4+
5+
if __name__ == "__main__":
6+
main()

dargs/_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from typing import List
4+
5+
from dargs.dargs import Argument
6+
7+
8+
def test_arguments() -> list[Argument]:
9+
"""Returns a list of arguments."""
10+
return [
11+
Argument(name="test1", dtype=int, doc="Argument 1"),
12+
Argument(name="test2", dtype=[float, None], doc="Argument 2"),
13+
Argument(
14+
name="test3",
15+
dtype=List[str],
16+
default=["test"],
17+
optional=True,
18+
doc="Argument 3",
19+
),
20+
]
21+
22+
23+
__all__ = [
24+
"test_arguments",
25+
]

dargs/check.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
from dargs.dargs import Argument
4+
5+
6+
def check(
7+
arginfo: Argument | list[Argument] | tuple[Argument, ...],
8+
data: dict,
9+
strict: bool = True,
10+
trim_pattern: str = "_*",
11+
) -> dict:
12+
"""Check and normalize input data.
13+
14+
Parameters
15+
----------
16+
arginfo : Union[Argument, List[Argument], Tuple[Argument, ...]]
17+
Argument object
18+
data : dict
19+
data to check
20+
strict : bool, optional
21+
If True, raise an error if the key is not pre-defined, by default True
22+
trim_pattern : str, optional
23+
Pattern to trim the key, by default "_*"
24+
25+
Returns
26+
-------
27+
dict
28+
normalized data
29+
"""
30+
if isinstance(arginfo, (list, tuple)):
31+
arginfo = Argument("base", dtype=dict, sub_fields=arginfo)
32+
33+
data = arginfo.normalize_value(data, trim_pattern=trim_pattern)
34+
arginfo.check_value(data, strict=strict)
35+
return data

dargs/cli.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import json
5+
import sys
6+
from typing import IO
7+
8+
from dargs._version import __version__
9+
from dargs.check import check
10+
11+
12+
def main_parser() -> argparse.ArgumentParser:
13+
"""Create the main parser for the command line interface.
14+
15+
Returns
16+
-------
17+
argparse.ArgumentParser
18+
The main parser
19+
"""
20+
parser = argparse.ArgumentParser(
21+
description="dargs: Argument checking for Python programs"
22+
)
23+
subparsers = parser.add_subparsers(help="Sub-commands")
24+
parser_check = subparsers.add_parser(
25+
"check",
26+
help="Check a JSON file against an Argument",
27+
epilog="Example: dargs check -f dargs._test.test_arguments test_arguments.json",
28+
)
29+
parser_check.add_argument(
30+
"-f",
31+
"--func",
32+
type=str,
33+
help="Function that returns an Argument object. E.g., `dargs._test.test_arguments`",
34+
required=True,
35+
)
36+
parser_check.add_argument(
37+
"jdata",
38+
type=argparse.FileType("r"),
39+
default=[sys.stdin],
40+
nargs="*",
41+
help="Path to the JSON file. If not given, read from stdin.",
42+
)
43+
parser_check.add_argument(
44+
"--no-strict",
45+
action="store_false",
46+
dest="strict",
47+
help="Do not raise an error if the key is not pre-defined",
48+
)
49+
parser_check.add_argument(
50+
"--trim-pattern",
51+
type=str,
52+
default="_*",
53+
help="Pattern to trim the key",
54+
)
55+
parser_check.set_defaults(entrypoint=check_cli)
56+
57+
# --version
58+
parser.add_argument("--version", action="version", version=__version__)
59+
return parser
60+
61+
62+
def main():
63+
"""Main entry point for the command line interface."""
64+
parser = main_parser()
65+
args = parser.parse_args()
66+
67+
args.entrypoint(**vars(args))
68+
69+
70+
def check_cli(
71+
*,
72+
func: str,
73+
jdata: list[IO],
74+
strict: bool,
75+
**kwargs,
76+
) -> None:
77+
"""Normalize and check input data.
78+
79+
Parameters
80+
----------
81+
func : str
82+
Function that returns an Argument object. E.g., `dargs._test.test_arguments`
83+
jdata : IO
84+
File object that contains the JSON data
85+
strict : bool
86+
If True, raise an error if the key is not pre-defined
87+
88+
Returns
89+
-------
90+
dict
91+
normalized data
92+
"""
93+
module_name, attr_name = func.rsplit(".", 1)
94+
try:
95+
mod = __import__(module_name, globals(), locals(), [attr_name])
96+
except ImportError as e:
97+
raise RuntimeError(
98+
f'Failed to import "{attr_name}" from "{module_name}".\n{sys.exc_info()[1]}'
99+
) from e
100+
101+
if not hasattr(mod, attr_name):
102+
raise RuntimeError(f'Module "{module_name}" has no attribute "{attr_name}"')
103+
func_obj = getattr(mod, attr_name)
104+
arginfo = func_obj()
105+
for jj in jdata:
106+
data = json.load(jj)
107+
check(arginfo, data, strict=strict)

dargs/sphinx.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ def run(self):
5757

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

6565
if not hasattr(mod, attr_name):
6666
raise self.error(
@@ -217,18 +217,3 @@ def _test_argument() -> Argument:
217217
),
218218
],
219219
)
220-
221-
222-
def _test_arguments() -> list[Argument]:
223-
"""Returns a list of arguments."""
224-
return [
225-
Argument(name="test1", dtype=int, doc="Argument 1"),
226-
Argument(name="test2", dtype=[float, None], doc="Argument 2"),
227-
Argument(
228-
name="test3",
229-
dtype=List[str],
230-
default=["test"],
231-
optional=True,
232-
doc="Argument 3",
233-
),
234-
]

docs/cli.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. _cli:
2+
3+
Command line interface
4+
======================
5+
6+
.. argparse::
7+
:module: dargs.cli
8+
:func: main_parser
9+
:prog: dargs

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"numpydoc",
5050
"myst_nb",
5151
"dargs.sphinx",
52+
"sphinxarg.ext",
5253
]
5354

5455
# Add any paths that contain templates here, relative to this directory.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Welcome to dargs's documentation!
1111
:caption: Contents:
1212

1313
intro
14+
cli
1415
sphinx
1516
dpgui
1617
nb

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ numpydoc
33
deepmodeling_sphinx>=0.1.1
44
myst-nb
55
sphinx_rtd_theme
6+
sphinx-argparse

docs/sphinx.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ Then `dargs` directive will be enabled:
1515
.. code-block:: rst
1616
1717
.. dargs::
18-
:module: dargs.sphinx
19-
:func: _test_argument
18+
:module: dargs._test
19+
:func: test_argument
2020
21-
where `_test_argument` returns an :class:`Argument <dargs.Argument>`. The documentation will be rendered as:
21+
where `test_argument` returns an :class:`Argument <dargs.Argument>`. The documentation will be rendered as:
2222

2323
.. dargs::
24-
:module: dargs.sphinx
25-
:func: _test_argument
24+
:module: dargs._test
25+
:func: test_argument
2626

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

2929
.. dargs::
30-
:module: dargs.sphinx
31-
:func: _test_arguments
30+
:module: dargs._test
31+
:func: test_arguments
3232

3333
Cross-referencing Arguments
3434
---------------------------

0 commit comments

Comments
 (0)