0
# Exception Handling System
1
2
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.
3
4
## Capabilities
5
6
### Base Exception Class
7
8
The root exception class for all Cleo-specific errors with exit code support.
9
10
```python { .api }
11
class CleoError(Exception):
12
"""Base exception class for all Cleo errors."""
13
14
exit_code: int | None = None
15
16
def __init__(self, message: str = "", exit_code: int | None = None) -> None:
17
"""
18
Create a Cleo error.
19
20
Args:
21
message (str): Error message
22
exit_code (int | None): Suggested exit code for the application
23
"""
24
super().__init__(message)
25
if exit_code is not None:
26
self.exit_code = exit_code
27
```
28
29
### Configuration and Logic Errors
30
31
Errors related to incorrect configuration or programming logic issues.
32
33
```python { .api }
34
class CleoLogicError(CleoError):
35
"""
36
Raised when there is error in command arguments
37
and/or options configuration logic.
38
"""
39
pass
40
```
41
42
### Runtime Execution Errors
43
44
Errors that occur during command execution at runtime.
45
46
```python { .api }
47
class CleoRuntimeError(CleoError):
48
"""
49
Raised when command is called with invalid options or arguments.
50
"""
51
pass
52
```
53
54
### Value and Type Errors
55
56
Errors related to incorrect values or type mismatches.
57
58
```python { .api }
59
class CleoValueError(CleoError):
60
"""
61
Raised when wrong value was given to Cleo components.
62
"""
63
pass
64
```
65
66
### User Input Errors
67
68
Base class for errors caused by user input issues.
69
70
```python { .api }
71
class CleoUserError(CleoError):
72
"""
73
Base exception for user input errors.
74
75
These errors are typically caused by incorrect user input
76
and should result in helpful error messages.
77
"""
78
pass
79
```
80
81
### Specific User Input Error Types
82
83
Specialized exceptions for common user input problems.
84
85
```python { .api }
86
class CleoNoSuchOptionError(CleoError):
87
"""
88
Exception for undefined or invalid options.
89
90
Raised when command does not have given option.
91
"""
92
pass
93
94
class CleoMissingArgumentsError(CleoUserError):
95
"""
96
Exception for missing required arguments.
97
98
Raised when called command was not given required arguments.
99
"""
100
pass
101
102
class CleoCommandNotFoundError(CleoUserError):
103
"""
104
Exception for unknown commands.
105
106
Raised when called command does not exist.
107
"""
108
109
def __init__(self, name: str, commands: list[str] | None = None) -> None:
110
"""
111
Create command not found error with optional command suggestions.
112
113
Args:
114
name (str): The command name that was not found
115
commands (list[str] | None): Available commands for suggestions
116
"""
117
118
class CleoNamespaceNotFoundError(CleoUserError):
119
"""
120
Exception for unknown command namespaces.
121
122
Raised when called namespace has no commands.
123
"""
124
125
def __init__(self, name: str, namespaces: list[str] | None = None) -> None:
126
"""
127
Create namespace not found error with optional namespace suggestions.
128
129
Args:
130
name (str): The namespace name that was not found
131
namespaces (list[str] | None): Available namespaces for suggestions
132
"""
133
```
134
135
## Usage Examples
136
137
### Basic Exception Handling in Commands
138
139
```python
140
from cleo.commands.command import Command
141
from cleo.exceptions import CleoError, CleoUserError, CleoRuntimeError
142
143
class ProcessCommand(Command):
144
name = "process"
145
description = "Process data files"
146
147
def handle(self):
148
try:
149
filename = self.argument("file")
150
151
# Validate input
152
if not filename:
153
raise CleoUserError("Filename is required", exit_code=1)
154
155
if not os.path.exists(filename):
156
raise CleoUserError(f"File '{filename}' not found", exit_code=2)
157
158
# Process file
159
result = self.process_file(filename)
160
161
if not result:
162
raise CleoRuntimeError("Processing failed", exit_code=3)
163
164
self.line(f"<info>Processed {filename} successfully</info>")
165
return 0
166
167
except CleoUserError as e:
168
self.line(f"<error>Error: {e}</error>")
169
return e.exit_code or 1
170
171
except CleoRuntimeError as e:
172
self.line(f"<error>Runtime error: {e}</error>")
173
return e.exit_code or 1
174
175
except Exception as e:
176
self.line(f"<error>Unexpected error: {e}</error>")
177
return 1
178
179
def process_file(self, filename):
180
# File processing logic
181
try:
182
with open(filename, 'r') as f:
183
data = f.read()
184
185
if len(data) == 0:
186
raise CleoValueError("File is empty")
187
188
# Process data...
189
return True
190
191
except IOError as e:
192
raise CleoRuntimeError(f"Failed to read file: {e}")
193
```
194
195
### Application-Level Exception Handling
196
197
```python
198
from cleo.application import Application
199
from cleo.exceptions import CleoCommandNotFoundError, CleoNamespaceNotFoundError
200
201
class MyApplication(Application):
202
def run(self, input=None, output=None, error_output=None):
203
try:
204
return super().run(input, output, error_output)
205
206
except CleoCommandNotFoundError as e:
207
self.render_error(f"Command not found: {e}", output or error_output)
208
209
# Suggest similar commands
210
similar = self.find_similar_commands(str(e))
211
if similar:
212
self.render_error(f"Did you mean: {', '.join(similar)}", output or error_output)
213
214
return 1
215
216
except CleoNamespaceNotFoundError as e:
217
self.render_error(f"Namespace not found: {e}", output or error_output)
218
return 1
219
220
except KeyboardInterrupt:
221
self.render_error("\nOperation cancelled by user", output or error_output)
222
return 130 # Standard exit code for SIGINT
223
224
except Exception as e:
225
self.render_error(f"Unexpected error: {e}", output or error_output)
226
return 1
227
228
def render_error(self, message, output):
229
if output:
230
output.write_line(f"<error>{message}</error>")
231
```
232
233
### Custom Exception Classes
234
235
```python
236
from cleo.exceptions import CleoUserError, CleoRuntimeError
237
238
class DatabaseConnectionError(CleoRuntimeError):
239
"""Exception for database connection failures."""
240
241
def __init__(self, host, port, message="Database connection failed"):
242
super().__init__(f"{message}: {host}:{port}", exit_code=10)
243
244
class ConfigurationError(CleoUserError):
245
"""Exception for configuration file errors."""
246
247
def __init__(self, config_file, issue):
248
super().__init__(f"Configuration error in {config_file}: {issue}", exit_code=5)
249
250
class ValidationError(CleoValueError):
251
"""Exception for input validation failures."""
252
253
def __init__(self, field, value, expected):
254
super().__init__(
255
f"Invalid value '{value}' for {field}. Expected: {expected}",
256
exit_code=2
257
)
258
259
# Usage in commands
260
class DatabaseCommand(Command):
261
def handle(self):
262
try:
263
host = self.option("host")
264
port = int(self.option("port"))
265
266
# Validate port
267
if not 1 <= port <= 65535:
268
raise ValidationError("port", port, "1-65535")
269
270
# Connect to database
271
if not self.connect_database(host, port):
272
raise DatabaseConnectionError(host, port)
273
274
self.line("<info>Database connection successful</info>")
275
276
except ValidationError as e:
277
self.line(f"<error>{e}</error>")
278
return e.exit_code
279
280
except DatabaseConnectionError as e:
281
self.line(f"<error>{e}</error>")
282
self.line("<comment>Check your database server and network connectivity</comment>")
283
return e.exit_code
284
```
285
286
### Exception Handling with Recovery
287
288
```python
289
class BackupCommand(Command):
290
def handle(self):
291
backup_locations = ['/primary/backup', '/secondary/backup', '/tertiary/backup']
292
293
for i, location in enumerate(backup_locations):
294
try:
295
self.create_backup(location)
296
self.line(f"<info>Backup created at {location}</info>")
297
return 0
298
299
except CleoRuntimeError as e:
300
self.line(f"<comment>Backup failed at {location}: {e}</comment>")
301
302
if i == len(backup_locations) - 1:
303
# Last location, no more fallbacks
304
self.line("<error>All backup locations failed</error>")
305
return e.exit_code or 1
306
else:
307
# Try next location
308
self.line(f"<info>Trying next location...</info>")
309
continue
310
311
def create_backup(self, location):
312
if not os.path.exists(location):
313
raise CleoRuntimeError(f"Backup location does not exist: {location}")
314
315
if not os.access(location, os.W_OK):
316
raise CleoRuntimeError(f"No write access to backup location: {location}")
317
318
# Create backup...
319
```
320
321
### Exception Handling in Testing
322
323
```python
324
import pytest
325
from cleo.testers.command_tester import CommandTester
326
from cleo.exceptions import CleoUserError, CleoRuntimeError
327
328
def test_command_error_handling():
329
command = ProcessCommand()
330
tester = CommandTester(command)
331
332
# Test missing argument error
333
exit_code = tester.execute("")
334
assert exit_code == 1
335
assert "Filename is required" in tester.get_display()
336
337
# Test file not found error
338
exit_code = tester.execute("nonexistent.txt")
339
assert exit_code == 2
340
assert "File 'nonexistent.txt' not found" in tester.get_display()
341
342
def test_custom_exception_handling():
343
command = DatabaseCommand()
344
tester = CommandTester(command)
345
346
# Test invalid port
347
exit_code = tester.execute("--port 99999")
348
assert exit_code == 2
349
assert "Invalid value '99999' for port" in tester.get_display()
350
351
def test_exception_propagation():
352
"""Test that exceptions are properly caught and handled."""
353
command = FailingCommand()
354
tester = CommandTester(command)
355
356
# Should handle exception gracefully
357
exit_code = tester.execute("")
358
assert exit_code != 0 # Should indicate failure
359
assert "error" in tester.get_display().lower() # Should show error message
360
```
361
362
### Global Exception Handler
363
364
```python
365
import sys
366
import logging
367
from cleo.exceptions import CleoError
368
369
def setup_global_exception_handler():
370
"""Set up global exception handling for the application."""
371
372
def handle_exception(exc_type, exc_value, exc_traceback):
373
if isinstance(exc_value, CleoError):
374
# Cleo exceptions are handled by the application
375
return
376
377
# Log unexpected exceptions
378
logging.error(
379
"Uncaught exception",
380
exc_info=(exc_type, exc_value, exc_traceback)
381
)
382
383
# Show user-friendly message
384
print("An unexpected error occurred. Please check the logs for details.")
385
sys.exit(1)
386
387
sys.excepthook = handle_exception
388
389
# Use in main application
390
if __name__ == "__main__":
391
setup_global_exception_handler()
392
393
app = MyApplication()
394
try:
395
exit_code = app.run()
396
sys.exit(exit_code)
397
except KeyboardInterrupt:
398
print("\nOperation cancelled by user")
399
sys.exit(130)
400
```
401
402
### Error Reporting and Logging
403
404
```python
405
import logging
406
from cleo.exceptions import CleoError
407
408
class ReportingCommand(Command):
409
def __init__(self):
410
super().__init__()
411
self.logger = logging.getLogger(__name__)
412
413
def handle(self):
414
try:
415
# Command logic here
416
self.process_data()
417
418
except CleoUserError as e:
419
# User errors don't need full logging
420
self.line(f"<error>{e}</error>")
421
return e.exit_code or 1
422
423
except CleoRuntimeError as e:
424
# Runtime errors should be logged for debugging
425
self.logger.error(f"Runtime error in {self.name}: {e}")
426
self.line(f"<error>Operation failed: {e}</error>")
427
return e.exit_code or 1
428
429
except Exception as e:
430
# Unexpected errors need full logging
431
self.logger.exception(f"Unexpected error in {self.name}")
432
self.line("<error>An unexpected error occurred. Check logs for details.</error>")
433
return 1
434
435
def process_data(self):
436
# Processing logic that might raise exceptions
437
pass
438
```