Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 100% docstring coverage #78

Merged
merged 2 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,11 @@ repos:
hooks:
- id: gitleaks
stages: [manual, pre-push]
- repo: https://github.com/econchick/interrogate
rev: 1.7.0
hooks:
- id: interrogate
exclude: ^(docs/conf.py|setup.py|tests/|scripts)
args: ["-vv", "-c", "pyproject.toml", "command_line_assistant", "--fail-under", "100"]
# Workaround mentioned at https://github.com/econchick/interrogate/issues/60#issuecomment-735436566
pass_filenames: false
1 change: 1 addition & 0 deletions command_line_assistant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""This is the Command Line Assistant root module."""
1 change: 1 addition & 0 deletions command_line_assistant/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Module to track all the CLI commands."""
53 changes: 51 additions & 2 deletions command_line_assistant/commands/history.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Module to handle the history command."""

import logging
from argparse import Namespace

Expand All @@ -21,13 +23,26 @@


def _initialize_spinner_renderer() -> SpinnerRenderer:
"""Initialize a new text renderer class

Returns:
SpinnerRenderer: Instance of a text renderer class with decorators.
"""
spinner = SpinnerRenderer(message="Loading history", stream=StdoutStream(end=""))
spinner.update(TextWrapDecorator())

return spinner


def _initialize_qa_renderer(is_assistant: bool = False) -> TextRenderer:
"""Initialize a new text renderer class.

Args:
is_assistant (bool): Apply different decorators if it is assistant.

Returns:
TextRenderer: Instance of a text renderer class with decorators.
"""
text = TextRenderer(stream=StdoutStream(end="\n"))
foreground = "lightblue" if is_assistant else "lightgreen"
text.update(ColorDecorator(foreground=foreground))
Expand All @@ -36,12 +51,30 @@ def _initialize_qa_renderer(is_assistant: bool = False) -> TextRenderer:


def _initialize_text_renderer() -> TextRenderer:
"""Initialize a new text renderer class

Returns:
TextRenderer: Instance of a text renderer class with decorators.
"""
text = TextRenderer(stream=StdoutStream(end="\n"))
return text


class HistoryCommand(BaseCLICommand):
"""Class that represents the history command."""

def __init__(self, clear: bool, first: bool, last: bool) -> None:
"""Constructor of the class.

Note:
If none of the above is specified, the command will retrieve all
user history.

Args:
clear (bool): If the history should be cleared
first (bool): Retrieve only the first conversation from history
last (bool): Retrieve only last conversation from history
"""
self._clear = clear
self._first = first
self._last = last
Expand All @@ -55,6 +88,11 @@ def __init__(self, clear: bool, first: bool, last: bool) -> None:
super().__init__()

def run(self) -> int:
"""Main entrypoint for the command to run.

Returns:
int: Status code of the execution.
"""
try:
if self._clear:
self._clear_history()
Expand Down Expand Up @@ -92,6 +130,7 @@ def _retrieve_all_conversations(self) -> None:
self._text_renderer.render("-" * 50) # Separator between conversations

def _retrieve_first_conversation(self) -> None:
"""Retrieve the first conversation in the conversation cache."""
logger.info("Getting first conversation from history.")
response = self._proxy.GetFirstConversation()
history = HistoryEntry.from_structure(response)
Expand All @@ -106,6 +145,7 @@ def _retrieve_first_conversation(self) -> None:
self._text_renderer.render(f"Time: {entry.timestamp}")

def _retrieve_last_conversation(self):
"""Retrieve the last conversation in the conversation cache."""
logger.info("Getting last conversation from history.")
response = self._proxy.GetLastConversation()

Expand All @@ -123,16 +163,17 @@ def _retrieve_last_conversation(self):
self._text_renderer.render(f"Time: {entry.timestamp}")

def _clear_history(self) -> None:
"""Clear the user history"""
self._text_renderer.render("Cleaning the history.")
self._proxy.ClearHistory()


def register_subcommand(parser: SubParsersAction):
"""
Register this command to argparse so it's available for the datasets-cli
Register this command to argparse so it's available for the root parser.

Args:
parser: Root parser to register command-specific arguments
parser (SubParsersAction): Root parser to register command-specific arguments
"""
history_parser = parser.add_parser(
"history",
Expand All @@ -157,4 +198,12 @@ def register_subcommand(parser: SubParsersAction):


def _command_factory(args: Namespace) -> HistoryCommand:
"""Internal command factory to create the command class

Args:
args (Namespace): The arguments processed with argparse.

Returns:
HistoryCommand: Return an instance of class
"""
return HistoryCommand(args.clear, args.first, args.last)
48 changes: 45 additions & 3 deletions command_line_assistant/commands/query.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Module to handle the query command."""

from argparse import Namespace

from command_line_assistant.dbus.constants import QUERY_IDENTIFIER
Expand All @@ -24,12 +26,19 @@
"Interactions with RHEL Lightspeed may be reviewed and used to improve our "
"products and service."
)
#: Legal notice that we need to output once per user
ALWAYS_LEGAL_MESSAGE = (
"Always check AI/LLM-generated responses for accuracy prior to use."
)
#: Always good to have legal message.


def _initialize_spinner_renderer() -> SpinnerRenderer:
"""Initialize a new spinner renderer class

Returns:
SpinnerRenderer: Instance of a spinner renderer class with decorators.
"""
spinner = SpinnerRenderer(
message="Requesting knowledge from AI", stream=StdoutStream(end="")
)
Expand All @@ -41,6 +50,11 @@ def _initialize_spinner_renderer() -> SpinnerRenderer:


def _initialize_text_renderer() -> TextRenderer:
"""Initialize a new text renderer class

Returns:
TextRenderer: Instance of a text renderer class with decorators.
"""
text = TextRenderer(stream=StdoutStream(end="\n"))
text.update(ColorDecorator(foreground="green")) # Robot emoji
text.update(TextWrapDecorator())
Expand All @@ -49,6 +63,14 @@ def _initialize_text_renderer() -> TextRenderer:


def _initialize_legal_renderer(write_once: bool = False) -> TextRenderer:
"""Initialize a new text renderer class

Args:
write_once (bool): If it should add the `py:WriteOnceDecorator` or not.

Returns:
SpinnerRenderer: Instance of a text renderer class with decorators.
"""
text = TextRenderer(stream=StderrStream())
text.update(ColorDecorator(foreground="lightyellow"))
text.update(TextWrapDecorator())
Expand All @@ -60,7 +82,14 @@ def _initialize_legal_renderer(write_once: bool = False) -> TextRenderer:


class QueryCommand(BaseCLICommand):
"""Class that represents the query command."""

def __init__(self, query_string: str) -> None:
"""Constructor of the class.

Args:
query_string (str): The query provided by the user.
"""
self._query = query_string

self._spinner_renderer: SpinnerRenderer = _initialize_spinner_renderer()
Expand All @@ -71,6 +100,11 @@ def __init__(self, query_string: str) -> None:
super().__init__()

def run(self) -> int:
"""Main entrypoint for the command to run.

Returns:
int: Status code of the execution
"""
proxy = QUERY_IDENTIFIER.get_proxy()
input_query = Message()
input_query.message = self._query
Expand Down Expand Up @@ -99,14 +133,14 @@ def run(self) -> int:

def register_subcommand(parser: SubParsersAction) -> None:
"""
Register this command to argparse so it's available for the datasets-cli
Register this command to argparse so it's available for the root parser.

Args:
parser: Root parser to register command-specific arguments
parser (SubParsersAction): Root parser to register command-specific arguments
"""
query_parser = parser.add_parser(
"query",
help="ask a question and get an answer from llm.",
help="Ask a question and get an answer from LLM.",
)
# Positional argument, required only if no optional arguments are provided
query_parser.add_argument(
Expand All @@ -117,4 +151,12 @@ def register_subcommand(parser: SubParsersAction) -> None:


def _command_factory(args: Namespace) -> QueryCommand:
"""Internal command factory to create the command class

Args:
args (Namespace): The arguments processed with argparse.

Returns:
QueryCommand: Return an instance of class
"""
return QueryCommand(args.query_string)
32 changes: 28 additions & 4 deletions command_line_assistant/commands/record.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""Module to handle the record command.

Note:
This needs more refinement, script session can't be combined with other arguments
"""

import logging
from argparse import Namespace
from pathlib import Path
Expand All @@ -7,25 +13,35 @@

logger = logging.getLogger(__name__)

# NOTE: This needs more refinement, script session can't be combined with other arguments


class RecordCommand(BaseCLICommand):
"""Class that represents the record command."""

def __init__(self, output_file: str) -> None:
"""Constructor of the class.

Args:
output_file (str): The file to write the output.
"""
self._output_file = output_file
super().__init__()

def run(self) -> int:
"""Main entrypoint for the command to run.

Returns:
int: Status code of the execution.
"""
handle_script_session(Path(self._output_file))
return 0


def register_subcommand(parser: SubParsersAction):
"""
Register this command to argparse so it's available for the datasets-cli
Register this command to argparse so it's available for the root parser.

Args:
parser: Root parser to register command-specific arguments
parser (SubParsersAction): Root parser to register command-specific arguments
"""
record_parser = parser.add_parser(
"record",
Expand All @@ -39,4 +55,12 @@ def register_subcommand(parser: SubParsersAction):


def _command_factory(args: Namespace) -> RecordCommand:
"""Internal command factory to create the command class

Args:
args (Namespace): The arguments processed with argparse.

Returns:
RecordCommand: Return an instance of class
"""
return RecordCommand(args.output_file)
22 changes: 16 additions & 6 deletions command_line_assistant/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Main configuration module."""

from __future__ import annotations

import dataclasses
Expand All @@ -24,6 +26,7 @@
"command_line_assistant",
"config.toml",
)
#: Define the config file path.

logger = logging.getLogger(__name__)

Expand All @@ -37,11 +40,11 @@ class Config:
>>> config = Config()
>>> config.output.enforce_script

The currently available top-level fields are:
* output = Match the `py:OutputSchema` class and their fields
* history = Match the `py:HistorySchema` class and their fields
* backend = Match the `py:BackendSchema` class and their fields
* logging = Match the `py:LoggingSchema` class and their fields
Attributes:
output (OutputSchema): Match the `py:OutputSchema` class and their fields
history (HistorySchema): Match the `py:HistorySchema` class and their fields
backend (BackendSchema): Match the `py:BackendSchema` class and their fields
logging (LoggingSchema): Match the `py:LoggingSchema` class and their fields
"""

output: OutputSchema = dataclasses.field(default_factory=OutputSchema)
Expand All @@ -51,8 +54,15 @@ class Config:


def load_config_file() -> Config:
"""Read configuration file."""
"""Load the configuration file from the system.

Raises:
FileNotFoundError: In case the configuration file is missing
tomllib.TOMLDecodeError: In case it is not possible to decode the config file

Returns:
Config: An instance of the configuration file
"""
config_dict = {}
config_file_path = Path(get_xdg_config_path(), *CONFIG_FILE_DEFINITION)

Expand Down
Loading
Loading