0
# Observers and Decorators
1
2
Decorators and handlers for observing trait changes, validating values, and providing dynamic defaults. These enable reactive programming patterns and sophisticated validation logic in trait-based classes.
3
4
## Capabilities
5
6
### Observer Decorators
7
8
Decorators for registering functions to respond to trait changes.
9
10
```python { .api }
11
def observe(*names, type='change'):
12
"""
13
Decorator to observe trait changes.
14
15
Registers a method to be called when specified traits change.
16
The decorated method receives a change dictionary with details
17
about the change event.
18
19
Parameters:
20
- *names: str - Trait names to observe (empty for all traits)
21
- type: str - Type of change to observe ('change' is most common)
22
23
Returns:
24
function - Decorator function
25
26
Usage:
27
@observe('trait_name', 'other_trait')
28
def _trait_changed(self, change):
29
# Handle change
30
pass
31
"""
32
33
def observe_compat(*names, type='change'):
34
"""
35
Backward-compatibility decorator for observers.
36
37
Provides compatibility with older observer patterns while
38
using the new observe decorator internally.
39
40
Parameters:
41
- *names: str - Trait names to observe
42
- type: str - Type of change to observe
43
44
Returns:
45
function - Decorator function
46
"""
47
```
48
49
### Validator Decorators
50
51
Decorators for registering cross-validation functions.
52
53
```python { .api }
54
def validate(*names):
55
"""
56
Decorator to register cross-validator.
57
58
Registers a method to validate trait values before they are set.
59
The validator can modify the value or raise TraitError to reject it.
60
61
Parameters:
62
- *names: str - Trait names to validate
63
64
Returns:
65
function - Decorator function
66
67
Usage:
68
@validate('trait_name')
69
def _validate_trait(self, proposal):
70
# proposal is dict with 'value' key
71
value = proposal['value']
72
# Validate and possibly modify value
73
return validated_value
74
"""
75
```
76
77
### Default Value Decorators
78
79
Decorators for providing dynamic default values.
80
81
```python { .api }
82
def default(name):
83
"""
84
Decorator for dynamic default value generator.
85
86
Registers a method to provide default values for traits when
87
they are first accessed and no value has been set.
88
89
Parameters:
90
- name: str - Trait name to provide default for
91
92
Returns:
93
function - Decorator function
94
95
Usage:
96
@default('trait_name')
97
def _default_trait(self):
98
return computed_default_value
99
"""
100
```
101
102
### Event Handler Classes
103
104
Classes representing different types of event handlers.
105
106
```python { .api }
107
class EventHandler:
108
"""
109
Base class for event handlers.
110
111
Provides common functionality for all types of event handlers
112
including observer, validator, and default handlers.
113
"""
114
115
class ObserveHandler(EventHandler):
116
"""
117
Handler for observe decorator.
118
119
Manages observation of trait changes and dispatching
120
to registered handler methods.
121
"""
122
123
class ValidateHandler(EventHandler):
124
"""
125
Handler for validate decorator.
126
127
Manages validation of trait values before assignment
128
and dispatching to registered validator methods.
129
"""
130
131
class DefaultHandler(EventHandler):
132
"""
133
Handler for default decorator.
134
135
Manages generation of default values for traits
136
and dispatching to registered default methods.
137
"""
138
```
139
140
## Usage Examples
141
142
### Basic Change Observation
143
144
```python
145
from traitlets import HasTraits, Unicode, Int, observe
146
147
class Person(HasTraits):
148
name = Unicode()
149
age = Int()
150
151
@observe('name')
152
def _name_changed(self, change):
153
print(f"Name changed from '{change['old']}' to '{change['new']}'")
154
155
@observe('age')
156
def _age_changed(self, change):
157
print(f"Age changed from {change['old']} to {change['new']}")
158
159
@observe('name', 'age')
160
def _any_change(self, change):
161
print(f"Trait '{change['name']}' changed on {change['owner']}")
162
163
person = Person()
164
person.name = "Alice" # Triggers _name_changed and _any_change
165
person.age = 30 # Triggers _age_changed and _any_change
166
person.name = "Bob" # Triggers _name_changed and _any_change
167
```
168
169
### Change Event Details
170
171
```python
172
from traitlets import HasTraits, Unicode, observe
173
174
class Example(HasTraits):
175
value = Unicode()
176
177
@observe('value')
178
def _value_changed(self, change):
179
print("Change event details:")
180
print(f" name: {change['name']}") # 'value'
181
print(f" old: {change['old']}") # Previous value
182
print(f" new: {change['new']}") # New value
183
print(f" owner: {change['owner']}") # Self
184
print(f" type: {change['type']}") # 'change'
185
186
example = Example()
187
example.value = "initial" # old=u'', new='initial'
188
example.value = "updated" # old='initial', new='updated'
189
```
190
191
### Cross-Validation
192
193
```python
194
from traitlets import HasTraits, Int, TraitError, validate
195
196
class Rectangle(HasTraits):
197
width = Int(min=0)
198
height = Int(min=0)
199
max_area = Int(default_value=1000)
200
201
@validate('width', 'height')
202
def _validate_dimensions(self, proposal):
203
value = proposal['value']
204
trait_name = proposal['trait'].name
205
206
# Get the other dimension
207
if trait_name == 'width':
208
other_dim = self.height
209
else:
210
other_dim = self.width
211
212
# Check if area would exceed maximum
213
if value * other_dim > self.max_area:
214
raise TraitError(f"Area would exceed maximum of {self.max_area}")
215
216
return value
217
218
rect = Rectangle(max_area=100)
219
rect.width = 10
220
rect.height = 8 # 10 * 8 = 80, within limit
221
222
# rect.height = 15 # Would raise TraitError (10 * 15 = 150 > 100)
223
```
224
225
### Dynamic Defaults
226
227
```python
228
import time
229
from traitlets import HasTraits, Unicode, Float, default
230
231
class LogEntry(HasTraits):
232
message = Unicode()
233
timestamp = Float()
234
hostname = Unicode()
235
236
@default('timestamp')
237
def _default_timestamp(self):
238
return time.time()
239
240
@default('hostname')
241
def _default_hostname(self):
242
import socket
243
return socket.gethostname()
244
245
# Each instance gets current timestamp and hostname
246
entry1 = LogEntry(message="First log")
247
time.sleep(0.1)
248
entry2 = LogEntry(message="Second log")
249
250
print(entry1.timestamp != entry2.timestamp) # True - different times
251
print(entry1.hostname == entry2.hostname) # True - same host
252
```
253
254
### Conditional Default Values
255
256
```python
257
from traitlets import HasTraits, Unicode, Bool, default
258
259
class User(HasTraits):
260
username = Unicode()
261
email = Unicode()
262
is_admin = Bool(default_value=False)
263
display_name = Unicode()
264
265
@default('display_name')
266
def _default_display_name(self):
267
if self.email:
268
return self.email.split('@')[0]
269
elif self.username:
270
return self.username.title()
271
else:
272
return "Anonymous"
273
274
user1 = User(email="alice@example.com")
275
print(user1.display_name) # "alice"
276
277
user2 = User(username="bob_smith")
278
print(user2.display_name) # "Bob_Smith"
279
280
user3 = User()
281
print(user3.display_name) # "Anonymous"
282
```
283
284
### Validation with Modification
285
286
```python
287
from traitlets import HasTraits, Unicode, validate
288
289
class NormalizedText(HasTraits):
290
text = Unicode()
291
292
@validate('text')
293
def _validate_text(self, proposal):
294
value = proposal['value']
295
296
# Normalize whitespace and case
297
normalized = ' '.join(value.split()).lower()
298
299
# Remove forbidden characters
300
forbidden = ['<', '>', '&']
301
for char in forbidden:
302
normalized = normalized.replace(char, '')
303
304
return normalized
305
306
text_obj = NormalizedText()
307
text_obj.text = " Hello WORLD <script> "
308
print(text_obj.text) # "hello world script"
309
```
310
311
### Observer for All Traits
312
313
```python
314
from traitlets import HasTraits, Unicode, Int, observe, All
315
316
class Monitored(HasTraits):
317
name = Unicode()
318
value = Int()
319
description = Unicode()
320
321
@observe(All) # Observe all traits
322
def _any_trait_changed(self, change):
323
print(f"Any trait changed: {change['name']} = {change['new']}")
324
325
@observe(All, type='change') # Explicit change type
326
def _log_changes(self, change):
327
import datetime
328
timestamp = datetime.datetime.now().isoformat()
329
print(f"[{timestamp}] {change['name']}: {change['old']} -> {change['new']}")
330
331
obj = Monitored()
332
obj.name = "test" # Triggers both observers
333
obj.value = 42 # Triggers both observers
334
obj.description = "demo" # Triggers both observers
335
```
336
337
### Complex Validation Logic
338
339
```python
340
from traitlets import HasTraits, Unicode, Int, List, TraitError, validate, observe
341
342
class Project(HasTraits):
343
name = Unicode()
344
priority = Int(min=1, max=5)
345
tags = List(Unicode())
346
assignees = List(Unicode())
347
348
@validate('name')
349
def _validate_name(self, proposal):
350
name = proposal['value']
351
352
# Must be non-empty and alphanumeric with underscores
353
if not name or not name.replace('_', '').isalnum():
354
raise TraitError("Name must be alphanumeric with underscores only")
355
356
return name.lower() # Normalize to lowercase
357
358
@validate('tags')
359
def _validate_tags(self, proposal):
360
tags = proposal['value']
361
362
# Remove duplicates and normalize
363
normalized_tags = list(set(tag.lower().strip() for tag in tags))
364
365
# Limit to 5 tags maximum
366
if len(normalized_tags) > 5:
367
raise TraitError("Maximum 5 tags allowed")
368
369
return normalized_tags
370
371
@observe('priority')
372
def _priority_changed(self, change):
373
if change['new'] >= 4:
374
print(f"High priority project: {self.name}")
375
376
project = Project()
377
project.name = "My_Project_123" # Becomes "my_project_123"
378
project.priority = 4 # Triggers high priority message
379
project.tags = ["Python", "WEB", "python", "api", "REST"] # Normalized and deduplicated
380
print(project.tags) # ['python', 'web', 'api', 'rest']
381
```