0
# Hook System
1
2
The hook system provides extension points throughout the application lifecycle, enabling custom functionality injection at defined points in application execution. Hooks allow developers to extend and customize application behavior without modifying core framework code.
3
4
## Capabilities
5
6
### Hook Manager
7
8
Manages application hooks including hook definition, registration, and execution.
9
10
```python { .api }
11
class HookManager:
12
"""
13
Manages application hooks for extending functionality at defined points.
14
15
The hook manager provides centralized control over hook registration,
16
execution, and lifecycle management.
17
"""
18
19
def define(self, name: str) -> None:
20
"""
21
Define a new hook in the system.
22
23
Args:
24
name: Hook name to define
25
"""
26
27
def defined(self, name: str) -> bool:
28
"""
29
Check if a hook is defined.
30
31
Args:
32
name: Hook name to check
33
34
Returns:
35
True if hook is defined, False otherwise
36
"""
37
38
def register(self, name: str, func: Callable, weight: int = 0) -> None:
39
"""
40
Register a function to run when hook is executed.
41
42
Args:
43
name: Hook name to register for
44
func: Function to execute when hook runs
45
weight: Execution weight (lower weights run first)
46
"""
47
48
def run(self, name: str, app: 'App', *args: Any, **kwargs: Any) -> None:
49
"""
50
Execute all registered functions for a hook.
51
52
Args:
53
name: Hook name to execute
54
app: Application instance
55
*args: Additional arguments passed to hook functions
56
**kwargs: Additional keyword arguments passed to hook functions
57
"""
58
```
59
60
## Built-in Hook Points
61
62
The framework provides several built-in hooks for extending functionality:
63
64
- `pre_setup` - Before application setup
65
- `post_setup` - After application setup
66
- `pre_run` - Before application run
67
- `post_run` - After application run
68
- `pre_close` - Before application close
69
- `post_close` - After application close
70
- `signal` - When signals are caught
71
- `pre_render` - Before output rendering
72
- `post_render` - After output rendering
73
74
## Usage Examples
75
76
### Registering Hook Functions
77
78
```python
79
from cement import App, Controller, ex
80
81
def my_pre_setup_hook(app):
82
"""Hook function that runs before app setup."""
83
print('Running pre-setup initialization')
84
# Initialize custom components
85
app.custom_data = {'initialized': True}
86
87
def my_post_setup_hook(app):
88
"""Hook function that runs after app setup."""
89
print('Running post-setup configuration')
90
# Validate configuration
91
if not hasattr(app, 'custom_data'):
92
raise Exception('Custom data not initialized')
93
94
def my_pre_run_hook(app):
95
"""Hook function that runs before app execution."""
96
print(f'Starting application: {app._meta.label}')
97
# Log application start
98
app.log.info('Application execution started')
99
100
def my_post_run_hook(app):
101
"""Hook function that runs after app execution."""
102
print('Application execution completed')
103
# Cleanup or final logging
104
app.log.info('Application execution completed')
105
106
class BaseController(Controller):
107
class Meta:
108
label = 'base'
109
110
@ex(help='test command')
111
def test(self):
112
"""Test command to demonstrate hooks."""
113
print('Executing test command')
114
print(f'Custom data: {self.app.custom_data}')
115
116
class MyApp(App):
117
class Meta:
118
label = 'myapp'
119
base_controller = 'base'
120
handlers = [BaseController]
121
122
def load_hooks(app):
123
"""Load application hooks."""
124
app.hook.register('pre_setup', my_pre_setup_hook)
125
app.hook.register('post_setup', my_post_setup_hook)
126
app.hook.register('pre_run', my_pre_run_hook)
127
app.hook.register('post_run', my_post_run_hook)
128
129
with MyApp() as app:
130
load_hooks(app)
131
app.run()
132
```
133
134
### Hook Weights and Execution Order
135
136
```python
137
from cement import App
138
139
def first_hook(app):
140
"""Hook with lowest weight runs first."""
141
print('First hook (weight: -10)')
142
143
def middle_hook(app):
144
"""Hook with default weight."""
145
print('Middle hook (weight: 0)')
146
147
def last_hook(app):
148
"""Hook with highest weight runs last."""
149
print('Last hook (weight: 10)')
150
151
class MyApp(App):
152
class Meta:
153
label = 'myapp'
154
155
def load_hooks(app):
156
"""Load hooks with different weights."""
157
app.hook.register('pre_setup', first_hook, weight=-10)
158
app.hook.register('pre_setup', middle_hook, weight=0)
159
app.hook.register('pre_setup', last_hook, weight=10)
160
161
with MyApp() as app:
162
load_hooks(app)
163
app.setup()
164
app.run()
165
166
# Output:
167
# First hook (weight: -10)
168
# Middle hook (weight: 0)
169
# Last hook (weight: 10)
170
```
171
172
### Custom Hook Definition
173
174
```python
175
from cement import App, Controller, ex
176
177
def database_connect_hook(app, connection_string):
178
"""Custom hook for database connection."""
179
print(f'Connecting to database: {connection_string}')
180
# Simulate database connection
181
app.db_connected = True
182
183
def database_migrate_hook(app):
184
"""Custom hook for database migration."""
185
if not getattr(app, 'db_connected', False):
186
raise Exception('Database not connected')
187
print('Running database migrations')
188
app.db_migrated = True
189
190
def validate_database_hook(app):
191
"""Custom hook for database validation."""
192
if not getattr(app, 'db_migrated', False):
193
raise Exception('Database not migrated')
194
print('Database validation successful')
195
196
class DatabaseController(Controller):
197
class Meta:
198
label = 'database'
199
stacked_on = 'base'
200
stacked_type = 'nested'
201
202
@ex(help='initialize database')
203
def init(self):
204
"""Initialize database system."""
205
# Define and run custom hooks
206
if not self.app.hook.defined('database_connect'):
207
self.app.hook.define('database_connect')
208
if not self.app.hook.defined('database_migrate'):
209
self.app.hook.define('database_migrate')
210
if not self.app.hook.defined('database_validate'):
211
self.app.hook.define('database_validate')
212
213
# Run hooks in sequence
214
self.app.hook.run('database_connect', self.app, 'postgresql://localhost/myapp')
215
self.app.hook.run('database_migrate', self.app)
216
self.app.hook.run('database_validate', self.app)
217
218
print('Database initialization completed')
219
220
class BaseController(Controller):
221
class Meta:
222
label = 'base'
223
224
class MyApp(App):
225
class Meta:
226
label = 'myapp'
227
base_controller = 'base'
228
handlers = [BaseController, DatabaseController]
229
230
def load_hooks(app):
231
"""Load custom database hooks."""
232
# Register hooks (they will be defined when needed)
233
app.hook.register('database_connect', database_connect_hook)
234
app.hook.register('database_migrate', database_migrate_hook)
235
app.hook.register('database_validate', validate_database_hook)
236
237
with MyApp() as app:
238
load_hooks(app)
239
app.run()
240
241
# Usage:
242
# myapp database init
243
```
244
245
### Extension Loading Hooks
246
247
```python
248
from cement import App
249
250
def load_redis_extension(app):
251
"""Hook to load Redis extension if needed."""
252
if app.config.get('myapp', 'cache_handler') == 'redis':
253
print('Loading Redis extension for caching')
254
app.extend('redis', app)
255
256
def load_jinja2_extension(app):
257
"""Hook to load Jinja2 extension if needed."""
258
if app.config.get('myapp', 'template_handler') == 'jinja2':
259
print('Loading Jinja2 extension for templating')
260
app.extend('jinja2', app)
261
262
def validate_extensions(app):
263
"""Hook to validate required extensions are loaded."""
264
required_extensions = ['colorlog', 'yaml']
265
for ext in required_extensions:
266
if ext not in app.loaded_extensions:
267
print(f'Warning: Required extension {ext} not loaded')
268
269
class MyApp(App):
270
class Meta:
271
label = 'myapp'
272
extensions = ['colorlog', 'yaml']
273
274
def load_hooks(app):
275
"""Load extension management hooks."""
276
app.hook.register('post_setup', load_redis_extension, weight=-5)
277
app.hook.register('post_setup', load_jinja2_extension, weight=-5)
278
app.hook.register('post_setup', validate_extensions, weight=5)
279
280
with MyApp() as app:
281
load_hooks(app)
282
app.setup()
283
app.run()
284
```
285
286
### Configuration Validation Hooks
287
288
```python
289
from cement import App, init_defaults
290
291
CONFIG = init_defaults('myapp', 'database')
292
CONFIG['myapp']['environment'] = 'development'
293
CONFIG['database']['host'] = 'localhost'
294
295
def validate_environment_config(app):
296
"""Validate environment-specific configuration."""
297
env = app.config.get('myapp', 'environment')
298
299
if env == 'production':
300
required_keys = ['database.host', 'database.password', 'database.ssl']
301
for key_path in required_keys:
302
section, key = key_path.split('.')
303
try:
304
value = app.config.get(section, key)
305
if not value:
306
raise ValueError(f'Production environment requires {key_path}')
307
except KeyError:
308
raise ValueError(f'Production environment requires {key_path}')
309
print('Production configuration validated')
310
else:
311
print(f'Configuration validated for {env} environment')
312
313
def setup_logging_config(app):
314
"""Configure logging based on environment."""
315
env = app.config.get('myapp', 'environment')
316
317
if env == 'production':
318
app.log.set_level('WARNING')
319
print('Logging set to WARNING level for production')
320
else:
321
app.log.set_level('DEBUG')
322
print('Logging set to DEBUG level for development')
323
324
class MyApp(App):
325
class Meta:
326
label = 'myapp'
327
config_defaults = CONFIG
328
329
def load_hooks(app):
330
"""Load configuration hooks."""
331
app.hook.register('post_setup', validate_environment_config, weight=-10)
332
app.hook.register('post_setup', setup_logging_config, weight=-5)
333
334
with MyApp() as app:
335
load_hooks(app)
336
app.setup()
337
app.run()
338
```
339
340
### Signal Handling Hooks
341
342
```python
343
from cement import App
344
import signal
345
346
def cleanup_resources(app, signum, frame):
347
"""Hook to cleanup resources when signal received."""
348
print(f'Received signal {signum}, cleaning up resources')
349
350
# Close database connections
351
if hasattr(app, 'db_connection'):
352
app.db_connection.close()
353
print('Database connection closed')
354
355
# Save application state
356
if hasattr(app, 'state'):
357
with open('/tmp/app_state.json', 'w') as f:
358
json.dump(app.state, f)
359
print('Application state saved')
360
361
def log_signal_received(app, signum, frame):
362
"""Hook to log signal reception."""
363
signal_names = {
364
signal.SIGTERM: 'SIGTERM',
365
signal.SIGINT: 'SIGINT',
366
signal.SIGHUP: 'SIGHUP'
367
}
368
signal_name = signal_names.get(signum, f'Signal {signum}')
369
app.log.warning(f'Received {signal_name}, initiating shutdown')
370
371
class MyApp(App):
372
class Meta:
373
label = 'myapp'
374
catch_signals = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]
375
376
def load_hooks(app):
377
"""Load signal handling hooks."""
378
app.hook.register('signal', cleanup_resources, weight=-10)
379
app.hook.register('signal', log_signal_received, weight=-5)
380
381
with MyApp() as app:
382
load_hooks(app)
383
app.setup()
384
385
# Simulate some application state
386
app.state = {'last_action': 'startup', 'user_count': 0}
387
388
app.run()
389
```
390
391
### Render Hooks for Output Processing
392
393
```python
394
from cement import App, Controller, ex
395
import json
396
397
def add_metadata_to_output(app, data, rendered_output):
398
"""Hook to add metadata to rendered output."""
399
if isinstance(data, dict):
400
# Add timestamp and version metadata
401
import datetime
402
data['_metadata'] = {
403
'timestamp': datetime.datetime.now().isoformat(),
404
'version': app.config.get('myapp', 'version', '1.0.0'),
405
'environment': app.config.get('myapp', 'environment', 'development')
406
}
407
408
def log_render_operation(app, data, rendered_output):
409
"""Hook to log render operations."""
410
data_size = len(str(data))
411
output_size = len(rendered_output)
412
app.log.debug(f'Rendered {data_size} bytes of data to {output_size} bytes of output')
413
414
class BaseController(Controller):
415
class Meta:
416
label = 'base'
417
418
@ex(help='get user information')
419
def user_info(self):
420
"""Get user information with metadata."""
421
user_data = {
422
'id': 123,
423
'name': 'John Doe',
424
'email': 'john@example.com',
425
'status': 'active'
426
}
427
428
# Render with hooks
429
output = self.app.render(user_data)
430
print(output)
431
432
class MyApp(App):
433
class Meta:
434
label = 'myapp'
435
base_controller = 'base'
436
handlers = [BaseController]
437
438
def load_hooks(app):
439
"""Load render hooks."""
440
app.hook.register('pre_render', add_metadata_to_output)
441
app.hook.register('post_render', log_render_operation)
442
443
with MyApp() as app:
444
load_hooks(app)
445
app.run()
446
447
# Usage:
448
# myapp user-info
449
```