simplify the app using typer

This commit is contained in:
Matteo Rosati
2026-04-22 22:14:26 +02:00
parent 2dfaa68466
commit b52952a2eb
17 changed files with 334 additions and 505 deletions
+131
View File
@@ -0,0 +1,131 @@
from __future__ import annotations
import unittest
from collections.abc import Sequence
from unittest.mock import patch
from click.testing import Result
from typer.testing import CliRunner
from chromy.cli import app
class CliTests(unittest.TestCase):
def test_list_collections_and_alias(self) -> None:
for command in ("list-collections", "lc"):
with patch(
"chromy.handlers.list_collections.list_collections",
return_value=[],
):
result = _invoke([command])
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "No collections found.\n")
def test_create_collection_and_alias(self) -> None:
for command in ("create-collection", "cc"):
with patch(
"chromy.handlers.create_collection.create_collection",
return_value="notes",
) as create_collection:
result = _invoke([command, "notes"])
create_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "Created collection 'notes'.\n")
def test_delete_collection_and_alias(self) -> None:
for command in ("delete-collection", "dc"):
with patch(
"chromy.handlers.delete_collection.delete_collection",
) as delete_collection:
result = _invoke([command, "notes"])
delete_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "Deleted collection 'notes'.\n")
def test_count_and_alias(self) -> None:
for command in ("count", "co"):
with patch(
"chromy.handlers.count_collection.count_collection",
return_value=7,
) as count_collection:
result = _invoke([command, "notes"])
count_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "7\n")
def test_add_data_and_alias(self) -> None:
for command in ("add-data", "ad"):
with patch(
"chromy.handlers.add_data.ingest_file",
return_value=3,
) as ingest_file:
result = _invoke([command, "notes", "romeo_and_juliet.txt"])
ingest_file.assert_called_once_with("notes", "romeo_and_juliet.txt")
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "Added 3 records to collection 'notes'.\n")
def test_query_and_alias(self) -> None:
query_result = {"ids": [["1"]], "documents": [["hello"]]}
for command in ("query", "q"):
with (
patch(
"chromy.handlers.query.run_query", return_value=query_result
) as run,
patch(
"chromy.handlers.query.format_query_result",
return_value=["Query results:", "1"],
) as format_result,
):
result = _invoke([command, "notes", "Where is Romeo?"])
run.assert_called_once_with("notes", "Where is Romeo?")
format_result.assert_called_once_with(query_result)
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "Query results:\n1\n")
def test_delete_records_and_alias(self) -> None:
for command in ("delete", "del"):
with patch(
"chromy.handlers.delete_collection.delete_data",
return_value=2,
) as delete_data:
result = _invoke(
[command, "notes", "--where", " file_name = play.txt "],
)
delete_data.assert_called_once_with("notes", {"file_name": "play.txt"})
self.assertEqual(result.exit_code, 0)
self.assertEqual(
result.stdout,
"Deleted 2 record(s) from collection 'notes' "
"where file_name=play.txt.\n",
)
def test_invalid_delete_filter_keeps_user_facing_error(self) -> None:
result = _invoke(["delete", "notes", "--where", "file_name"])
self.assertEqual(result.exit_code, 1)
self.assertEqual(
result.stdout,
"Invalid --where value. Expected <condition>=<value>.\n",
)
def test_delete_requires_where_option(self) -> None:
result = _invoke(["delete", "notes"])
self.assertNotEqual(result.exit_code, 0)
self.assertIn("Missing option", result.output)
def _invoke(arguments: Sequence[str]) -> Result:
return CliRunner().invoke(app, list(arguments))
if __name__ == "__main__":
unittest.main()
-98
View File
@@ -1,98 +0,0 @@
from __future__ import annotations
import io
import unittest
from argparse import Namespace
from collections.abc import Sequence
from contextlib import redirect_stdout
from chromy.cli_app import build_command_input, execute_command
from chromy.cli_parser import build_parser
from chromy.command_inputs import (
AddDataInput,
CountCollectionInput,
CreateCollectionInput,
DeleteCollectionInput,
DeleteRecordsInput,
ListCollectionsInput,
QueryInput,
)
class BuildCommandInputTests(unittest.TestCase):
def test_parser_converts_list_collections_and_alias(self) -> None:
self.assertEqual(_parse_input(["list-collections"]), ListCollectionsInput())
self.assertEqual(_parse_input(["lc"]), ListCollectionsInput())
def test_parser_converts_create_collection_and_alias(self) -> None:
expected = CreateCollectionInput(collection="notes")
self.assertEqual(_parse_input(["create-collection", "notes"]), expected)
self.assertEqual(_parse_input(["cc", "notes"]), expected)
def test_parser_converts_delete_collection_and_alias(self) -> None:
expected = DeleteCollectionInput(collection="notes")
self.assertEqual(_parse_input(["delete-collection", "notes"]), expected)
self.assertEqual(_parse_input(["dc", "notes"]), expected)
def test_parser_converts_count_and_alias(self) -> None:
expected = CountCollectionInput(collection="notes")
self.assertEqual(_parse_input(["count", "notes"]), expected)
self.assertEqual(_parse_input(["co", "notes"]), expected)
def test_parser_converts_add_data_and_alias(self) -> None:
expected = AddDataInput(collection="notes", file="romeo_and_juliet.txt")
self.assertEqual(
_parse_input(["add-data", "notes", "romeo_and_juliet.txt"]),
expected,
)
self.assertEqual(
_parse_input(["ad", "notes", "romeo_and_juliet.txt"]),
expected,
)
def test_parser_converts_query_and_alias(self) -> None:
expected = QueryInput(collection="notes", query_text="Where is Romeo?")
self.assertEqual(
_parse_input(["query", "notes", "Where is Romeo?"]),
expected,
)
self.assertEqual(_parse_input(["q", "notes", "Where is Romeo?"]), expected)
def test_parser_converts_delete_records_and_alias(self) -> None:
expected = DeleteRecordsInput(collection="notes", where="file_name=play.txt")
self.assertEqual(
_parse_input(["delete", "notes", "--where", "file_name=play.txt"]),
expected,
)
self.assertEqual(
_parse_input(["del", "notes", "--where", "file_name=play.txt"]),
expected,
)
def test_invalid_delete_filter_keeps_user_facing_error(self) -> None:
args = Namespace(command="delete", collection="notes", where="file_name")
output = io.StringIO()
with redirect_stdout(output):
exit_code = execute_command(args)
self.assertEqual(exit_code, 1)
self.assertEqual(
output.getvalue().strip(),
"Invalid --where value. Expected <condition>=<value>.",
)
self.assertFalse(hasattr(args, "error_message"))
def _parse_input(argv: Sequence[str]) -> object:
return build_command_input(build_parser().parse_args(argv))
if __name__ == "__main__":
unittest.main()
+13 -23
View File
@@ -7,15 +7,6 @@ from contextlib import redirect_stdout
from typing import TypeVar
from unittest.mock import patch
from chromy.command_inputs import (
AddDataInput,
CountCollectionInput,
CreateCollectionInput,
DeleteCollectionInput,
DeleteRecordsInput,
ListCollectionsInput,
QueryInput,
)
from chromy.handlers.add_data import handle_add_data
from chromy.handlers.count_collection import handle_count_collection
from chromy.handlers.create_collection import handle_create_collection
@@ -36,7 +27,6 @@ class HandlerTests(unittest.TestCase):
):
exit_code, output = _capture_output(
handle_list_collections,
ListCollectionsInput(),
)
self.assertEqual(exit_code, 0)
@@ -49,7 +39,6 @@ class HandlerTests(unittest.TestCase):
):
exit_code, output = _capture_output(
handle_list_collections,
ListCollectionsInput(),
)
self.assertEqual(exit_code, 0)
@@ -62,7 +51,7 @@ class HandlerTests(unittest.TestCase):
) as create_collection:
exit_code, output = _capture_output(
handle_create_collection,
CreateCollectionInput(collection="notes"),
"notes",
)
create_collection.assert_called_once_with("notes")
@@ -73,7 +62,7 @@ class HandlerTests(unittest.TestCase):
with patch("chromy.handlers.delete_collection.delete_collection") as delete:
exit_code, output = _capture_output(
handle_delete_collection,
DeleteCollectionInput(collection="notes"),
"notes",
)
delete.assert_called_once_with("notes")
@@ -87,7 +76,7 @@ class HandlerTests(unittest.TestCase):
) as count:
exit_code, output = _capture_output(
handle_count_collection,
CountCollectionInput(collection="notes"),
"notes",
)
count.assert_called_once_with("notes")
@@ -101,7 +90,8 @@ class HandlerTests(unittest.TestCase):
) as ingest_file:
exit_code, output = _capture_output(
handle_add_data,
AddDataInput(collection="notes", file="romeo_and_juliet.txt"),
"notes",
"romeo_and_juliet.txt",
)
ingest_file.assert_called_once_with("notes", "romeo_and_juliet.txt")
@@ -119,7 +109,8 @@ class HandlerTests(unittest.TestCase):
):
exit_code, output = _capture_output(
handle_query,
QueryInput(collection="notes", query_text="hello"),
"notes",
"hello",
)
run.assert_called_once_with("notes", "hello")
@@ -134,7 +125,8 @@ class HandlerTests(unittest.TestCase):
) as delete_data:
exit_code, output = _capture_output(
handle_delete_records,
DeleteRecordsInput(collection="notes", where=" file_name = play.txt "),
"notes",
" file_name = play.txt ",
)
delete_data.assert_called_once_with("notes", {"file_name": "play.txt"})
@@ -149,19 +141,17 @@ class HandlerTests(unittest.TestCase):
ValueError,
"Invalid --where value. Expected <condition>=<value>.",
):
handle_delete_records(
DeleteRecordsInput(collection="notes", where="file_name")
)
handle_delete_records("notes", "file_name")
def _capture_output(
handler: Callable[[CommandT], int],
command: CommandT,
handler: Callable[..., int],
*arguments: CommandT,
) -> tuple[int, str]:
output = io.StringIO()
with redirect_stdout(output):
exit_code = handler(command)
exit_code = handler(*arguments)
return exit_code, output.getvalue()