A pure Python library providing typed attributes with validation, change notifications, and configuration management for the IPython/Jupyter ecosystem.
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.
Creates two-way synchronization between traits on different objects.
class link:
"""
Bidirectional link between traits on different objects.
When either linked trait changes, the other automatically
updates to match. Changes propagate in both directions.
"""
def __init__(self, source, target):
"""
Create bidirectional link between traits.
Parameters:
- source: tuple - (object, 'trait_name') pair for first trait
- target: tuple - (object, 'trait_name') pair for second trait
Returns:
link - Link object that can be used to unlink later
"""
def unlink(self):
"""
Remove the bidirectional link.
After calling this method, changes to either trait will
no longer propagate to the other.
"""Creates one-way synchronization from source to target trait.
class directional_link:
"""
Directional link from source to target trait.
When the source trait changes, the target automatically updates.
Changes to the target do not affect the source.
"""
def __init__(self, source, target, transform=None):
"""
Create directional link from source to target.
Parameters:
- source: tuple - (object, 'trait_name') pair for source trait
- target: tuple - (object, 'trait_name') pair for target trait
- transform: callable|None - Optional function to transform values
The transform function receives the source value and should
return the value to set on the target trait.
Returns:
directional_link - Link object that can be used to unlink later
"""
def unlink(self):
"""
Remove the directional link.
After calling this method, changes to the source trait will
no longer propagate to the target.
"""
# Alias for directional_link
dlink = directional_linkfrom traitlets import HasTraits, Unicode, Int, link
class Model(HasTraits):
name = Unicode()
value = Int()
class View(HasTraits):
display_name = Unicode()
display_value = Int()
# Create instances
model = Model(name="Initial", value=42)
view = View()
# Create bidirectional links
name_link = link((model, 'name'), (view, 'display_name'))
value_link = link((model, 'value'), (view, 'display_value'))
print(view.display_name) # "Initial" (synchronized from model)
print(view.display_value) # 42 (synchronized from model)
# Changes propagate both ways
model.name = "Updated"
print(view.display_name) # "Updated"
view.display_value = 100
print(model.value) # 100
# Clean up links
name_link.unlink()
value_link.unlink()from traitlets import HasTraits, Unicode, Float, directional_link
class TemperatureSensor(HasTraits):
celsius = Float()
class Display(HasTraits):
fahrenheit = Unicode()
kelvin = Unicode()
def celsius_to_fahrenheit(celsius):
return f"{celsius * 9/5 + 32:.1f}°F"
def celsius_to_kelvin(celsius):
return f"{celsius + 273.15:.1f}K"
# Create instances
sensor = TemperatureSensor()
display = Display()
# Create directional links with transforms
fahrenheit_link = directional_link(
(sensor, 'celsius'),
(display, 'fahrenheit'),
transform=celsius_to_fahrenheit
)
kelvin_link = directional_link(
(sensor, 'celsius'),
(display, 'kelvin'),
transform=celsius_to_kelvin
)
# Changes in sensor update display
sensor.celsius = 25.0
print(display.fahrenheit) # "77.0°F"
print(display.kelvin) # "298.2K"
sensor.celsius = 0.0
print(display.fahrenheit) # "32.0°F"
print(display.kelvin) # "273.2K"
# Changes in display don't affect sensor (directional only)
display.fahrenheit = "100.0°F"
print(sensor.celsius) # Still 0.0from traitlets import HasTraits, Unicode, Bool, link, directional_link
class ConfigA(HasTraits):
theme = Unicode(default_value="light")
debug = Bool(default_value=False)
class ConfigB(HasTraits):
ui_theme = Unicode()
verbose = Bool()
class ConfigC(HasTraits):
color_scheme = Unicode()
debug_mode = Bool()
# Create instances
config_a = ConfigA()
config_b = ConfigB()
config_c = ConfigC()
# Create bidirectional links for theme synchronization
theme_link_ab = link((config_a, 'theme'), (config_b, 'ui_theme'))
theme_link_ac = link((config_a, 'theme'), (config_c, 'color_scheme'))
# Create directional links for debug mode
debug_link_ab = directional_link((config_a, 'debug'), (config_b, 'verbose'))
debug_link_ac = directional_link((config_a, 'debug'), (config_c, 'debug_mode'))
# All themes synchronized
config_a.theme = "dark"
print(config_b.ui_theme) # "dark"
print(config_c.color_scheme) # "dark"
config_c.color_scheme = "high_contrast"
print(config_a.theme) # "high_contrast"
print(config_b.ui_theme) # "high_contrast"
# Debug flows from A to B and C only
config_a.debug = True
print(config_b.verbose) # True
print(config_c.debug_mode) # True
config_b.verbose = False # Doesn't affect config_a.debug
print(config_a.debug) # Still Truefrom traitlets import HasTraits, Int, observe, link
class Counter(HasTraits):
value = Int()
class Display(HasTraits):
count = Int()
@observe('count')
def _count_changed(self, change):
print(f"Display updated: {change['new']}")
counter1 = Counter()
counter2 = Counter()
display = Display()
# Initially link counter1 to display
current_link = link((counter1, 'value'), (display, 'count'))
counter1.value = 10 # Display updated: 10
# Switch to counter2
current_link.unlink()
current_link = link((counter2, 'value'), (display, 'count'))
counter1.value = 20 # No update (unlinked)
counter2.value = 30 # Display updated: 30
# Multiple displays
display2 = Display()
link2 = link((counter2, 'value'), (display2, 'count'))
counter2.value = 40 # Both displays updatefrom traitlets import HasTraits, List, Unicode, directional_link
class DataSource(HasTraits):
items = List()
class FormattedDisplay(HasTraits):
formatted_text = Unicode()
def format_list(items):
if not items:
return "No items"
elif len(items) == 1:
return f"1 item: {items[0]}"
else:
return f"{len(items)} items: {', '.join(str(item) for item in items[:3])}" + \
("..." if len(items) > 3 else "")
# Create instances
source = DataSource()
display = FormattedDisplay()
# Link with formatting transform
formatter_link = directional_link(
(source, 'items'),
(display, 'formatted_text'),
transform=format_list
)
source.items = []
print(display.formatted_text) # "No items"
source.items = ["apple"]
print(display.formatted_text) # "1 item: apple"
source.items = ["apple", "banana", "cherry"]
print(display.formatted_text) # "3 items: apple, banana, cherry"
source.items = ["apple", "banana", "cherry", "date", "elderberry"]
print(display.formatted_text) # "5 items: apple, banana, cherry..."from traitlets import HasTraits, Int, TraitError, validate, link
class Source(HasTraits):
value = Int()
class Target(HasTraits):
constrained_value = Int()
@validate('constrained_value')
def _validate_constrained_value(self, proposal):
value = proposal['value']
if value < 0:
raise TraitError("Value must be non-negative")
if value > 100:
return 100 # Clamp to maximum
return value
source = Source()
target = Target()
# Link with validation on target
value_link = link((source, 'value'), (target, 'constrained_value'))
source.value = 50
print(target.constrained_value) # 50
source.value = 150
print(target.constrained_value) # 100 (clamped)
# This would cause validation error if set directly on target
# target.constrained_value = -10 # TraitError
# But through linking, negative values from source are handled
try:
source.value = -10
except TraitError as e:
print(f"Validation error: {e}")from traitlets import HasTraits, Bool, Unicode, observe
class ConditionalLinker(HasTraits):
enabled = Bool(default_value=True)
def __init__(self, source, target, **kwargs):
super().__init__(**kwargs)
self.source = source
self.target = target
self.current_link = None
self._update_link()
@observe('enabled')
def _update_link(self, change=None):
# Remove existing link
if self.current_link:
self.current_link.unlink()
self.current_link = None
# Create new link if enabled
if self.enabled:
from traitlets import link
self.current_link = link(self.source, self.target)
class Model(HasTraits):
data = Unicode()
class View(HasTraits):
display = Unicode()
model = Model()
view = View()
# Conditional linking
linker = ConditionalLinker((model, 'data'), (view, 'display'))
model.data = "test1"
print(view.display) # "test1" (linked)
# Disable linking
linker.enabled = False
model.data = "test2"
print(view.display) # Still "test1" (not linked)
# Re-enable linking
linker.enabled = True
model.data = "test3"
print(view.display) # "test3" (linked again)Install with Tessl CLI
npx tessl i tessl/pypi-traitlets