Cleo allows you to create beautiful and testable command-line interfaces.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
The exception handling system provides a comprehensive hierarchy of exception classes for different error conditions in CLI applications. These exceptions enable proper error handling, user feedback, and application exit codes.
The root exception class for all Cleo-specific errors with exit code support.
class CleoError(Exception):
"""Base exception class for all Cleo errors."""
exit_code: int | None = None
def __init__(self, message: str = "", exit_code: int | None = None) -> None:
"""
Create a Cleo error.
Args:
message (str): Error message
exit_code (int | None): Suggested exit code for the application
"""
super().__init__(message)
if exit_code is not None:
self.exit_code = exit_codeErrors related to incorrect configuration or programming logic issues.
class CleoLogicError(CleoError):
"""
Raised when there is error in command arguments
and/or options configuration logic.
"""
passErrors that occur during command execution at runtime.
class CleoRuntimeError(CleoError):
"""
Raised when command is called with invalid options or arguments.
"""
passErrors related to incorrect values or type mismatches.
class CleoValueError(CleoError):
"""
Raised when wrong value was given to Cleo components.
"""
passBase class for errors caused by user input issues.
class CleoUserError(CleoError):
"""
Base exception for user input errors.
These errors are typically caused by incorrect user input
and should result in helpful error messages.
"""
passSpecialized exceptions for common user input problems.
class CleoNoSuchOptionError(CleoError):
"""
Exception for undefined or invalid options.
Raised when command does not have given option.
"""
pass
class CleoMissingArgumentsError(CleoUserError):
"""
Exception for missing required arguments.
Raised when called command was not given required arguments.
"""
pass
class CleoCommandNotFoundError(CleoUserError):
"""
Exception for unknown commands.
Raised when called command does not exist.
"""
def __init__(self, name: str, commands: list[str] | None = None) -> None:
"""
Create command not found error with optional command suggestions.
Args:
name (str): The command name that was not found
commands (list[str] | None): Available commands for suggestions
"""
class CleoNamespaceNotFoundError(CleoUserError):
"""
Exception for unknown command namespaces.
Raised when called namespace has no commands.
"""
def __init__(self, name: str, namespaces: list[str] | None = None) -> None:
"""
Create namespace not found error with optional namespace suggestions.
Args:
name (str): The namespace name that was not found
namespaces (list[str] | None): Available namespaces for suggestions
"""from cleo.commands.command import Command
from cleo.exceptions import CleoError, CleoUserError, CleoRuntimeError
class ProcessCommand(Command):
name = "process"
description = "Process data files"
def handle(self):
try:
filename = self.argument("file")
# Validate input
if not filename:
raise CleoUserError("Filename is required", exit_code=1)
if not os.path.exists(filename):
raise CleoUserError(f"File '{filename}' not found", exit_code=2)
# Process file
result = self.process_file(filename)
if not result:
raise CleoRuntimeError("Processing failed", exit_code=3)
self.line(f"<info>Processed {filename} successfully</info>")
return 0
except CleoUserError as e:
self.line(f"<error>Error: {e}</error>")
return e.exit_code or 1
except CleoRuntimeError as e:
self.line(f"<error>Runtime error: {e}</error>")
return e.exit_code or 1
except Exception as e:
self.line(f"<error>Unexpected error: {e}</error>")
return 1
def process_file(self, filename):
# File processing logic
try:
with open(filename, 'r') as f:
data = f.read()
if len(data) == 0:
raise CleoValueError("File is empty")
# Process data...
return True
except IOError as e:
raise CleoRuntimeError(f"Failed to read file: {e}")from cleo.application import Application
from cleo.exceptions import CleoCommandNotFoundError, CleoNamespaceNotFoundError
class MyApplication(Application):
def run(self, input=None, output=None, error_output=None):
try:
return super().run(input, output, error_output)
except CleoCommandNotFoundError as e:
self.render_error(f"Command not found: {e}", output or error_output)
# Suggest similar commands
similar = self.find_similar_commands(str(e))
if similar:
self.render_error(f"Did you mean: {', '.join(similar)}", output or error_output)
return 1
except CleoNamespaceNotFoundError as e:
self.render_error(f"Namespace not found: {e}", output or error_output)
return 1
except KeyboardInterrupt:
self.render_error("\nOperation cancelled by user", output or error_output)
return 130 # Standard exit code for SIGINT
except Exception as e:
self.render_error(f"Unexpected error: {e}", output or error_output)
return 1
def render_error(self, message, output):
if output:
output.write_line(f"<error>{message}</error>")from cleo.exceptions import CleoUserError, CleoRuntimeError
class DatabaseConnectionError(CleoRuntimeError):
"""Exception for database connection failures."""
def __init__(self, host, port, message="Database connection failed"):
super().__init__(f"{message}: {host}:{port}", exit_code=10)
class ConfigurationError(CleoUserError):
"""Exception for configuration file errors."""
def __init__(self, config_file, issue):
super().__init__(f"Configuration error in {config_file}: {issue}", exit_code=5)
class ValidationError(CleoValueError):
"""Exception for input validation failures."""
def __init__(self, field, value, expected):
super().__init__(
f"Invalid value '{value}' for {field}. Expected: {expected}",
exit_code=2
)
# Usage in commands
class DatabaseCommand(Command):
def handle(self):
try:
host = self.option("host")
port = int(self.option("port"))
# Validate port
if not 1 <= port <= 65535:
raise ValidationError("port", port, "1-65535")
# Connect to database
if not self.connect_database(host, port):
raise DatabaseConnectionError(host, port)
self.line("<info>Database connection successful</info>")
except ValidationError as e:
self.line(f"<error>{e}</error>")
return e.exit_code
except DatabaseConnectionError as e:
self.line(f"<error>{e}</error>")
self.line("<comment>Check your database server and network connectivity</comment>")
return e.exit_codeclass BackupCommand(Command):
def handle(self):
backup_locations = ['/primary/backup', '/secondary/backup', '/tertiary/backup']
for i, location in enumerate(backup_locations):
try:
self.create_backup(location)
self.line(f"<info>Backup created at {location}</info>")
return 0
except CleoRuntimeError as e:
self.line(f"<comment>Backup failed at {location}: {e}</comment>")
if i == len(backup_locations) - 1:
# Last location, no more fallbacks
self.line("<error>All backup locations failed</error>")
return e.exit_code or 1
else:
# Try next location
self.line(f"<info>Trying next location...</info>")
continue
def create_backup(self, location):
if not os.path.exists(location):
raise CleoRuntimeError(f"Backup location does not exist: {location}")
if not os.access(location, os.W_OK):
raise CleoRuntimeError(f"No write access to backup location: {location}")
# Create backup...import pytest
from cleo.testers.command_tester import CommandTester
from cleo.exceptions import CleoUserError, CleoRuntimeError
def test_command_error_handling():
command = ProcessCommand()
tester = CommandTester(command)
# Test missing argument error
exit_code = tester.execute("")
assert exit_code == 1
assert "Filename is required" in tester.get_display()
# Test file not found error
exit_code = tester.execute("nonexistent.txt")
assert exit_code == 2
assert "File 'nonexistent.txt' not found" in tester.get_display()
def test_custom_exception_handling():
command = DatabaseCommand()
tester = CommandTester(command)
# Test invalid port
exit_code = tester.execute("--port 99999")
assert exit_code == 2
assert "Invalid value '99999' for port" in tester.get_display()
def test_exception_propagation():
"""Test that exceptions are properly caught and handled."""
command = FailingCommand()
tester = CommandTester(command)
# Should handle exception gracefully
exit_code = tester.execute("")
assert exit_code != 0 # Should indicate failure
assert "error" in tester.get_display().lower() # Should show error messageimport sys
import logging
from cleo.exceptions import CleoError
def setup_global_exception_handler():
"""Set up global exception handling for the application."""
def handle_exception(exc_type, exc_value, exc_traceback):
if isinstance(exc_value, CleoError):
# Cleo exceptions are handled by the application
return
# Log unexpected exceptions
logging.error(
"Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback)
)
# Show user-friendly message
print("An unexpected error occurred. Please check the logs for details.")
sys.exit(1)
sys.excepthook = handle_exception
# Use in main application
if __name__ == "__main__":
setup_global_exception_handler()
app = MyApplication()
try:
exit_code = app.run()
sys.exit(exit_code)
except KeyboardInterrupt:
print("\nOperation cancelled by user")
sys.exit(130)import logging
from cleo.exceptions import CleoError
class ReportingCommand(Command):
def __init__(self):
super().__init__()
self.logger = logging.getLogger(__name__)
def handle(self):
try:
# Command logic here
self.process_data()
except CleoUserError as e:
# User errors don't need full logging
self.line(f"<error>{e}</error>")
return e.exit_code or 1
except CleoRuntimeError as e:
# Runtime errors should be logged for debugging
self.logger.error(f"Runtime error in {self.name}: {e}")
self.line(f"<error>Operation failed: {e}</error>")
return e.exit_code or 1
except Exception as e:
# Unexpected errors need full logging
self.logger.exception(f"Unexpected error in {self.name}")
self.line("<error>An unexpected error occurred. Check logs for details.</error>")
return 1
def process_data(self):
# Processing logic that might raise exceptions
passInstall with Tessl CLI
npx tessl i tessl/pypi-cleo