From 7d990f4065f5d851372963fd473ea8da1d4f3780 Mon Sep 17 00:00:00 2001 From: #Einswilli Date: Mon, 30 Mar 2026 14:15:37 +0000 Subject: [PATCH] fix: Improve error handling in CLI to show clean error messages instead of tracebacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace raw exception tracebacks with clean, user-friendly error messages - Add verbose mode logging for debugging when needed - Standardize error message formatting with 💥 prefix - Apply improvements to both download and info commands - Add context parameter to status command for verbose logging access - Improve import organization for better readability Changes: - In download command: Show '💥 Error: {message}' instead of full traceback - In info command: Show '❌ Error: {message}' with verbose debug logging - In status command: Show '❌ Error: {message}' with verbose debug logging - When verbose flag is enabled, log full traceback for debugging - Maintain proper exit codes for error conditions --- .gitignore | 1 + forklet/__main__.py | 13 ++-- forklet/interfaces/cli.py | 144 +++++++++++++++++++------------------- 3 files changed, 82 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index ba32444..2b3e41d 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,4 @@ __marimo__/ # Test Downloas .downloads/* +forklet/*.py.backup diff --git a/forklet/__main__.py b/forklet/__main__.py index 3cb3c81..282f70c 100644 --- a/forklet/__main__.py +++ b/forklet/__main__.py @@ -152,12 +152,15 @@ async def get_repo_info(): click.echo(f"🎯 Current ref: {git_ref}") except Exception as e: - click.echo(f"❌ Error: {e}", err=True) + click.echo(f"💥 Error: {e}", err=True) + if ctx.obj.get("verbose", False): + logger.debug("Error in info command", exc_info=True) sys.exit(1) @cli.command() -def status(): +@click.pass_context +def status(ctx): """Show current download status and progress""" try: @@ -182,7 +185,9 @@ def status(): click.echo(f" ⏱️ ETA: {progress.eta_seconds:.0f} seconds") except Exception as e: - click.echo(f"❌ Error: {e}", err=True) + click.echo(f"💥 Error: {e}", err=True) + if ctx.obj.get("verbose", False): + logger.debug("Error in status command", exc_info=True) sys.exit(1) @@ -196,5 +201,3 @@ def version(): #### MAIN ENTRYPOINT FOR THE FORKLET CLI def main(): cli() - - diff --git a/forklet/interfaces/cli.py b/forklet/interfaces/cli.py index 2a1110d..1a7485d 100644 --- a/forklet/interfaces/cli.py +++ b/forklet/interfaces/cli.py @@ -11,13 +11,19 @@ from forklet.core import DownloadOrchestrator from forklet.services import GitHubAPIService, DownloadService from forklet.infrastructure import ( - RateLimiter, RetryManager, DownloadError, - RateLimitError, AuthenticationError, RepositoryNotFoundError + RateLimiter, + RetryManager, + DownloadError, + RateLimitError, + AuthenticationError, + RepositoryNotFoundError, ) from forklet.infrastructure.logger import logger from forklet.models import ( - DownloadRequest, DownloadStrategy, FilterCriteria, - DownloadResult + DownloadRequest, + DownloadStrategy, + FilterCriteria, + DownloadResult, ) @@ -26,17 +32,15 @@ ##### class ForkletCLI: """Main CLI application class.""" - + def __init__(self): self.rate_limiter = RateLimiter() self.retry_manager = RetryManager() self.github_service: Optional[GitHubAPIService] = None self.download_service: Optional[DownloadService] = None self.orchestrator: Optional[DownloadOrchestrator] = None - - def initialize_services( - self, auth_token: Optional[str] = None - ) -> None: + + def initialize_services(self, auth_token: Optional[str] = None) -> None: """Initialize all services with optional authentication.""" self.github_service = GitHubAPIService( @@ -46,34 +50,30 @@ def initialize_services( self.orchestrator = DownloadOrchestrator( self.github_service, self.download_service ) - + def parse_repository_string(self, repo_str: str) -> Tuple[str, str]: """ Parse repository string in format owner/repo. - + Args: repo_str: Repository string - + Returns: Tuple of (owner, repo) - + Raises: click.BadParameter: If format is invalid """ - if '/' not in repo_str: - raise click.BadParameter( - "Repository must be in format 'owner/repo'" - ) - - parts = repo_str.split('/') + if "/" not in repo_str: + raise click.BadParameter("Repository must be in format 'owner/repo'") + + parts = repo_str.split("/") if len(parts) != 2: - raise click.BadParameter( - "Repository must be in format 'owner/repo'" - ) - + raise click.BadParameter("Repository must be in format 'owner/repo'") + return parts[0], parts[1] - + def create_filter_criteria( self, include: List[str], @@ -84,11 +84,11 @@ def create_filter_criteria( exclude_extensions: List[str], include_hidden: bool, include_binary: bool, - target_paths: List[str] + target_paths: List[str], ) -> FilterCriteria: """ Create filter criteria from CLI options. - + Args: include: Include patterns exclude: Exclude patterns @@ -99,23 +99,23 @@ def create_filter_criteria( include_hidden: Include hidden files include_binary: Include binary files target_paths: Specific paths to download - + Returns: FilterCriteria object """ return FilterCriteria( - include_patterns = include, - exclude_patterns = exclude, - max_file_size = max_size, - min_file_size = min_size, - file_extensions = set(extensions), - excluded_extensions = set(exclude_extensions), - include_hidden = include_hidden, - include_binary = include_binary, - target_paths = target_paths + include_patterns=include, + exclude_patterns=exclude, + max_file_size=max_size, + min_file_size=min_size, + file_extensions=set(extensions), + excluded_extensions=set(exclude_extensions), + include_hidden=include_hidden, + include_binary=include_binary, + target_paths=target_paths, ) - + async def execute_download( self, repository: str, @@ -132,7 +132,7 @@ async def execute_download( ) -> None: """ Execute the download operation. - + Args: repository: Repository string (owner/repo) destination: Destination directory @@ -147,66 +147,66 @@ async def execute_download( try: # Initialize services self.initialize_services(token) - + # Parse repository owner, repo_name = self.parse_repository_string(repository) - + # Get repository info click.echo(f"📦 Fetching repository information for {owner}/{repo_name}...") repo_info = await self.github_service.get_repository_info(owner, repo_name) - + # Resolve Git reference click.echo(f"🔍 Resolving reference '{ref}'...") git_ref = await self.github_service.resolve_reference(owner, repo_name, ref) - + # Create download request request = DownloadRequest( - repository = repo_info, - git_ref = git_ref, - destination = Path(destination), - strategy = strategy, - filters = filters, - token = token, - max_concurrent_downloads = concurrent, - overwrite_existing = overwrite, - show_progress_bars = progress - ,dry_run = dry_run + repository=repo_info, + git_ref=git_ref, + destination=Path(destination), + strategy=strategy, + filters=filters, + token=token, + max_concurrent_downloads=concurrent, + overwrite_existing=overwrite, + show_progress_bars=progress, + dry_run=dry_run, ) - + # Execute download - click.echo( - f"🚀 Starting download with {concurrent} concurrent workers..." - ) + click.echo(f"🚀 Starting download with {concurrent} concurrent workers...") result = await self.orchestrator.execute_download(request) # Display results (pass through verbose flag) self.display_results(result, verbose=verbose) - + except ( - RateLimitError, AuthenticationError, - RepositoryNotFoundError, DownloadError + RateLimitError, + AuthenticationError, + RepositoryNotFoundError, + DownloadError, ) as e: - click.echo(f"❌ Error: {e}", err=True) + click.echo(f"💥 Error: {e}", err=True) sys.exit(1) except Exception as e: click.echo(f"💥 Unexpected error: {e}", err=True) - logger.exception("Unexpected error in download operation") + logger.debug("Unexpected error in download operation", exc_info=True) sys.exit(1) - + def display_results(self, result: DownloadResult, verbose: bool = False) -> None: """ Display download results in a user-friendly format. - + Args: result: Download result """ - if hasattr(result, 'is_successful') and result.is_successful: + if hasattr(result, "is_successful") and result.is_successful: click.echo("✅ Download completed successfully!") click.echo(f" 📁 Files: {len(result.downloaded_files)} downloaded") click.echo(f" 💾 Size: {result.progress.downloaded_bytes} bytes") - + if result.average_speed is not None: click.echo(f" ⚡ Speed: {result.average_speed:.2f} bytes/sec") @@ -216,7 +216,7 @@ def display_results(self, result: DownloadResult, verbose: bool = False) -> None # When verbose, display file paths (matched / downloaded / skipped) if verbose: # Matched files (available in dry-run and set by orchestrator) - if hasattr(result, 'matched_files') and result.matched_files: + if hasattr(result, "matched_files") and result.matched_files: click.echo(" 🔎 Matched files:") for p in result.matched_files: click.echo(f" {p}") @@ -231,17 +231,19 @@ def display_results(self, result: DownloadResult, verbose: bool = False) -> None click.echo(" ⏭️ Skipped paths:") for p in result.skipped_files: click.echo(f" {p}") - - elif hasattr(result, 'failed_files') and result.failed_files: + + elif hasattr(result, "failed_files") and result.failed_files: click.echo("⚠️ Download completed with errors:") click.echo(f" ✅ Successful: {len(result.downloaded_files)}") click.echo(f" ❌ Failed: {len(result.failed_files)}") - + # Show first few errors - for i, (filename, error) in enumerate(list(result.failed_files.items())[:3]): + for i, (filename, error) in enumerate( + list(result.failed_files.items())[:3] + ): click.echo(f" {filename}: {error}") if len(result.failed_files) > 3: click.echo(f" ... and {len(result.failed_files) - 3} more errors") - + else: click.echo("❌ Download failed completely")