diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66847c7..66e7b8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,7 @@ repos: - pytest - requests - tomli; python_version<"3.11" + - setuptools - repo: https://github.com/gitleaks/gitleaks rev: v8.21.2 diff --git a/command_line_assistant/__main__.py b/command_line_assistant/__main__.py index 93388b4..89fc78f 100644 --- a/command_line_assistant/__main__.py +++ b/command_line_assistant/__main__.py @@ -1,46 +1,39 @@ -import logging -import os import sys from pathlib import Path -from command_line_assistant.cli import get_args +from command_line_assistant.commands import history, query, record from command_line_assistant.config import ( + CONFIG_DEFAULT_PATH, load_config_file, ) -from command_line_assistant.handlers import ( - handle_history_write, - handle_query, - handle_script_session, -) from command_line_assistant.logger import setup_logging +from command_line_assistant.utils.cli import add_default_command, create_argument_parser def main() -> int: - parser, args = get_args() - - config_file = Path(args.config).expanduser() + config_file = Path(CONFIG_DEFAULT_PATH) config = load_config_file(config_file) - setup_logging(config, args.verbose) + setup_logging(config, False) + + parser, commands_parser = create_argument_parser() - enforce_script_session = config.output.enforce_script - output_file = config.output.file + # TODO: add autodetection of BaseCLICommand classes in the future so we can just drop + # new subcommand python modules into the directory and then loop and call `register_subcommand()` + # on each one. + query.register_subcommand(commands_parser, config) # type: ignore + history.register_subcommand(commands_parser, config) # type: ignore + record.register_subcommand(commands_parser, config) # type: ignore - if enforce_script_session and (not args.record or not os.path.exists(output_file)): - parser.error( - f"Please call `{sys.argv[0]} --record` first to initialize script session or create the output file." - ) + args = add_default_command(sys.argv) + args = parser.parse_args(args) - # NOTE: This needs more refinement, script session can't be combined with other arguments - if args.record: - handle_script_session(output_file) - return 0 - if args.history_clear: - logging.info("Clearing history of conversation") - handle_history_write(config, [], "") - if args.query_string: - handle_query(args.query_string, config) + if not hasattr(args, "func"): + parser.print_help() + return 1 + service = args.func(args) + service.run() return 0 diff --git a/command_line_assistant/cli.py b/command_line_assistant/cli.py deleted file mode 100644 index 2a21883..0000000 --- a/command_line_assistant/cli.py +++ /dev/null @@ -1,44 +0,0 @@ -import argparse - -from command_line_assistant.config import CONFIG_DEFAULT_PATH -from command_line_assistant.utils import read_stdin - - -def get_args(): - """Handle CLI options.""" - parser = argparse.ArgumentParser( - description="A script with multiple optional arguments and a required positional argument if no optional arguments are provided." - ) - - parser.add_argument( - "--history-clear", action="store_true", help="Clear the history." - ) - parser.add_argument( - "--record", - action="store_true", - help="Initialize a script session (all other arguments will be ignored).", - ) - parser.add_argument( - "--config", - default=CONFIG_DEFAULT_PATH, - help="Path to the config file.", - ) - parser.add_argument( - "--verbose", action="store_true", help="Enable verbose logging in terminal." - ) - - # Positional argument, required only if no optional arguments are provided - parser.add_argument("query_string", nargs="?", help="Query string to be processed.") - - args = parser.parse_args() - optional_args = [ - args.history_clear, - args.record, - ] - input_data = read_stdin() - if input_data and args.query_string: - args.query_string = f"{args.query_string} {input_data.strip()}" - - if not any(optional_args) and not args.query_string: - parser.error("Query string is required if no optional arguments are provided.") - return parser, args diff --git a/command_line_assistant/commands/__init__.py b/command_line_assistant/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/command_line_assistant/commands/history.py b/command_line_assistant/commands/history.py new file mode 100644 index 0000000..e00000f --- /dev/null +++ b/command_line_assistant/commands/history.py @@ -0,0 +1,43 @@ +import logging +from argparse import Namespace + +from command_line_assistant.config import Config +from command_line_assistant.history import handle_history_write +from command_line_assistant.utils.cli import BaseCLICommand, SubParsersAction + +logger = logging.getLogger(__name__) + + +class HistoryCommand(BaseCLICommand): + def __init__(self, clear: bool, config: Config) -> None: + self._clear = clear + self._config = config + super().__init__() + + def run(self) -> None: + if self._clear: + logger.info("Clearing history of conversation") + handle_history_write(self._config, [], "") + + +def register_subcommand(parser: SubParsersAction, config: Config): + """ + Register this command to argparse so it's available for the datasets-cli + + Args: + parser: Root parser to register command-specific arguments + """ + history_parser = parser.add_parser( + "history", + help="Manage conversation history", + ) + history_parser.add_argument( + "--clear", action="store_true", help="Clear the history." + ) + + # TODO(r0x0d): This is temporary as it will get removed + history_parser.set_defaults(func=lambda args: _command_factory(args, config)) + + +def _command_factory(args: Namespace, config: Config) -> HistoryCommand: + return HistoryCommand(args.clear, config) diff --git a/command_line_assistant/commands/query.py b/command_line_assistant/commands/query.py new file mode 100644 index 0000000..4abb147 --- /dev/null +++ b/command_line_assistant/commands/query.py @@ -0,0 +1,39 @@ +from argparse import Namespace + +from command_line_assistant.config import Config +from command_line_assistant.handlers import handle_query +from command_line_assistant.utils.cli import BaseCLICommand, SubParsersAction + + +class QueryCommand(BaseCLICommand): + def __init__(self, query_string: str, config: Config) -> None: + self._query = query_string + self._config = config + super().__init__() + + def run(self) -> None: + handle_query(self._query, self._config) + + +def register_subcommand(parser: SubParsersAction, config: Config) -> None: + """ + Register this command to argparse so it's available for the datasets-cli + + Args: + parser: Root parser to register command-specific arguments + """ + query_parser = parser.add_parser( + "query", + help="", + ) + # Positional argument, required only if no optional arguments are provided + query_parser.add_argument( + "query_string", nargs="?", help="Query string to be processed." + ) + + # TODO(r0x0d): This is temporary as it will get removed + query_parser.set_defaults(func=lambda args: _command_factory(args, config)) + + +def _command_factory(args: Namespace, config: Config) -> QueryCommand: + return QueryCommand(args.query_string, config) diff --git a/command_line_assistant/commands/record.py b/command_line_assistant/commands/record.py new file mode 100644 index 0000000..7d1b141 --- /dev/null +++ b/command_line_assistant/commands/record.py @@ -0,0 +1,49 @@ +import logging +import os +import sys + +from command_line_assistant.config import Config +from command_line_assistant.handlers import handle_script_session +from command_line_assistant.utils.cli import BaseCLICommand, SubParsersAction + +logger = logging.getLogger(__name__) + +# NOTE: This needs more refinement, script session can't be combined with other arguments + + +class RecordCommand(BaseCLICommand): + def __init__(self, config: Config) -> None: + self._config = config + super().__init__() + + def run(self) -> None: + enforce_script_session = self._config.output.enforce_script + output_file = self._config.output.file + + if enforce_script_session and not os.path.exists(output_file): + logger.error( + "Please call `%s record` first to initialize script session or create the output file.", + sys.argv[0], + ) + + handle_script_session(output_file) + + +def register_subcommand(parser: SubParsersAction, config: Config): + """ + Register this command to argparse so it's available for the datasets-cli + + Args: + parser: Root parser to register command-specific arguments + """ + record_parser = parser.add_parser( + "record", + help="Start a recording session for script output.", + ) + + # TODO(r0x0d): This is temporary as it will get removed + record_parser.set_defaults(func=lambda args: _command_factory(config)) + + +def _command_factory(config: Config) -> RecordCommand: + return RecordCommand(config) diff --git a/command_line_assistant/config.py b/command_line_assistant/config.py index c8acd72..b1b9b6a 100644 --- a/command_line_assistant/config.py +++ b/command_line_assistant/config.py @@ -13,8 +13,9 @@ except ImportError: import tomli as tomllib # pyright: ignore[reportMissingImports] - -CONFIG_DEFAULT_PATH: Path = Path("~/.config/command-line-assistant/config.toml") +CONFIG_DEFAULT_PATH: Path = Path( + "~/.config/command-line-assistant/config.toml" +).expanduser() # tomllib does not support writting files, so we will create our own. CONFIG_TEMPLATE = """\ diff --git a/command_line_assistant/constants.py b/command_line_assistant/constants.py new file mode 100644 index 0000000..1cf6267 --- /dev/null +++ b/command_line_assistant/constants.py @@ -0,0 +1 @@ +VERSION = "0.1.0" diff --git a/command_line_assistant/utils.py b/command_line_assistant/utils/__init__.py similarity index 100% rename from command_line_assistant/utils.py rename to command_line_assistant/utils/__init__.py diff --git a/command_line_assistant/utils/cli.py b/command_line_assistant/utils/cli.py new file mode 100644 index 0000000..1d54637 --- /dev/null +++ b/command_line_assistant/utils/cli.py @@ -0,0 +1,99 @@ +import select +import sys +from abc import ABC, abstractmethod +from argparse import SUPPRESS, ArgumentParser, _SubParsersAction +from typing import Optional + +from command_line_assistant.config import CONFIG_DEFAULT_PATH +from command_line_assistant.constants import VERSION + +# Define the type here so pyright is happy with it. +SubParsersAction = _SubParsersAction + +PARENT_ARGS: list[str] = ["--version", "-v", "-h", "--help"] +ARGS_WITH_VALUES: list[str] = ["--clear"] + + +class BaseCLICommand(ABC): + @abstractmethod + def run(self): + raise NotImplementedError("Not implemented in base class.") + + +def add_default_command(argv): + """Add the default command when none is given""" + args = argv[1:] + + # Early exit if we don't have any argv + if not args: + return args + + subcommand = _subcommand_used(argv) + if subcommand is None: + args.insert(0, "query") + + return args + + +def _subcommand_used(args): + """Return what subcommand has been used by the user. Return None if no subcommand has been used.""" + for index, argument in enumerate(args): + # If we have a exact match for any of the commands, return directly + if argument in ("query", "history"): + return argument + + # It means that we hit a --version/--help + if argument in PARENT_ARGS: + return argument + + # Otherwise, check if this is the second part of an arg that takes a value. + elif args[index - 1] in ARGS_WITH_VALUES: + continue + + return None + + +def create_argument_parser() -> tuple[ArgumentParser, SubParsersAction]: + """Create the argument parser for command line assistant.""" + parser = ArgumentParser( + description="A script with multiple optional arguments and a required positional argument if no optional arguments are provided.", + ) + parser.add_argument( + "--version", + action="version", + version=VERSION, + default=SUPPRESS, + help="Show command line assistant version and exit.", + ) + parser.add_argument( + "--config", + default=CONFIG_DEFAULT_PATH, + help="Path to the config file.", + ) + commands_parser = parser.add_subparsers( + dest="command", help="command line assistant helpers" + ) + + return parser, commands_parser + + +def read_stdin() -> Optional[str]: + """Parse the std input when a user give us. + + For example, consider the following scenario: + >>> echo "how to run podman?" | c + + Or a more complex one + >>> cat error-log | c "How to fix this?" + + Returns: + In case we have a stdin, we parse and retrieve it. Otherwise, just + return None. + """ + # Check if there's input available on stdin + if select.select([sys.stdin], [], [], 0.0)[0]: + # If there is input, read it + input_data = sys.stdin.read().strip() + return input_data + # If no input, return None or handle as you prefer + return None diff --git a/packaging/command-line-assistant.spec b/packaging/command-line-assistant.spec index 85af10c..1995ab5 100644 --- a/packaging/command-line-assistant.spec +++ b/packaging/command-line-assistant.spec @@ -1,3 +1,6 @@ +%global python_package_src command_line_assistant +%global binary_name c + Name: command-line-assistant Version: 0.1.0 Release: 1%{?dist} @@ -11,20 +14,17 @@ BuildArch: noarch BuildRequires: python3-devel BuildRequires: python3-setuptools -# Not needed after RHEL 10 as it is native in Python 3.11+ -%if 0%{?rhel} && 0%{?rhel} < 10 -BuildRequires: python3-tomli -%endif +BuildRequires: python3-wheel +BuildRequires: python3-pip Requires: python3-requests + # Not needed after RHEL 10 as it is native in Python 3.11+ %if 0%{?rhel} && 0%{?rhel} < 10 -Requires: python3-tomli +BuildRequires: python3-tomli +Requires: python3-tomli %endif -%global python_package_src command_line_assistant -%global binary_name c - %description A simple wrapper to interact with RAG @@ -32,16 +32,17 @@ A simple wrapper to interact with RAG %autosetup -n %{name}-%{version} %build -%py3_build +%py3_build_wheel %install -%py3_install +%py3_install_wheel %{python_package_src}-%{version}-py3-none-any.whl %files %doc README.md %license LICENSE %{python3_sitelib}/%{python_package_src}/ -%{python3_sitelib}/%{python_package_src}-*.egg-info/ +%{python3_sitelib}/%{python_package_src}-%{version}.dist-info/ + # Our binary is just called "c" %{_bindir}/%{binary_name} diff --git a/pyproject.toml b/pyproject.toml index 5cbe0d7..6a5b72d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,27 +18,31 @@ requires-python = ">=3.9" # RHEL 9 and 10 readme = "README.md" license = { file = "LICENSE" } classifiers = ["Programming Language :: Python :: 3"] - -[project.urls] -Repository = "https://github.com/rhel-lightspeed/command-line-assistant.git" -Issues = "https://github.com/rhel-lightspeed/command-line-assistant/issues" - -[project.scripts] -c = "command_line_assistant.__main__:main" +urls = { Repository = "https://github.com/rhel-lightspeed/command-line-assistant.git", Issues = "https://github.com/rhel-lightspeed/command-line-assistant/issues" } +scripts = { c = "command_line_assistant.__main__:main" } [build-system] # pdm build is not available in rhel baseos repositories requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" - [dependency-groups] -dev = ["pytest==8.3.4", "pytest-cov==6.0.0", "pytest-randomly==3.16.0", "coverage==7.6.8", "pytest-sugar==1.0.0", "pytest-clarity==1.0.1", "tox>=4.23.2", "tox-pdm>=0.7.2"] +dev = [ + "pytest==8.3.4", + "pytest-cov==6.0.0", + "pytest-randomly==3.16.0", + "coverage==7.6.8", + "pytest-sugar==1.0.0", + "pytest-clarity==1.0.1", + "tox>=4.23.2", + "tox-pdm>=0.7.2", +] # ----- Tooling specifics -[tool.setuptools] -packages = ["command_line_assistant"] +[tool.setuptools.packages.find] +include = ["command_line_assistant*"] +namespaces = false [tool.ruff] # Enable ruff rules to act like flake8 diff --git a/setup.py b/setup.py index 096e9e7..3aa09cc 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ import sys -from setuptools import setup +from setuptools import find_packages, setup if sys.version_info >= (3, 11): import tomllib @@ -19,16 +19,24 @@ for script_name, script_path in pyproject_settings["project"]["scripts"].items(): entry_points["console_scripts"].append(f"{script_name} = {script_path}") +description = None +with open( + pyproject_settings["project"]["readme"], mode="r", encoding="utf-8" +) as handler: + description = handler.read() + setup( name=pyproject_settings["project"]["name"], version=pyproject_settings["project"]["version"], author=pyproject_settings["project"]["authors"][0]["name"], author_email=pyproject_settings["project"]["authors"][0]["email"], description=pyproject_settings["project"]["description"], - long_description=open(pyproject_settings["project"]["readme"]).read(), + long_description=description, long_description_content_type="text/markdown", url=pyproject_settings["project"]["urls"]["Repository"], - packages=pyproject_settings["tool"]["setuptools"]["packages"], + packages=find_packages( + include=pyproject_settings["tool"]["setuptools"]["packages"]["find"]["include"] + ), install_requires=pyproject_settings["project"]["dependencies"], entry_points=entry_points, classifiers=pyproject_settings["project"]["classifiers"], diff --git a/tests/commands/test_history.py b/tests/commands/test_history.py new file mode 100644 index 0000000..cdf3e12 --- /dev/null +++ b/tests/commands/test_history.py @@ -0,0 +1,70 @@ +from argparse import ArgumentParser, Namespace +from unittest.mock import patch + +import pytest + +from command_line_assistant.commands.history import ( + HistoryCommand, + _command_factory, + register_subcommand, +) + + +@pytest.fixture +def history_command(mock_config): + """Fixture to create a HistoryCommand instance""" + return HistoryCommand(clear=True, config=mock_config) + + +@pytest.fixture +def history_command_no_clear(mock_config): + """Fixture to create a HistoryCommand instance""" + return HistoryCommand(clear=False, config=mock_config) + + +class TestHistoryCommand: + def test_init(self, history_command, mock_config): + """Test HistoryCommand initialization""" + assert history_command._clear is True + assert history_command._config == mock_config + + @patch("command_line_assistant.commands.history.handle_history_write") + def test_run_with_clear(self, mock_history_write, history_command): + """Test run() method when clear=True""" + history_command.run() + mock_history_write.assert_called_once_with(history_command._config, [], "") + + @patch("command_line_assistant.commands.history.handle_history_write") + def test_run_without_clear(self, mock_history_write, history_command_no_clear): + """Test run() method when clear=False""" + history_command_no_clear.run() + mock_history_write.assert_not_called() + + @patch("command_line_assistant.commands.history.handle_history_write") + def test_run_with_clear_and_disabled_history(self, mock_history_write, mock_config): + """Test run() method when history is disabled""" + mock_config.history.enabled = False + command = HistoryCommand(clear=True, config=mock_config) + command.run() + mock_history_write.assert_called_once_with(command._config, [], "") + + +def test_register_subcommand(mock_config): + """Test register_subcommand function""" + parser = ArgumentParser() + sub_parser = parser.add_subparsers() + + register_subcommand(sub_parser, mock_config) + + parser.parse_args(["history", "--clear"]) + + +def test_command_factory(mock_config): + """Test _command_factory function""" + + args = Namespace(clear=True) + command = _command_factory(args, mock_config) + + assert isinstance(command, HistoryCommand) + assert command._clear is True + assert command._config == mock_config diff --git a/tests/commands/test_query.py b/tests/commands/test_query.py new file mode 100644 index 0000000..e611216 --- /dev/null +++ b/tests/commands/test_query.py @@ -0,0 +1,97 @@ +from unittest.mock import Mock, patch + +import pytest + +from command_line_assistant.commands.query import QueryCommand, register_subcommand +from command_line_assistant.config import ( + Config, +) + + +@pytest.fixture +def query_command(mock_config): + """Fixture to create a QueryCommand instance""" + return QueryCommand("test query", mock_config) + + +def test_query_command_initialization(query_command): + """Test QueryCommand initialization""" + assert query_command._query == "test query" + assert isinstance(query_command._config, Config) + + +@patch("command_line_assistant.commands.query.handle_query") +def test_query_command_run(mock_handle_query, query_command): + """Test QueryCommand run method""" + query_command.run() + mock_handle_query.assert_called_once_with("test query", query_command._config) + + +@patch("command_line_assistant.commands.query.handle_query") +def test_query_command_with_empty_query(mock_handle_query, mock_config): + """Test QueryCommand with empty query""" + command = QueryCommand("", mock_config) + command.run() + mock_handle_query.assert_called_once_with("", mock_config) + + +def test_register_subcommand(): + """Test subcommand registration""" + mock_parser = Mock() + mock_subparser = Mock() + mock_parser.add_parser.return_value = mock_subparser + mock_config = Mock() + + register_subcommand(mock_parser, mock_config) + + # Verify parser configuration + mock_parser.add_parser.assert_called_once_with("query", help="") + mock_subparser.add_argument.assert_called_once_with( + "query_string", nargs="?", help="Query string to be processed." + ) + assert mock_subparser.set_defaults.called + + +@pytest.mark.parametrize( + "query_string,expected", + [ + ("normal query", "normal query"), + ("", ""), + ("complex query with spaces", "complex query with spaces"), + ("query?with!special@chars", "query?with!special@chars"), + ], +) +def test_query_command_different_inputs(mock_config, query_string, expected): + """Test QueryCommand with different input strings""" + command = QueryCommand(query_string, mock_config) + assert command._query == expected + + +@patch("command_line_assistant.commands.query.handle_query") +def test_query_command_error_handling(mock_handle_query, query_command): + """Test QueryCommand error handling""" + mock_handle_query.side_effect = Exception("Test error") + + with pytest.raises(Exception) as exc_info: + query_command.run() + + assert str(exc_info.value) == "Test error" + mock_handle_query.assert_called_once() + + +def test_query_command_config_validation(mock_config): + """Test QueryCommand with invalid config""" + # Modify config to be invalid + mock_config.backend.endpoint = "" + + command = QueryCommand("test query", mock_config) + assert command._config.backend.endpoint == "" + + +@patch("command_line_assistant.commands.query.handle_query") +def test_query_command_with_special_characters(mock_handle_query, mock_config): + """Test QueryCommand with special characters in query""" + special_query = "test\nquery\twith\rspecial\characters" + command = QueryCommand(special_query, mock_config) + command.run() + mock_handle_query.assert_called_once_with(special_query, mock_config) diff --git a/tests/commands/test_record.py b/tests/commands/test_record.py new file mode 100644 index 0000000..7db6cc6 --- /dev/null +++ b/tests/commands/test_record.py @@ -0,0 +1,75 @@ +from unittest.mock import Mock, patch + +import pytest + +from command_line_assistant.commands.record import RecordCommand + + +@pytest.fixture +def record_command(mock_config): + """Fixture for RecordCommand instance""" + return RecordCommand(mock_config) + + +class TestRecordCommand: + def test_init(self, record_command, mock_config): + """Test RecordCommand initialization""" + assert record_command._config == mock_config + + @patch("command_line_assistant.commands.record.handle_script_session") + @patch("os.path.exists") + def test_run_with_existing_file( + self, mock_exists, mock_script_session, record_command + ): + """Test run() when output file exists""" + mock_exists.return_value = True + record_command.run() + mock_script_session.assert_called_once_with(record_command._config.output.file) + + @patch("command_line_assistant.commands.record.handle_script_session") + @patch("os.path.exists") + @patch("sys.exit") + def test_run_without_file_enforced( + self, mock_exit, mock_exists, mock_script_session, record_command + ): + """Test run() when output file doesn't exist and script is enforced""" + mock_exists.return_value = False + record_command.run() + mock_script_session.assert_called_once_with(record_command._config.output.file) + + @patch("command_line_assistant.commands.record.handle_script_session") + @patch("os.path.exists") + def test_run_without_enforcement( + self, mock_exists, mock_script_session, mock_config + ): + """Test run() when script enforcement is disabled""" + mock_config.output.enforce_script = False + command = RecordCommand(mock_config) + mock_exists.return_value = False + + command.run() + mock_script_session.assert_called_once_with(command._config.output.file) + + +def test_register_subcommand(mock_config): + """Test register_subcommand function""" + mock_parser = Mock() + mock_parser.add_parser.return_value = mock_parser + + from command_line_assistant.commands.record import register_subcommand + + register_subcommand(mock_parser, mock_config) + + mock_parser.add_parser.assert_called_once_with( + "record", help="Start a recording session for script output." + ) + assert mock_parser.set_defaults.called + + +def test_command_factory(mock_config): + """Test _command_factory function""" + from command_line_assistant.commands.record import _command_factory + + command = _command_factory(mock_config) + assert isinstance(command, RecordCommand) + assert command._config == mock_config diff --git a/tests/conftest.py b/tests/conftest.py index 5980c4c..eced74a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,13 @@ import pytest from command_line_assistant import config, logger +from command_line_assistant.config import ( + BackendSchema, + Config, + HistorySchema, + LoggingSchema, + OutputSchema, +) @pytest.fixture(autouse=True) @@ -21,3 +28,22 @@ def setup_logger(request, tmp_path): root_logger = logging.getLogger() for handler in root_logger.handlers: root_logger.removeHandler(handler) + + +@pytest.fixture +def mock_config(): + """Fixture to create a mock configuration""" + return Config( + output=OutputSchema( + enforce_script=False, + file=Path("/tmp/test_output.txt"), + prompt_separator="$", + ), + backend=BackendSchema( + endpoint="http://test.endpoint/v1/query", verify_ssl=True + ), + history=HistorySchema( + enabled=True, file=Path("/tmp/test_history.json"), max_size=100 + ), + logging=LoggingSchema(type="minimal"), + ) diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 5882ab0..0000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,41 +0,0 @@ -import sys - -import pytest - -from command_line_assistant import cli - - -def mock_cli_arguments(args): - """ - Return a list of cli arguments where the first one is always the name of - the executable, followed by 'args'. - """ - return sys.argv[0:1] + args - - -@pytest.mark.parametrize( - ("stdin", "expected"), - ( - ( - None, - "test", - ), - ("test", "test test"), - ), -) -def test_get_args(monkeypatch, stdin, expected): - monkeypatch.setattr(sys, "argv", mock_cli_arguments(["test"])) - monkeypatch.setattr(cli, "read_stdin", lambda: stdin) - parser, args = cli.get_args() - - assert parser - assert args.query_string == expected - - -def test_no_query_args(monkeypatch): - monkeypatch.setattr(sys, "argv", mock_cli_arguments([])) - # Empty output to make sure that there is nothing being assigned to args.query_string - monkeypatch.setattr(cli, "read_stdin", lambda: "") - - with pytest.raises(SystemExit): - cli.get_args() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index acd6f4f..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import select -import sys - -from command_line_assistant import utils - - -def test_get_payload(): - expected = {"query": "test"} - assert utils.get_payload(query="test") == expected - - -def test_read_stdin(monkeypatch): - # Mock select.select to simulate user input - def mock_select(*args, **kwargs): - return [sys.stdin], [], [] - - monkeypatch.setattr(select, "select", mock_select) - - # Mock sys.stdin.readline to return the desired input - monkeypatch.setattr(sys.stdin, "read", lambda: "test\n") - - assert utils.read_stdin() == "test" - - -def test_read_stdin_no_input(monkeypatch): - # Mock select.select to simulate user input - def mock_select(*args, **kwargs): - return [], [], [] - - monkeypatch.setattr(select, "select", mock_select) - - assert not utils.read_stdin() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/test_cli.py b/tests/utils/test_cli.py new file mode 100644 index 0000000..e4afe9c --- /dev/null +++ b/tests/utils/test_cli.py @@ -0,0 +1,67 @@ +import select +import sys + +import pytest + +from command_line_assistant.utils import cli + + +def test_read_stdin(monkeypatch): + # Mock select.select to simulate user input + def mock_select(*args, **kwargs): + return [sys.stdin], [], [] + + monkeypatch.setattr(select, "select", mock_select) + + # Mock sys.stdin.readline to return the desired input + monkeypatch.setattr(sys.stdin, "read", lambda: "test\n") + + assert cli.read_stdin() == "test" + + +def test_read_stdin_no_input(monkeypatch): + # Mock select.select to simulate user input + def mock_select(*args, **kwargs): + return [], [], [] + + monkeypatch.setattr(select, "select", mock_select) + + assert not cli.read_stdin() + + +@pytest.mark.parametrize( + ("input_args", "expected"), + [ + (["script_name"], []), + (["script_name", "history", "--clear"], ["history", "--clear"]), + (["script_name", "how to list files"], ["query", "how to list files"]), + ], +) +def test_add_default_command(input_args, expected): + """Test add_default_command with various inputs""" + args = cli.add_default_command(input_args) + assert args == expected + + +@pytest.mark.parametrize( + ("input_args", "expected"), + [ + (["script_name", "query", "some text"], "query"), + (["script_name", "history", "--clear"], "history"), + (["script_name", "--version"], "--version"), + (["script_name", "--help"], "--help"), + (["script_name", "some text"], None), + ], +) +def test_subcommand_used(input_args, expected): + """Test _subcommand_used with various inputs""" + assert cli._subcommand_used(input_args) == expected + + +def test_create_argument_parser(): + """Test create_argument_parser returns parser and subparser""" + parser, commands_parser = cli.create_argument_parser() + assert parser is not None + assert commands_parser is not None + assert parser.description is not None + assert commands_parser.dest == "command" diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py new file mode 100644 index 0000000..375a03d --- /dev/null +++ b/tests/utils/test_utils.py @@ -0,0 +1,6 @@ +from command_line_assistant import utils + + +def test_get_payload(): + expected = {"query": "test"} + assert utils.get_payload(query="test") == expected