0
# Trait Linking
1
2
Functions for linking traits between objects, enabling synchronization of trait values across different instances. This provides powerful mechanisms for building reactive interfaces and synchronized data structures.
3
4
## Capabilities
5
6
### Bidirectional Linking
7
8
Creates two-way synchronization between traits on different objects.
9
10
```python { .api }
11
class link:
12
"""
13
Bidirectional link between traits on different objects.
14
15
When either linked trait changes, the other automatically
16
updates to match. Changes propagate in both directions.
17
"""
18
19
def __init__(self, source, target):
20
"""
21
Create bidirectional link between traits.
22
23
Parameters:
24
- source: tuple - (object, 'trait_name') pair for first trait
25
- target: tuple - (object, 'trait_name') pair for second trait
26
27
Returns:
28
link - Link object that can be used to unlink later
29
"""
30
31
def unlink(self):
32
"""
33
Remove the bidirectional link.
34
35
After calling this method, changes to either trait will
36
no longer propagate to the other.
37
"""
38
```
39
40
### Directional Linking
41
42
Creates one-way synchronization from source to target trait.
43
44
```python { .api }
45
class directional_link:
46
"""
47
Directional link from source to target trait.
48
49
When the source trait changes, the target automatically updates.
50
Changes to the target do not affect the source.
51
"""
52
53
def __init__(self, source, target, transform=None):
54
"""
55
Create directional link from source to target.
56
57
Parameters:
58
- source: tuple - (object, 'trait_name') pair for source trait
59
- target: tuple - (object, 'trait_name') pair for target trait
60
- transform: callable|None - Optional function to transform values
61
62
The transform function receives the source value and should
63
return the value to set on the target trait.
64
65
Returns:
66
directional_link - Link object that can be used to unlink later
67
"""
68
69
def unlink(self):
70
"""
71
Remove the directional link.
72
73
After calling this method, changes to the source trait will
74
no longer propagate to the target.
75
"""
76
77
# Alias for directional_link
78
dlink = directional_link
79
```
80
81
## Usage Examples
82
83
### Basic Bidirectional Linking
84
85
```python
86
from traitlets import HasTraits, Unicode, Int, link
87
88
class Model(HasTraits):
89
name = Unicode()
90
value = Int()
91
92
class View(HasTraits):
93
display_name = Unicode()
94
display_value = Int()
95
96
# Create instances
97
model = Model(name="Initial", value=42)
98
view = View()
99
100
# Create bidirectional links
101
name_link = link((model, 'name'), (view, 'display_name'))
102
value_link = link((model, 'value'), (view, 'display_value'))
103
104
print(view.display_name) # "Initial" (synchronized from model)
105
print(view.display_value) # 42 (synchronized from model)
106
107
# Changes propagate both ways
108
model.name = "Updated"
109
print(view.display_name) # "Updated"
110
111
view.display_value = 100
112
print(model.value) # 100
113
114
# Clean up links
115
name_link.unlink()
116
value_link.unlink()
117
```
118
119
### Directional Linking with Transform
120
121
```python
122
from traitlets import HasTraits, Unicode, Float, directional_link
123
124
class TemperatureSensor(HasTraits):
125
celsius = Float()
126
127
class Display(HasTraits):
128
fahrenheit = Unicode()
129
kelvin = Unicode()
130
131
def celsius_to_fahrenheit(celsius):
132
return f"{celsius * 9/5 + 32:.1f}°F"
133
134
def celsius_to_kelvin(celsius):
135
return f"{celsius + 273.15:.1f}K"
136
137
# Create instances
138
sensor = TemperatureSensor()
139
display = Display()
140
141
# Create directional links with transforms
142
fahrenheit_link = directional_link(
143
(sensor, 'celsius'),
144
(display, 'fahrenheit'),
145
transform=celsius_to_fahrenheit
146
)
147
148
kelvin_link = directional_link(
149
(sensor, 'celsius'),
150
(display, 'kelvin'),
151
transform=celsius_to_kelvin
152
)
153
154
# Changes in sensor update display
155
sensor.celsius = 25.0
156
print(display.fahrenheit) # "77.0°F"
157
print(display.kelvin) # "298.2K"
158
159
sensor.celsius = 0.0
160
print(display.fahrenheit) # "32.0°F"
161
print(display.kelvin) # "273.2K"
162
163
# Changes in display don't affect sensor (directional only)
164
display.fahrenheit = "100.0°F"
165
print(sensor.celsius) # Still 0.0
166
```
167
168
### Multiple Object Synchronization
169
170
```python
171
from traitlets import HasTraits, Unicode, Bool, link, directional_link
172
173
class ConfigA(HasTraits):
174
theme = Unicode(default_value="light")
175
debug = Bool(default_value=False)
176
177
class ConfigB(HasTraits):
178
ui_theme = Unicode()
179
verbose = Bool()
180
181
class ConfigC(HasTraits):
182
color_scheme = Unicode()
183
debug_mode = Bool()
184
185
# Create instances
186
config_a = ConfigA()
187
config_b = ConfigB()
188
config_c = ConfigC()
189
190
# Create bidirectional links for theme synchronization
191
theme_link_ab = link((config_a, 'theme'), (config_b, 'ui_theme'))
192
theme_link_ac = link((config_a, 'theme'), (config_c, 'color_scheme'))
193
194
# Create directional links for debug mode
195
debug_link_ab = directional_link((config_a, 'debug'), (config_b, 'verbose'))
196
debug_link_ac = directional_link((config_a, 'debug'), (config_c, 'debug_mode'))
197
198
# All themes synchronized
199
config_a.theme = "dark"
200
print(config_b.ui_theme) # "dark"
201
print(config_c.color_scheme) # "dark"
202
203
config_c.color_scheme = "high_contrast"
204
print(config_a.theme) # "high_contrast"
205
print(config_b.ui_theme) # "high_contrast"
206
207
# Debug flows from A to B and C only
208
config_a.debug = True
209
print(config_b.verbose) # True
210
print(config_c.debug_mode) # True
211
212
config_b.verbose = False # Doesn't affect config_a.debug
213
print(config_a.debug) # Still True
214
```
215
216
### Dynamic Linking and Unlinking
217
218
```python
219
from traitlets import HasTraits, Int, observe, link
220
221
class Counter(HasTraits):
222
value = Int()
223
224
class Display(HasTraits):
225
count = Int()
226
227
@observe('count')
228
def _count_changed(self, change):
229
print(f"Display updated: {change['new']}")
230
231
counter1 = Counter()
232
counter2 = Counter()
233
display = Display()
234
235
# Initially link counter1 to display
236
current_link = link((counter1, 'value'), (display, 'count'))
237
238
counter1.value = 10 # Display updated: 10
239
240
# Switch to counter2
241
current_link.unlink()
242
current_link = link((counter2, 'value'), (display, 'count'))
243
244
counter1.value = 20 # No update (unlinked)
245
counter2.value = 30 # Display updated: 30
246
247
# Multiple displays
248
display2 = Display()
249
link2 = link((counter2, 'value'), (display2, 'count'))
250
251
counter2.value = 40 # Both displays update
252
```
253
254
### Complex Transform Functions
255
256
```python
257
from traitlets import HasTraits, List, Unicode, directional_link
258
259
class DataSource(HasTraits):
260
items = List()
261
262
class FormattedDisplay(HasTraits):
263
formatted_text = Unicode()
264
265
def format_list(items):
266
if not items:
267
return "No items"
268
elif len(items) == 1:
269
return f"1 item: {items[0]}"
270
else:
271
return f"{len(items)} items: {', '.join(str(item) for item in items[:3])}" + \
272
("..." if len(items) > 3 else "")
273
274
# Create instances
275
source = DataSource()
276
display = FormattedDisplay()
277
278
# Link with formatting transform
279
formatter_link = directional_link(
280
(source, 'items'),
281
(display, 'formatted_text'),
282
transform=format_list
283
)
284
285
source.items = []
286
print(display.formatted_text) # "No items"
287
288
source.items = ["apple"]
289
print(display.formatted_text) # "1 item: apple"
290
291
source.items = ["apple", "banana", "cherry"]
292
print(display.formatted_text) # "3 items: apple, banana, cherry"
293
294
source.items = ["apple", "banana", "cherry", "date", "elderberry"]
295
print(display.formatted_text) # "5 items: apple, banana, cherry..."
296
```
297
298
### Linking with Validation
299
300
```python
301
from traitlets import HasTraits, Int, TraitError, validate, link
302
303
class Source(HasTraits):
304
value = Int()
305
306
class Target(HasTraits):
307
constrained_value = Int()
308
309
@validate('constrained_value')
310
def _validate_constrained_value(self, proposal):
311
value = proposal['value']
312
if value < 0:
313
raise TraitError("Value must be non-negative")
314
if value > 100:
315
return 100 # Clamp to maximum
316
return value
317
318
source = Source()
319
target = Target()
320
321
# Link with validation on target
322
value_link = link((source, 'value'), (target, 'constrained_value'))
323
324
source.value = 50
325
print(target.constrained_value) # 50
326
327
source.value = 150
328
print(target.constrained_value) # 100 (clamped)
329
330
# This would cause validation error if set directly on target
331
# target.constrained_value = -10 # TraitError
332
# But through linking, negative values from source are handled
333
try:
334
source.value = -10
335
except TraitError as e:
336
print(f"Validation error: {e}")
337
```
338
339
### Conditional Linking
340
341
```python
342
from traitlets import HasTraits, Bool, Unicode, observe
343
344
class ConditionalLinker(HasTraits):
345
enabled = Bool(default_value=True)
346
347
def __init__(self, source, target, **kwargs):
348
super().__init__(**kwargs)
349
self.source = source
350
self.target = target
351
self.current_link = None
352
self._update_link()
353
354
@observe('enabled')
355
def _update_link(self, change=None):
356
# Remove existing link
357
if self.current_link:
358
self.current_link.unlink()
359
self.current_link = None
360
361
# Create new link if enabled
362
if self.enabled:
363
from traitlets import link
364
self.current_link = link(self.source, self.target)
365
366
class Model(HasTraits):
367
data = Unicode()
368
369
class View(HasTraits):
370
display = Unicode()
371
372
model = Model()
373
view = View()
374
375
# Conditional linking
376
linker = ConditionalLinker((model, 'data'), (view, 'display'))
377
378
model.data = "test1"
379
print(view.display) # "test1" (linked)
380
381
# Disable linking
382
linker.enabled = False
383
model.data = "test2"
384
print(view.display) # Still "test1" (not linked)
385
386
# Re-enable linking
387
linker.enabled = True
388
model.data = "test3"
389
print(view.display) # "test3" (linked again)
390
```