Skip to content

Commit 973420b

Browse files
authored
Unify CLI with sub-commands (#55)
* set up unified cli * update platform handling * update tests * update docs
1 parent 57a2e49 commit 973420b

File tree

5 files changed

+102
-70
lines changed

5 files changed

+102
-70
lines changed

docs/source/cli.rst

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,12 @@ To translate a search query on the command line, run
1010

1111
.. code-block:: bash
1212
13-
search-query-translate --from wos \
14-
--input input_query.txt \
13+
search-query translate --input input_query.json \
1514
--to pubmed \
16-
--output output_query.txt
15+
--output output_query.json
1716
1817
**Arguments**
1918

20-
- ``--from`` (required):
21-
The source query format.
22-
Example: ``wos``
23-
2419
- ``--input`` (required):
2520
Path to the input file containing the original query.
2621

@@ -31,22 +26,10 @@ To translate a search query on the command line, run
3126
- ``--output`` (required):
3227
Path to the file where the converted query will be written.
3328

34-
35-
**Example**
36-
37-
Suppose you have a Web of Science search query saved in ``input_query.txt`` and you want to convert it to a PubMed-compatible format. Run:
38-
39-
.. code-block:: bash
40-
41-
search-query-translate --from wos \
42-
--input input_query.txt \
43-
--to pubmed \
44-
--output output_query.txt
45-
46-
The converted query will be saved in ``output_query.txt``.
29+
The format of the ``input_query.json`` is stored in the ``platform`` field of the JSON file.
4730

4831
Linters can be run on the CLI:
4932

5033
.. code-block:: bash
5134
52-
search-query-lint search-file.json
35+
search-query lint search-file.json

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ dev = [
4747
]
4848

4949
[project.scripts]
50-
search-query-translate = "search_query.cli:translate"
51-
search-query-lint = "search_query.cli:lint"
50+
search-query = "search_query.cli:main"
5251

5352
[tool.pylint.MAIN]
5453
extension-pkg-whitelist = "lxml.etree"

search_query/cli.py

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,125 @@
11
#!/usr/bin/env python3
22
"""CLI for search-query."""
3+
from __future__ import annotations
4+
35
import argparse
46
import sys
57
from pathlib import Path
68

79
import search_query.linter
810
import search_query.parser
911
from search_query import load_search_file
12+
from search_query.exception import QuerySyntaxError
13+
14+
def _cmd_translate(args: argparse.Namespace) -> int:
15+
"""Translate a query file from one platform/format to another."""
16+
17+
print(f"Reading query from {args.input_file}")
18+
input_path = Path(args.input_file)
19+
if input_path.suffix != ".json":
20+
print("Only .json search files are supported at the moment.", file=sys.stderr)
21+
return 2
22+
23+
search_file = load_search_file(args.input_file)
24+
try:
25+
query = search_query.parser.parse(
26+
search_file.search_string,
27+
platform=search_file.platform,
28+
field_general=search_file.field,
29+
)
30+
except QuerySyntaxError as e:
31+
print(f"Fatal error parsing query.")
32+
return 1
33+
34+
print(f"Converting from {search_file.platform} to {args.target}")
35+
try:
36+
translated_query = query.translate(args.target)
37+
except Exception as e:
38+
print(f"Error translating query: {e}")
39+
return 1
40+
41+
converted_query = translated_query.to_string()
42+
search_file.search_string = converted_query
43+
search_file.platform = args.target
44+
45+
print(f"Writing converted query to {args.output_file}")
46+
search_file.save(args.output_file)
47+
return 0
48+
49+
50+
def _lint(args: argparse.Namespace) -> int:
51+
"""Lint files."""
52+
exit_code = 0
53+
for file_path in args.files:
54+
search_file = load_search_file(file_path)
55+
try:
56+
result = search_query.linter.lint_file(search_file)
57+
if result:
58+
exit_code = 1
59+
except QuerySyntaxError as e:
60+
print(f"Error linting file {file_path}: {e}")
61+
exit_code = 1
1062

63+
return exit_code
1164

12-
def translate() -> None:
13-
"""Main entrypoint for the query translation CLI"""
1465

66+
def build_parser() -> argparse.ArgumentParser:
1567
parser = argparse.ArgumentParser(
16-
description="Convert search queries between formats"
68+
prog="search-query",
69+
description="Tools for working with search queries (linting, translation, etc.)",
1770
)
18-
parser.add_argument(
19-
"--from",
20-
dest="source",
21-
required=True,
22-
help="Source query format (e.g., colrev_web_of_science)",
71+
subparsers = parser.add_subparsers(dest="command", metavar="<command>", required=True)
72+
73+
# translate
74+
p_tr = subparsers.add_parser(
75+
"translate",
76+
help="Convert search queries between formats",
77+
description="Convert search queries between formats.",
2378
)
24-
parser.add_argument(
79+
p_tr.add_argument(
2580
"--input",
2681
dest="input_file",
2782
required=True,
28-
help="Input file containing the query",
83+
help="Input .json file containing the query",
2984
)
30-
parser.add_argument(
85+
p_tr.add_argument(
3186
"--to",
3287
dest="target",
3388
required=True,
3489
help="Target query format (e.g., colrev_pubmed)",
3590
)
36-
parser.add_argument(
91+
p_tr.add_argument(
3792
"--output",
3893
dest="output_file",
3994
required=True,
40-
help="Output file for the converted query",
95+
help="Output file path for the converted query",
4196
)
97+
p_tr.set_defaults(func=_cmd_translate)
4298

43-
args = parser.parse_args()
44-
45-
# Placeholder: Print what would happen
46-
print(f"Converting from {args.source} to {args.target}")
47-
print(f"Reading query from {args.input_file}")
48-
print(f"Writing converted query to {args.output_file}")
49-
print(f"Convert from {args.source} to {args.target}")
50-
51-
if Path(args.input_file).suffix == ".json":
52-
search_file = load_search_file(args.input_file)
53-
query = search_query.parser.parse(
54-
search_file.search_string,
55-
platform=args.source,
56-
field_general=search_file.field,
57-
)
58-
59-
translated_query = query.translate(args.target)
60-
converted_query = translated_query.to_string()
61-
search_file.search_string = converted_query
62-
search_file.save(args.output_file)
99+
# lint
100+
p_li = subparsers.add_parser(
101+
"lint",
102+
help="Lint query files",
103+
description="Lint one or more query files. Intended for standalone use or pre-commit.",
104+
)
105+
p_li.add_argument(
106+
"files",
107+
nargs="+",
108+
help="File(s) to lint",
109+
)
110+
p_li.set_defaults(func=_lint)
63111

64-
else:
65-
raise NotImplementedError
112+
return parser
66113

67114

68-
def lint() -> None:
69-
"""Main entrypoint for the query linter hook"""
115+
def main(argv: list[str] | None = None) -> int:
116+
parser = build_parser()
117+
args = parser.parse_args(argv)
118+
try:
119+
return args.func(args)
120+
except KeyboardInterrupt:
121+
return 130
70122

71-
file_path = sys.argv[1]
72123

73-
raise SystemExit(search_query.linter.pre_commit_hook(file_path))
124+
if __name__ == "__main__":
125+
raise SystemExit(main())

search_query/linter.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,19 @@ def _get_parser(
2020
) -> QueryStringParser:
2121
"""Run the linter on the search string"""
2222

23+
platform = search_query.parser.get_platform(platform)
2324
parser_class = search_query.parser.PARSERS[platform]
2425
parser = parser_class(search_string, field_general=field_general) # type: ignore
2526

2627
try:
2728
parser.parse()
2829
except Exception as exc:
29-
print(f"Error parsing query: {exc}")
3030
assert parser.linter.messages # type: ignore
3131
return parser
3232

3333

3434
def lint_file(search_file: SearchFile) -> dict:
3535
"""Lint a search file and return the messages."""
36-
# pylint: disable=too-many-locals
3736
platform = search_query.parser.get_platform(search_file.platform)
3837
if platform not in search_query.parser.PARSERS:
3938
raise ValueError(
@@ -43,7 +42,7 @@ def lint_file(search_file: SearchFile) -> dict:
4342

4443
return lint_query_string(
4544
search_file.search_string,
46-
platform=search_file.platform,
45+
platform=platform,
4746
field_general=search_file.field,
4847
)
4948

test/test_cli.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ def test_translate_cli() -> None:
1616

1717
result = subprocess.run(
1818
[
19-
"search-query-translate",
20-
"--from=wos",
19+
"search-query",
20+
"translate",
2121
f"--input={input_file}",
2222
"--to=ebscohost",
2323
f"--output={output_file}",
@@ -28,7 +28,7 @@ def test_translate_cli() -> None:
2828
print(result.stdout)
2929
print(result.stderr)
3030
assert result.returncode == 0
31-
assert "Converting from wos to ebscohost" in result.stdout
31+
assert "Converting from Web of Science to ebscohost" in result.stdout
3232
assert "Writing converted query to" in result.stdout
3333
assert output_file.exists()
3434

@@ -44,7 +44,7 @@ def test_linter_cli() -> None:
4444
input_file = test_data_dir / "search_history_file_2_linter.json"
4545

4646
result = subprocess.run(
47-
["search-query-lint", f"{input_file}"],
47+
["search-query", "lint", f"{input_file}"],
4848
capture_output=True,
4949
text=True,
5050
)
@@ -53,7 +53,6 @@ def test_linter_cli() -> None:
5353
print("STDERR:", result.stderr)
5454
print("RETURN CODE:", result.returncode)
5555

56-
assert "Lint: search_history_file_2_linter.json (wos)" in result.stdout
5756
assert "Unbalanced closing parenthesis" in result.stdout
5857
assert (
5958
"The query uses multiple operators with different precedence levels"

0 commit comments

Comments
 (0)