Skip to content

Commit 29171ea

Browse files
committed
allow jcvi-cli to launch modules
1 parent 6bd0c58 commit 29171ea

2 files changed

Lines changed: 87 additions & 4 deletions

File tree

src/jcvi/cli.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,47 @@
1-
# cli.py
21
import argparse
2+
import runpy
3+
import sys
34

45
from . import __version__
56

67

7-
def main():
8-
parser = argparse.ArgumentParser()
8+
def _normalize_module_name(module: str) -> str:
9+
if module.startswith("jcvi."):
10+
return module
11+
return f"jcvi.{module}"
12+
13+
14+
def main(argv=None):
15+
parser = argparse.ArgumentParser(
16+
prog="jcvi",
17+
description="Run jcvi modules, e.g. `jcvi formats.fasta extract`.",
18+
)
919
parser.add_argument(
1020
"--version", action="version", version=f"%(prog)s {__version__}"
1121
)
12-
args = parser.parse_args()
22+
parser.add_argument(
23+
"module",
24+
nargs="?",
25+
help="Module path without the `jcvi.` prefix (for example: formats.fasta).",
26+
)
27+
parser.add_argument("module_args", nargs=argparse.REMAINDER)
28+
args = parser.parse_args(argv)
29+
30+
if not args.module:
31+
parser.print_help()
32+
return 0
33+
34+
module_name = _normalize_module_name(args.module)
35+
36+
original_argv = sys.argv[:]
37+
try:
38+
# Match `python -m jcvi.<module> ...` behavior for argument parsing.
39+
sys.argv = [module_name, *args.module_args]
40+
runpy.run_module(module_name, run_name="__main__", alter_sys=True)
41+
finally:
42+
sys.argv = original_argv
43+
44+
return 0
1345

1446

1547
if __name__ == "__main__":

tests/test_cli.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from jcvi import cli
2+
3+
4+
def test_normalize_module_name():
5+
assert cli._normalize_module_name("formats.fasta") == "jcvi.formats.fasta"
6+
assert cli._normalize_module_name("jcvi.formats.fasta") == "jcvi.formats.fasta"
7+
8+
9+
def test_main_dispatches_module(monkeypatch):
10+
called = {}
11+
12+
def fake_run_module(module_name, run_name=None, alter_sys=False):
13+
called["module_name"] = module_name
14+
called["run_name"] = run_name
15+
called["alter_sys"] = alter_sys
16+
called["argv"] = cli.sys.argv[:]
17+
return {}
18+
19+
monkeypatch.setattr(cli.runpy, "run_module", fake_run_module)
20+
rc = cli.main(["formats.fasta", "extract", "in.fa"])
21+
22+
assert rc == 0
23+
assert called["module_name"] == "jcvi.formats.fasta"
24+
assert called["run_name"] == "__main__"
25+
assert called["alter_sys"] is True
26+
assert called["argv"] == ["jcvi.formats.fasta", "extract", "in.fa"]
27+
28+
29+
def test_main_version_exits():
30+
try:
31+
cli.main(["--version"])
32+
except SystemExit as exc:
33+
assert exc.code == 0
34+
else:
35+
raise AssertionError("Expected --version to exit")
36+
37+
38+
def test_main_no_module_prints_help(capsys):
39+
rc = cli.main([])
40+
assert rc == 0
41+
captured = capsys.readouterr()
42+
assert "Run jcvi modules" in captured.out
43+
44+
45+
def test_main_missing_module_raises_import_error():
46+
try:
47+
cli.main(["does.not.exist"])
48+
except ImportError:
49+
pass
50+
else:
51+
raise AssertionError("Expected ImportError for missing module")

0 commit comments

Comments
 (0)