diff --git a/forklet/infrastructure/logger.py b/forklet/infrastructure/logger.py index 4f5cba8..0d2efba 100644 --- a/forklet/infrastructure/logger.py +++ b/forklet/infrastructure/logger.py @@ -49,7 +49,7 @@ def setup_logger( # Add console handler if requested if console: - console_handler = logging.StreamHandler(sys.stdout) + console_handler = logging.StreamHandler(sys.stderr) console_handler.setFormatter(formatter) logger.addHandler(console_handler) diff --git a/forklet/interfaces/api.py b/forklet/interfaces/api.py index c96d1cd..0c69071 100644 --- a/forklet/interfaces/api.py +++ b/forklet/interfaces/api.py @@ -3,6 +3,7 @@ Python API for Forklet GitHub Repository Downloader. """ +import logging from typing import Optional, List, Dict, Any, Callable from pathlib import Path @@ -14,8 +15,6 @@ DownloadRequest, DownloadResult, DownloadStrategy, FilterCriteria, RepositoryInfo, GitReference, ProgressInfo, DownloadConfig ) - - ##### class GitHubDownloader: """ @@ -23,16 +22,27 @@ class GitHubDownloader: Provides a clean, typed interface for downloading GitHub repository content. """ - - def __init__(self, auth_token: Optional[str] = None): + + def __init__(self, auth_token: Optional[str] = None, verbose: bool = False): """ Initialize the downloader with optional authentication. Args: auth_token: GitHub personal access token for authentication + verbose: Enable detailed logging and progress information (default: False) """ - + + + self.verbose = verbose self.auth_token = auth_token + + # Configure logger based on verbose setting + if verbose: + logger.setLevel(logging.DEBUG) + logger.debug("Verbose logging enabled for GitHubDownloader") + else: + logger.setLevel(logging.INFO) + self.rate_limiter = RateLimiter() self.retry_manager = RetryManager() @@ -121,9 +131,18 @@ async def download( """ try: + logger.debug(f"Starting download for {owner}/{repo}@{ref}") + logger.debug(f"Destination: {destination}") + logger.debug(f"Strategy: {strategy}") + logger.debug(f"Include patterns: {include_patterns}") + logger.debug(f"Exclude patterns: {exclude_patterns}") + # Get repository information repo_info = await self.get_repository_info(owner, repo) + logger.debug(f"Repository info: {repo_info.full_name}, size: {repo_info.size}KB") + git_ref = await self.resolve_reference(owner, repo, ref) + logger.debug(f"Resolved reference {ref} to {git_ref.ref_type}: {git_ref.sha}") # Create filter criteria filters = FilterCriteria( @@ -148,8 +167,14 @@ async def download( ) # Execute download + logger.debug("Starting download execution...") + result = await self.orchestrator.execute_download(request) + logger.debug(f"Download completed: {len(result.downloaded_files)} files, " + f"{len(result.failed_files)} failures, " + f"{result.progress.downloaded_bytes} bytes") + return result except Exception as e: @@ -270,3 +295,18 @@ async def get_download_progress(self) -> Optional[ProgressInfo]: """ return await self.orchestrator.get_current_progress() + + def set_verbose(self, verbose: bool) -> None: + """ + Enable or disable verbose logging at runtime. + + Args: + verbose: True to enable verbose logging, False to disable + """ + self.verbose = verbose + if verbose: + logger.setLevel(logging.DEBUG) + logger.debug("Verbose logging enabled") + else: + logger.setLevel(logging.INFO) + logger.info("Verbose logging disabled") diff --git a/tests/interfaces/test_verbose_api.py b/tests/interfaces/test_verbose_api.py new file mode 100644 index 0000000..c59a422 --- /dev/null +++ b/tests/interfaces/test_verbose_api.py @@ -0,0 +1,95 @@ +""" +Unit tests for verbose logging functionality in GitHubDownloader API. +""" + +import pytest +import logging +from unittest.mock import Mock, patch +from forklet.interfaces.api import GitHubDownloader +from forklet.infrastructure.logger import logger + + +class TestVerboseLogging: + """Test cases for verbose logging functionality.""" + + def test_default_initialization(self): + """Test that GitHubDownloader initializes with verbose=False by default.""" + downloader = GitHubDownloader() + assert downloader.verbose is False + + def test_verbose_initialization(self): + """Test that GitHubDownloader can be initialized with verbose=True.""" + downloader = GitHubDownloader(verbose=True) + assert downloader.verbose is True + + def test_verbose_with_auth_token(self): + """Test that verbose mode works with authentication token.""" + downloader = GitHubDownloader(auth_token="test_token", verbose=True) + assert downloader.verbose is True + assert downloader.auth_token == "test_token" + + @patch('forklet.interfaces.api.logger') + def test_logger_level_verbose_true(self, mock_logger): + """Test that logger level is set to DEBUG when verbose=True.""" + GitHubDownloader(verbose=True) + mock_logger.setLevel.assert_called_with(logging.DEBUG) + + @patch('forklet.interfaces.api.logger') + def test_logger_level_verbose_false(self, mock_logger): + """Test that logger level is set to INFO when verbose=False.""" + GitHubDownloader(verbose=False) + mock_logger.setLevel.assert_called_with(logging.INFO) + + @patch('forklet.interfaces.api.logger') + def test_set_verbose_method_enable(self, mock_logger): + """Test the set_verbose method when enabling verbose mode.""" + downloader = GitHubDownloader(verbose=False) + downloader.set_verbose(True) + + assert downloader.verbose is True + # Should be called twice: once in __init__, once in set_verbose + assert mock_logger.setLevel.call_count >= 2 + mock_logger.setLevel.assert_called_with(logging.DEBUG) + + @patch('forklet.interfaces.api.logger') + def test_set_verbose_method_disable(self, mock_logger): + """Test the set_verbose method when disabling verbose mode.""" + downloader = GitHubDownloader(verbose=True) + downloader.set_verbose(False) + + assert downloader.verbose is False + # Should be called twice: once in __init__, once in set_verbose + assert mock_logger.setLevel.call_count >= 2 + mock_logger.setLevel.assert_called_with(logging.INFO) + + def test_verbose_mode_toggle(self): + """Test toggling verbose mode multiple times.""" + downloader = GitHubDownloader() + + # Start with False + assert downloader.verbose is False + + # Enable + downloader.set_verbose(True) + assert downloader.verbose is True + + # Disable + downloader.set_verbose(False) + assert downloader.verbose is False + + # Enable again + downloader.set_verbose(True) + assert downloader.verbose is True + + @patch('forklet.interfaces.api.logger') + def test_verbose_logging_in_download_method(self, mock_logger): + """Test that verbose logging occurs during download operations.""" + # This test would require mocking the entire download chain + # For now, we'll just test that the downloader is properly configured + downloader = GitHubDownloader(verbose=True) + + # Verify the downloader has verbose mode enabled + assert downloader.verbose is True + + # Verify logger was configured for DEBUG level + mock_logger.setLevel.assert_called_with(logging.DEBUG) \ No newline at end of file