Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add command line interface #72

Merged
merged 7 commits into from
Jul 14, 2024
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

Check warning on line 1 in dargs/__main__.py

View check run for this annotation

Codecov / codecov/patch

dargs/__main__.py#L1

Added line #L1 was not covered by tests

from dargs.cli import main

Check warning on line 3 in dargs/__main__.py

View check run for this annotation

Codecov / codecov/patch

dargs/__main__.py#L3

Added line #L3 was not covered by tests

if __name__ == "__main__":
main()

Check warning on line 6 in dargs/__main__.py

View check run for this annotation

Codecov / codecov/patch

dargs/__main__.py#L5-L6

Added lines #L5 - L6 were not covered by tests
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

Check warning on line 1 in dargs/_test.py

View check run for this annotation

Codecov / codecov/patch

dargs/_test.py#L1

Added line #L1 was not covered by tests

from typing import List

Check warning on line 3 in dargs/_test.py

View check run for this annotation

Codecov / codecov/patch

dargs/_test.py#L3

Added line #L3 was not covered by tests

from dargs.dargs import Argument

Check warning on line 5 in dargs/_test.py

View check run for this annotation

Codecov / codecov/patch

dargs/_test.py#L5

Added line #L5 was not covered by tests


def test_arguments() -> list[Argument]:

Check warning on line 8 in dargs/_test.py

View check run for this annotation

Codecov / codecov/patch

dargs/_test.py#L8

Added line #L8 was not covered by tests
"""Returns a list of arguments."""
return [

Check warning on line 10 in dargs/_test.py

View check run for this annotation

Codecov / codecov/patch

dargs/_test.py#L10

Added line #L10 was not covered by tests
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__ = [

Check warning on line 23 in dargs/_test.py

View check run for this annotation

Codecov / codecov/patch

dargs/_test.py#L23

Added line #L23 was not covered by tests
"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

Check warning on line 1 in dargs/check.py

View check run for this annotation

Codecov / codecov/patch

dargs/check.py#L1

Added line #L1 was not covered by tests

from dargs.dargs import Argument

Check warning on line 3 in dargs/check.py

View check run for this annotation

Codecov / codecov/patch

dargs/check.py#L3

Added line #L3 was not covered by tests


def check(

Check warning on line 6 in dargs/check.py

View check run for this annotation

Codecov / codecov/patch

dargs/check.py#L6

Added line #L6 was not covered by tests
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)

Check warning on line 31 in dargs/check.py

View check run for this annotation

Codecov / codecov/patch

dargs/check.py#L30-L31

Added lines #L30 - L31 were not covered by tests

data = arginfo.normalize_value(data, trim_pattern=trim_pattern)
arginfo.check_value(data, strict=strict)
return data

Check warning on line 35 in dargs/check.py

View check run for this annotation

Codecov / codecov/patch

dargs/check.py#L33-L35

Added lines #L33 - L35 were not covered by tests
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

Check warning on line 1 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L1

Added line #L1 was not covered by tests

import argparse
import json
import sys
from typing import IO

Check warning on line 6 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L3-L6

Added lines #L3 - L6 were not covered by tests

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

Check warning on line 9 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L8-L9

Added lines #L8 - L9 were not covered by tests


def main_parser() -> argparse.ArgumentParser:

Check warning on line 12 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L12

Added line #L12 was not covered by tests
"""Create the main parser for the command line interface.

Returns
-------
argparse.ArgumentParser
The main parser
"""
parser = argparse.ArgumentParser(

Check warning on line 20 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L20

Added line #L20 was not covered by tests
description="dargs: Argument checking for Python programs"
)
subparsers = parser.add_subparsers(help="Sub-commands")
parser_check = subparsers.add_parser(

Check warning on line 24 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L23-L24

Added lines #L23 - L24 were not covered by tests
"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(

Check warning on line 29 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L29

Added line #L29 was not covered by tests
"-f",
"--func",
type=str,
help="Function that returns an Argument object. E.g., `dargs._test.test_arguments`",
required=True,
)
parser_check.add_argument(

Check warning on line 36 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L36

Added line #L36 was not covered by tests
"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(

Check warning on line 43 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L43

Added line #L43 was not covered by tests
"--no-strict",
action="store_false",
dest="strict",
help="Do not raise an error if the key is not pre-defined",
)
parser_check.add_argument(

Check warning on line 49 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L49

Added line #L49 was not covered by tests
"--trim-pattern",
type=str,
default="_*",
help="Pattern to trim the key",
)
parser_check.set_defaults(entrypoint=check_cli)

Check warning on line 55 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L55

Added line #L55 was not covered by tests

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

Check warning on line 59 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L58-L59

Added lines #L58 - L59 were not covered by tests
njzjz marked this conversation as resolved.
Show resolved Hide resolved


def main():

Check warning on line 62 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L62

Added line #L62 was not covered by tests
"""Main entry point for the command line interface."""
parser = main_parser()
args = parser.parse_args()

Check warning on line 65 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L64-L65

Added lines #L64 - L65 were not covered by tests

args.entrypoint(**vars(args))

Check warning on line 67 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L67

Added line #L67 was not covered by tests
njzjz marked this conversation as resolved.
Show resolved Hide resolved


def check_cli(

Check warning on line 70 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L70

Added line #L70 was not covered by tests
*,
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(

Check warning on line 97 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L93-L97

Added lines #L93 - L97 were not covered by tests
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)

Check warning on line 107 in dargs/cli.py

View check run for this annotation

Codecov / codecov/patch

dargs/cli.py#L101-L107

Added lines #L101 - L107 were not covered by tests
njzjz marked this conversation as resolved.
Show resolved Hide resolved
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 @@

try:
mod = __import__(module_name, globals(), locals(), [attr_name])
except ImportError:
except ImportError as e:

Check warning on line 60 in dargs/sphinx.py

View check run for this annotation

Codecov / codecov/patch

dargs/sphinx.py#L60

Added line #L60 was not covered by tests
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_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,
)