Skip to content

Commit f0d83a8

Browse files
Merge pull request #1181 from datajoint/cli-1095-continued
DataJoint CLI continued (#1095)
2 parents ed9a520 + 67c6b5a commit f0d83a8

File tree

5 files changed

+199
-0
lines changed

5 files changed

+199
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 0.14.3 -- TBD
44
- Added - `dj.Top` restriction ([#1024](https://github.com/datajoint/datajoint-python/issues/1024)) PR [#1084](https://github.com/datajoint/datajoint-python/pull/1084)
55
- Fixed - Added encapsulating double quotes to comply with [DOT language](https://graphviz.org/doc/info/lang.html) - PR [#1177](https://github.com/datajoint/datajoint-python/pull/1177)
6+
- Added - Datajoint python CLI ([#940](https://github.com/datajoint/datajoint-python/issues/940)) PR [#1095](https://github.com/datajoint/datajoint-python/pull/1095)
67
- Added - Ability to set hidden attributes on a table - PR [#1091](https://github.com/datajoint/datajoint-python/pull/1091)
78

89
### 0.14.2 -- Aug 19, 2024

datajoint/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"key",
5353
"key_hash",
5454
"logger",
55+
"cli",
5556
]
5657

5758
from .logging import logger
@@ -71,6 +72,7 @@
7172
from .attribute_adapter import AttributeAdapter
7273
from . import errors
7374
from .errors import DataJointError
75+
from .cli import cli
7476

7577
ERD = Di = Diagram # Aliases for Diagram
7678
schema = Schema # Aliases for Schema

datajoint/cli.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import argparse
2+
from code import interact
3+
from collections import ChainMap
4+
import datajoint as dj
5+
6+
7+
def cli(args: list = None):
8+
"""
9+
Console interface for DataJoint Python
10+
11+
:param args: List of arguments to be passed in, defaults to reading stdin
12+
:type args: list, optional
13+
"""
14+
parser = argparse.ArgumentParser(
15+
prog="datajoint",
16+
description="DataJoint console interface.",
17+
conflict_handler="resolve",
18+
)
19+
parser.add_argument(
20+
"-V", "--version", action="version", version=f"{dj.__name__} {dj.__version__}"
21+
)
22+
parser.add_argument(
23+
"-u",
24+
"--user",
25+
type=str,
26+
default=dj.config["database.user"],
27+
required=False,
28+
help="Datajoint username",
29+
)
30+
parser.add_argument(
31+
"-p",
32+
"--password",
33+
type=str,
34+
default=dj.config["database.password"],
35+
required=False,
36+
help="Datajoint password",
37+
)
38+
parser.add_argument(
39+
"-h",
40+
"--host",
41+
type=str,
42+
default=dj.config["database.host"],
43+
required=False,
44+
help="Datajoint host",
45+
)
46+
parser.add_argument(
47+
"-s",
48+
"--schemas",
49+
nargs="+",
50+
type=str,
51+
required=False,
52+
help="A list of virtual module mappings in `db:schema ...` format",
53+
)
54+
kwargs = vars(parser.parse_args(args))
55+
mods = {}
56+
if kwargs["user"]:
57+
dj.config["database.user"] = kwargs["user"]
58+
if kwargs["password"]:
59+
dj.config["database.password"] = kwargs["password"]
60+
if kwargs["host"]:
61+
dj.config["database.host"] = kwargs["host"]
62+
if kwargs["schemas"]:
63+
for vm in kwargs["schemas"]:
64+
d, m = vm.split(":")
65+
mods[m] = dj.create_virtual_module(m, d)
66+
67+
banner = "dj repl\n"
68+
if mods:
69+
modstr = "\n".join(" - {}".format(m) for m in mods)
70+
banner += "\nschema modules:\n\n" + modstr + "\n"
71+
interact(banner, local=dict(ChainMap(mods, locals(), globals())))
72+
73+
raise SystemExit
74+
75+
76+
if __name__ == "__main__":
77+
cli()

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
"automated research workflows",
4040
],
4141
packages=find_packages(exclude=["contrib", "docs", "tests*"]),
42+
entry_points={
43+
"console_scripts": ["dj=datajoint.cli:cli", "datajoint=datajoint.cli:cli"],
44+
},
4245
install_requires=requirements,
4346
python_requires="~={}.{}".format(*min_py_version),
4447
setup_requires=["otumat"], # maybe remove due to conflicts?

tests/test_cli.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
Collection of test cases to test the dj cli
3+
"""
4+
5+
import json
6+
import ast
7+
import subprocess
8+
import pytest
9+
import datajoint as dj
10+
11+
12+
def test_cli_version(capsys):
13+
with pytest.raises(SystemExit) as pytest_wrapped_e:
14+
dj.cli(args=["-V"])
15+
assert pytest_wrapped_e.type == SystemExit
16+
assert pytest_wrapped_e.value.code == 0
17+
18+
captured_output = capsys.readouterr().out
19+
assert captured_output == f"{dj.__name__} {dj.__version__}\n"
20+
21+
22+
def test_cli_help(capsys):
23+
with pytest.raises(SystemExit) as pytest_wrapped_e:
24+
dj.cli(args=["--help"])
25+
assert pytest_wrapped_e.type == SystemExit
26+
assert pytest_wrapped_e.value.code == 0
27+
28+
captured_output = capsys.readouterr().out
29+
assert captured_output.strip()
30+
31+
32+
def test_cli_config():
33+
process = subprocess.Popen(
34+
["dj"],
35+
stdin=subprocess.PIPE,
36+
stdout=subprocess.PIPE,
37+
stderr=subprocess.PIPE,
38+
text=True,
39+
)
40+
41+
process.stdin.write("dj.config\n")
42+
process.stdin.flush()
43+
44+
stdout, stderr = process.communicate()
45+
cleaned = stdout.strip(" >\t\n\r")
46+
for key in ("database.user", "database.password", "database.host"):
47+
assert key in cleaned, f"Key {key} not found in config from stdout: {cleaned}"
48+
49+
50+
def test_cli_args():
51+
process = subprocess.Popen(
52+
["dj", "-utest_user", "-ptest_pass", "-htest_host"],
53+
stdin=subprocess.PIPE,
54+
stdout=subprocess.PIPE,
55+
stderr=subprocess.PIPE,
56+
text=True,
57+
)
58+
59+
process.stdin.write("dj.config['database.user']\n")
60+
process.stdin.write("dj.config['database.password']\n")
61+
process.stdin.write("dj.config['database.host']\n")
62+
process.stdin.flush()
63+
64+
stdout, stderr = process.communicate()
65+
assert "test_user" == stdout[5:14]
66+
assert "test_pass" == stdout[21:30]
67+
assert "test_host" == stdout[37:46]
68+
69+
70+
def test_cli_schemas(prefix, connection_root):
71+
schema = dj.Schema(prefix + "_cli", locals(), connection=connection_root)
72+
73+
@schema
74+
class IJ(dj.Lookup):
75+
definition = """ # tests restrictions
76+
i : int
77+
j : int
78+
"""
79+
contents = list(dict(i=i, j=j + 2) for i in range(3) for j in range(3))
80+
81+
process = subprocess.Popen(
82+
["dj", "-s", "djtest_cli:test_schema"],
83+
stdin=subprocess.PIPE,
84+
stdout=subprocess.PIPE,
85+
stderr=subprocess.PIPE,
86+
text=True,
87+
)
88+
89+
process.stdin.write("test_schema.__dict__['__name__']\n")
90+
process.stdin.write("test_schema.__dict__['schema']\n")
91+
process.stdin.write("test_schema.IJ.fetch(as_dict=True)\n")
92+
process.stdin.flush()
93+
94+
stdout, stderr = process.communicate()
95+
fetch_res = [
96+
{"i": 0, "j": 2},
97+
{"i": 0, "j": 3},
98+
{"i": 0, "j": 4},
99+
{"i": 1, "j": 2},
100+
{"i": 1, "j": 3},
101+
{"i": 1, "j": 4},
102+
{"i": 2, "j": 2},
103+
{"i": 2, "j": 3},
104+
{"i": 2, "j": 4},
105+
]
106+
assert (
107+
"\
108+
dj repl\n\n\
109+
\
110+
schema modules:\n\n\
111+
- test_schema"
112+
== stderr[159:200]
113+
)
114+
assert "'test_schema'" == stdout[4:17]
115+
assert "Schema `djtest_cli`" == stdout[22:41]
116+
assert fetch_res == json.loads(stdout[47:209].replace("'", '"'))

0 commit comments

Comments
 (0)