replace argparse.Namespace plumbing with typed command inputs

This commit is contained in:
Matteo Rosati
2026-04-22 16:03:51 +02:00
parent 8ebab832d5
commit 2962a2e088
15 changed files with 560 additions and 115 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for the Chromy CLI."""
+104
View File
@@ -0,0 +1,104 @@
from __future__ import annotations
import io
import unittest
from argparse import Namespace
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: list[str]) -> object:
return build_command_input(build_parser().parse_args(argv))
if __name__ == "__main__":
unittest.main()
+170
View File
@@ -0,0 +1,170 @@
from __future__ import annotations
import io
import unittest
from collections.abc import Callable
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
from chromy.handlers.delete_collection import (
handle_delete_collection,
handle_delete_records,
)
from chromy.handlers.list_collections import handle_list_collections
from chromy.handlers.query import handle_query
CommandT = TypeVar("CommandT")
class HandlerTests(unittest.TestCase):
def test_list_collections_prints_empty_message(self) -> None:
with patch("chromy.handlers.list_collections.list_collections", return_value=[]):
exit_code, output = _capture_output(
handle_list_collections,
ListCollectionsInput(),
)
self.assertEqual(exit_code, 0)
self.assertEqual(output, "No collections found.\n")
def test_list_collections_prints_collection_names(self) -> None:
with patch(
"chromy.handlers.list_collections.list_collections",
return_value=["notes", "plays"],
):
exit_code, output = _capture_output(
handle_list_collections,
ListCollectionsInput(),
)
self.assertEqual(exit_code, 0)
self.assertEqual(output, "notes\nplays\n")
def test_create_collection_uses_typed_input(self) -> None:
with patch(
"chromy.handlers.create_collection.create_collection",
return_value="notes",
) as create_collection:
exit_code, output = _capture_output(
handle_create_collection,
CreateCollectionInput(collection="notes"),
)
create_collection.assert_called_once_with("notes")
self.assertEqual(exit_code, 0)
self.assertEqual(output, "Created collection 'notes'.\n")
def test_delete_collection_uses_typed_input(self) -> None:
with patch("chromy.handlers.delete_collection.delete_collection") as delete:
exit_code, output = _capture_output(
handle_delete_collection,
DeleteCollectionInput(collection="notes"),
)
delete.assert_called_once_with("notes")
self.assertEqual(exit_code, 0)
self.assertEqual(output, "Deleted collection 'notes'.\n")
def test_count_collection_uses_typed_input(self) -> None:
with patch(
"chromy.handlers.count_collection.count_collection",
return_value=7,
) as count:
exit_code, output = _capture_output(
handle_count_collection,
CountCollectionInput(collection="notes"),
)
count.assert_called_once_with("notes")
self.assertEqual(exit_code, 0)
self.assertEqual(output, "7\n")
def test_add_data_uses_typed_input(self) -> None:
with patch(
"chromy.handlers.add_data.ingest_file",
return_value=3,
) as ingest_file:
exit_code, output = _capture_output(
handle_add_data,
AddDataInput(collection="notes", file="romeo_and_juliet.txt"),
)
ingest_file.assert_called_once_with("notes", "romeo_and_juliet.txt")
self.assertEqual(exit_code, 0)
self.assertEqual(output, "Added 3 records to collection 'notes'.\n")
def test_query_uses_typed_input(self) -> None:
query_result = {"ids": [["1"]], "documents": [["hello"]]}
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,
):
exit_code, output = _capture_output(
handle_query,
QueryInput(collection="notes", query_text="hello"),
)
run.assert_called_once_with("notes", "hello")
format_result.assert_called_once_with(query_result)
self.assertEqual(exit_code, 0)
self.assertEqual(output, "Query results:\n1\n")
def test_delete_records_parses_where_filter(self) -> None:
with patch(
"chromy.handlers.delete_collection.delete_data",
return_value=2,
) as delete_data:
exit_code, output = _capture_output(
handle_delete_records,
DeleteRecordsInput(collection="notes",
where=" file_name = play.txt "),
)
delete_data.assert_called_once_with("notes", {"file_name": "play.txt"})
self.assertEqual(exit_code, 0)
self.assertEqual(
output,
"Deleted 2 record(s) from collection 'notes' where file_name=play.txt.\n",
)
def test_delete_records_rejects_invalid_where_filter(self) -> None:
with self.assertRaisesRegex(
ValueError,
"Invalid --where value. Expected <condition>=<value>.",
):
handle_delete_records(
DeleteRecordsInput(collection="notes", where="file_name")
)
def _capture_output(
handler: Callable[[CommandT], int],
command: CommandT,
) -> tuple[int, str]:
output = io.StringIO()
with redirect_stdout(output):
exit_code = handler(command)
return exit_code, output.getvalue()
if __name__ == "__main__":
unittest.main()