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

observers.mddocs/

Observers and Decorators

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.

Capabilities

Observer Decorators

Decorators for registering functions to respond to trait changes.

def observe(*names, type='change'):
    """
    Decorator to observe trait changes.
    
    Registers a method to be called when specified traits change.
    The decorated method receives a change dictionary with details
    about the change event.
    
    Parameters:
    - *names: str - Trait names to observe (empty for all traits)
    - type: str - Type of change to observe ('change' is most common)
    
    Returns:
    function - Decorator function
    
    Usage:
    @observe('trait_name', 'other_trait')
    def _trait_changed(self, change):
        # Handle change
        pass
    """

def observe_compat(*names, type='change'):
    """
    Backward-compatibility decorator for observers.
    
    Provides compatibility with older observer patterns while
    using the new observe decorator internally.
    
    Parameters:
    - *names: str - Trait names to observe
    - type: str - Type of change to observe
    
    Returns:
    function - Decorator function
    """

Validator Decorators

Decorators for registering cross-validation functions.

def validate(*names):
    """
    Decorator to register cross-validator.
    
    Registers a method to validate trait values before they are set.
    The validator can modify the value or raise TraitError to reject it.
    
    Parameters:
    - *names: str - Trait names to validate
    
    Returns:
    function - Decorator function
    
    Usage:
    @validate('trait_name')
    def _validate_trait(self, proposal):
        # proposal is dict with 'value' key
        value = proposal['value']
        # Validate and possibly modify value
        return validated_value
    """

Default Value Decorators

Decorators for providing dynamic default values.

def default(name):
    """
    Decorator for dynamic default value generator.
    
    Registers a method to provide default values for traits when
    they are first accessed and no value has been set.
    
    Parameters:
    - name: str - Trait name to provide default for
    
    Returns:
    function - Decorator function
    
    Usage:
    @default('trait_name')
    def _default_trait(self):
        return computed_default_value
    """

Event Handler Classes

Classes representing different types of event handlers.

class EventHandler:
    """
    Base class for event handlers.
    
    Provides common functionality for all types of event handlers
    including observer, validator, and default handlers.
    """

class ObserveHandler(EventHandler):
    """
    Handler for observe decorator.
    
    Manages observation of trait changes and dispatching
    to registered handler methods.
    """

class ValidateHandler(EventHandler):
    """
    Handler for validate decorator.
    
    Manages validation of trait values before assignment
    and dispatching to registered validator methods.
    """

class DefaultHandler(EventHandler):
    """
    Handler for default decorator.
    
    Manages generation of default values for traits
    and dispatching to registered default methods.
    """

Usage Examples

Basic Change Observation

from traitlets import HasTraits, Unicode, Int, observe

class Person(HasTraits):
    name = Unicode()
    age = Int()
    
    @observe('name')
    def _name_changed(self, change):
        print(f"Name changed from '{change['old']}' to '{change['new']}'")
        
    @observe('age')  
    def _age_changed(self, change):
        print(f"Age changed from {change['old']} to {change['new']}")
        
    @observe('name', 'age')
    def _any_change(self, change):
        print(f"Trait '{change['name']}' changed on {change['owner']}")

person = Person()
person.name = "Alice"    # Triggers _name_changed and _any_change
person.age = 30          # Triggers _age_changed and _any_change
person.name = "Bob"      # Triggers _name_changed and _any_change

Change Event Details

from traitlets import HasTraits, Unicode, observe

class Example(HasTraits):
    value = Unicode()
    
    @observe('value')
    def _value_changed(self, change):
        print("Change event details:")
        print(f"  name: {change['name']}")        # 'value'
        print(f"  old: {change['old']}")          # Previous value
        print(f"  new: {change['new']}")          # New value  
        print(f"  owner: {change['owner']}")      # Self
        print(f"  type: {change['type']}")        # 'change'

example = Example()
example.value = "initial"    # old=u'', new='initial'
example.value = "updated"    # old='initial', new='updated'

Cross-Validation

from traitlets import HasTraits, Int, TraitError, validate

class Rectangle(HasTraits):
    width = Int(min=0)
    height = Int(min=0)
    max_area = Int(default_value=1000)
    
    @validate('width', 'height')
    def _validate_dimensions(self, proposal):
        value = proposal['value']
        trait_name = proposal['trait'].name
        
        # Get the other dimension
        if trait_name == 'width':
            other_dim = self.height
        else:
            other_dim = self.width
            
        # Check if area would exceed maximum
        if value * other_dim > self.max_area:
            raise TraitError(f"Area would exceed maximum of {self.max_area}")
            
        return value

rect = Rectangle(max_area=100)
rect.width = 10
rect.height = 8     # 10 * 8 = 80, within limit

# rect.height = 15  # Would raise TraitError (10 * 15 = 150 > 100)

Dynamic Defaults

import time
from traitlets import HasTraits, Unicode, Float, default

class LogEntry(HasTraits):
    message = Unicode()
    timestamp = Float()
    hostname = Unicode()
    
    @default('timestamp')
    def _default_timestamp(self):
        return time.time()
        
    @default('hostname')
    def _default_hostname(self):
        import socket
        return socket.gethostname()

# Each instance gets current timestamp and hostname
entry1 = LogEntry(message="First log")
time.sleep(0.1)
entry2 = LogEntry(message="Second log")

print(entry1.timestamp != entry2.timestamp)  # True - different times
print(entry1.hostname == entry2.hostname)    # True - same host

Conditional Default Values

from traitlets import HasTraits, Unicode, Bool, default

class User(HasTraits):
    username = Unicode()
    email = Unicode()
    is_admin = Bool(default_value=False)
    display_name = Unicode()
    
    @default('display_name')
    def _default_display_name(self):
        if self.email:
            return self.email.split('@')[0]
        elif self.username:
            return self.username.title()
        else:
            return "Anonymous"

user1 = User(email="alice@example.com")
print(user1.display_name)  # "alice"

user2 = User(username="bob_smith")  
print(user2.display_name)  # "Bob_Smith"

user3 = User()
print(user3.display_name)  # "Anonymous"

Validation with Modification

from traitlets import HasTraits, Unicode, validate

class NormalizedText(HasTraits):
    text = Unicode()
    
    @validate('text')
    def _validate_text(self, proposal):
        value = proposal['value']
        
        # Normalize whitespace and case
        normalized = ' '.join(value.split()).lower()
        
        # Remove forbidden characters
        forbidden = ['<', '>', '&']
        for char in forbidden:
            normalized = normalized.replace(char, '')
            
        return normalized

text_obj = NormalizedText()
text_obj.text = "  Hello    WORLD  <script>  "
print(text_obj.text)  # "hello world script"

Observer for All Traits

from traitlets import HasTraits, Unicode, Int, observe, All

class Monitored(HasTraits):
    name = Unicode()
    value = Int()
    description = Unicode()
    
    @observe(All)  # Observe all traits
    def _any_trait_changed(self, change):
        print(f"Any trait changed: {change['name']} = {change['new']}")
        
    @observe(All, type='change')  # Explicit change type
    def _log_changes(self, change):
        import datetime
        timestamp = datetime.datetime.now().isoformat()
        print(f"[{timestamp}] {change['name']}: {change['old']} -> {change['new']}")

obj = Monitored()
obj.name = "test"           # Triggers both observers
obj.value = 42              # Triggers both observers
obj.description = "demo"    # Triggers both observers

Complex Validation Logic

from traitlets import HasTraits, Unicode, Int, List, TraitError, validate, observe

class Project(HasTraits):
    name = Unicode()
    priority = Int(min=1, max=5)
    tags = List(Unicode())
    assignees = List(Unicode())
    
    @validate('name')
    def _validate_name(self, proposal):
        name = proposal['value']
        
        # Must be non-empty and alphanumeric with underscores
        if not name or not name.replace('_', '').isalnum():
            raise TraitError("Name must be alphanumeric with underscores only")
            
        return name.lower()  # Normalize to lowercase
    
    @validate('tags')
    def _validate_tags(self, proposal):
        tags = proposal['value']
        
        # Remove duplicates and normalize
        normalized_tags = list(set(tag.lower().strip() for tag in tags))
        
        # Limit to 5 tags maximum
        if len(normalized_tags) > 5:
            raise TraitError("Maximum 5 tags allowed")
            
        return normalized_tags
    
    @observe('priority')
    def _priority_changed(self, change):
        if change['new'] >= 4:
            print(f"High priority project: {self.name}")

project = Project()
project.name = "My_Project_123"     # Becomes "my_project_123"
project.priority = 4                # Triggers high priority message
project.tags = ["Python", "WEB", "python", "api", "REST"]  # Normalized and deduplicated
print(project.tags)  # ['python', 'web', 'api', 'rest']

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