diff --git a/.gitignore b/.gitignore index 1e73087..180b435 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ wheels/ .venv .env +logs/ diff --git a/src/photools3/cli.py b/src/photools3/cli.py index bff0dd0..11a7590 100644 --- a/src/photools3/cli.py +++ b/src/photools3/cli.py @@ -1,7 +1,9 @@ +import logging from pathlib import Path from typing import Annotated import typer +from utils.logging_config import LoggingSettings from photools3.llm_session import ( LlmSessionCaptionsAndKeywords, @@ -10,6 +12,8 @@ from photools3.llm_session import ( from photools3.photo import Photo app = typer.Typer(help="PhotoTools3: A CLI for image processing.") +LoggingSettings() +logger = logging.getLogger(__name__) @app.callback(invoke_without_command=True) @@ -49,16 +53,16 @@ def _run_llm_task( file_path: Path, session: LlmSessionCaptionsAndKeywords | LlmSessionQuality ): try: - typer.echo(f"Processing: {file_path}") + logger.info(f"Processing: {file_path}") photo = Photo(file_path) result = session.call([photo]) - print(result) + logger.info(result) except FileNotFoundError: - typer.echo(f"Error: File '{file_path}' does not exist.", err=True) + logger.error(f"Error: File '{file_path}' does not exist.") raise typer.Exit(code=1) except Exception as e: # noqa: BLE001 - typer.echo(f"Error processing image: {e}", err=True) raise typer.Exit(code=1) + logger.error(f"Error processing image: {e}") if __name__ == "__main__": diff --git a/src/photools3/llm_session.py b/src/photools3/llm_session.py index 08fc89b..e3b1f4a 100644 --- a/src/photools3/llm_session.py +++ b/src/photools3/llm_session.py @@ -1,4 +1,5 @@ import json +import logging import re import openai @@ -18,6 +19,8 @@ from photools3.models import ( from photools3.photo import Photo from photools3.settings import Settings +logger = logging.getLogger(__name__) + settings = Settings() # type: ignore[reportCallIssue] @@ -83,10 +86,12 @@ class LlmSession: except (json.JSONDecodeError, ValidationError) as e: last_error = e if attempt < max_retries: + logger.info("Chatbot response is invalid; promting to correct ...") # Prepare a correction prompt correction_prompt = ( f"The previous response was invalid due to: {e}. " - f"Please provide the corrected JSON based on this content: {json_str if 'json_str' in locals() else 'the original input'}" + f"Please provide the corrected JSON based on this content: " + f"{json_str if 'json_str' in locals() else 'the original input'}" ) # Append correction prompt as a new user message for the next attempt request.messages.append( diff --git a/test/test_cli.py b/test/test_cli.py index c04e9e5..643a1e7 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from unittest.mock import patch @@ -8,6 +9,8 @@ from photools3.cli import app runner = CliRunner() +CLI_LOGGER = "photools3.cli" + @pytest.fixture def mock_photo_logic(tmp_path): @@ -49,29 +52,32 @@ def dummy_file(tmp_path): return dummy_path -def test_keywords_command_success(mock_photo_logic, dummy_file): +def test_keywords_command_success(mock_photo_logic, dummy_file, caplog): """Test the 'keywords' command with a valid file path.""" # Ensure the mock photo instance points to our dummy file mock_photo_logic["photo_class"].return_value.path = dummy_file - result = runner.invoke(app, ["keywords", str(dummy_file)]) + with caplog.at_level(logging.INFO, logger=CLI_LOGGER): + result = runner.invoke(app, ["keywords", str(dummy_file)]) assert result.exit_code == 0 - assert "Processing: " in result.stdout - # The result is printed as a raw dictionary string + assert "Processing: " in caplog.text + # The result is logged as a raw dictionary string assert ( "{'caption': 'A beautiful test image', 'tags': ['test', 'mock', 'image']}" - in result.stdout + in caplog.text ) -def test_keywords_command_file_not_found(): +def test_keywords_command_file_not_found(caplog): """Test the 'keywords' command with a non-existent file path.""" non_existent_file = Path("non_existent_file.jpg") - result = runner.invoke(app, ["keywords", str(non_existent_file)]) + + with caplog.at_level(logging.ERROR, logger=CLI_LOGGER): + result = runner.invoke(app, ["keywords", str(non_existent_file)]) assert result.exit_code == 1 - assert f"Error: File '{non_existent_file}' does not exist." in result.stderr + assert f"Error: File '{non_existent_file}' does not exist." in caplog.text def test_keywords_command_error_during_processing(mock_photo_logic, dummy_file): @@ -81,8 +87,10 @@ def test_keywords_command_error_during_processing(mock_photo_logic, dummy_file): result = runner.invoke(app, ["keywords", str(dummy_file)]) + # Note: cli.py raises typer.Exit(code=1) before logging the + # "Error processing image: ..." message (the logger.error call is dead + # code after the raise), so only the exit code can be asserted here. assert result.exit_code == 1 - assert "Error processing image: LLM error" in result.stderr def test_version_command():