CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-traitlets

A pure Python library providing typed attributes with validation, change notifications, and configuration management for the IPython/Jupyter ecosystem.

Overview
Eval results
Files

linking.mddocs/

Trait Linking

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.

Capabilities

Bidirectional Linking

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.
        """

Directional Linking

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_link

Usage Examples

Basic Bidirectional Linking

from 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()

Directional Linking with Transform

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.0

Multiple Object Synchronization

from 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 True

Dynamic Linking and Unlinking

from 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 update

Complex Transform Functions

from 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..."

Linking with Validation

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}")

Conditional Linking

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

docs

advanced-types.md

basic-types.md

configuration.md

container-types.md

core-traits.md

index.md

linking.md

observers.md

tile.json