CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-anywidget

Custom Jupyter widgets made easy with modern web technologies and seamless platform integration

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

file-management.mddocs/

File Management

Dynamic file loading system with live reloading capabilities for development workflows and virtual file management for inline content. This system enables seamless development experiences with hot module replacement and flexible content management.

Capabilities

FileContents Class

Watches file changes and emits signals when files are modified, enabling live reloading during development.

class FileContents:
    """
    Object that watches for file changes and emits a signal when it changes.
    
    Provides live reloading capabilities by monitoring filesystem changes
    and automatically updating widget content during development.
    
    Attributes:
        changed (Signal): Emitted when file contents change
        deleted (Signal): Emitted when file is deleted
    """
    
    def __init__(self, path: str | Path, start_thread: bool = True):
        """
        Initialize file watcher for the specified path.
        
        Parameters:
            path (str | Path): The file to read and watch for content changes
            start_thread (bool): Whether to start watching for changes in a separate thread
            
        Raises:
            ValueError: If the file does not exist
        """
    
    def watch_in_thread(self):
        """
        Watch for file changes (emitting signals) from a separate thread.
        
        Starts a background thread that monitors the file for changes
        and emits the 'changed' signal when modifications are detected.
        """
    
    def stop_thread(self):
        """
        Stops an actively running file watching thread if it exists.
        
        Cleanly shuts down the background file monitoring thread
        and releases associated resources.
        """
    
    def watch(self):
        """
        Watch for file changes and emit changed/deleted signal events.
        
        Blocks indefinitely, yielding change events until the file is deleted.
        
        Returns:
            Iterator[tuple[int, str]]: Iterator yielding change events
            
        Raises:
            ImportError: If watchfiles package is not installed
        """
    
    def __str__(self) -> str:
        """
        Return current file contents as string.
        
        Reads and caches file contents, returning the current text.
        Cache is cleared when file changes are detected.
        
        Returns:
            str: Current file contents
        """

VirtualFileContents Class

Stores text file contents in memory with change signals, useful for dynamic content and cell magic integration.

class VirtualFileContents:
    """
    Stores text file contents in memory and emits a signal when it changes.
    
    Provides in-memory file simulation with change notifications,
    useful for dynamic content and integration with IPython cell magics.
    
    Attributes:
        changed (Signal): Emitted when contents change
        contents (str): Current text contents (property)
    """
    
    def __init__(self, contents: str = ""):
        """
        Initialize virtual file with optional initial contents.
        
        Parameters:
            contents (str): The initial contents of the file (default: "")
        """
    
    @property
    def contents(self) -> str:
        """
        Get current file contents.
        
        Returns:
            str: Current text contents
        """
    
    @contents.setter  
    def contents(self, value: str):
        """
        Set file contents and emit changed signal.
        
        Parameters:
            value (str): New file contents
        """
    
    def __str__(self) -> str:
        """
        Return current contents as string.
        
        Returns:
            str: Current file contents
        """

File Utilities

Helper functions for working with file paths and content management.

def try_file_contents(x) -> FileContents | VirtualFileContents | None:
    """
    Try to coerce an object into a FileContents object.
    
    Attempts to convert strings, paths, or virtual file references
    into appropriate FileContents or VirtualFileContents objects.
    
    Parameters:
        x: Object to try to coerce (str, Path, etc.)
        
    Returns:
        FileContents | VirtualFileContents | None: File contents object or None if conversion fails
        
    Raises:
        FileNotFoundError: If the file path exists but file is not found
    """

def try_file_path(x) -> Path | None:
    """
    Try to coerce an object into a pathlib.Path object.
    
    Handles various input types and validates file path patterns:
    - Returns None for URLs or multi-line strings
    - Returns Path for single-line strings with file extensions
    - Returns existing Path objects unchanged
    
    Parameters:
        x: Object to try to coerce into a path
        
    Returns:
        Path | None: Path object if x is a valid file path, otherwise None
    """

Global Registry

_VIRTUAL_FILES: weakref.WeakValueDictionary[str, VirtualFileContents]
    """
    Global registry of virtual files.
    
    Weak reference dictionary that stores virtual file contents
    by name, automatically cleaning up unused references.
    """

Usage Examples

File-Based Widget Development

import anywidget

class DevelopmentWidget(anywidget.AnyWidget):
    # Files are automatically watched for changes
    _esm = "./widget.js"      # Reloads when widget.js changes
    _css = "./widget.css"     # Reloads when widget.css changes

# Enable hot module replacement for development
import os
os.environ["ANYWIDGET_HMR"] = "1"

widget = DevelopmentWidget()
# Edit widget.js or widget.css - changes appear immediately

Manual File Watching

from anywidget._file_contents import FileContents
import pathlib

# Create file watcher
js_file = FileContents("./my-widget.js")

# Connect to change events
@js_file.changed.connect
def on_js_change(new_contents):
    print(f"JavaScript updated: {len(new_contents)} characters")

@js_file.deleted.connect  
def on_js_deleted():
    print("JavaScript file was deleted!")

# Access current contents
print(str(js_file))  # Reads current file contents

# Stop watching when done
js_file.stop_thread()

Virtual File Management

from anywidget._file_contents import VirtualFileContents, _VIRTUAL_FILES

# Create virtual file
virtual_js = VirtualFileContents("""
function render({ model, el }) {
    el.innerHTML = "<h1>Dynamic Content</h1>";
}
export default { render };
""")

# Register in global registry
_VIRTUAL_FILES["my-widget"] = virtual_js

# Connect to change events
@virtual_js.changed.connect
def on_virtual_change(new_contents):
    print("Virtual file updated!")

# Update contents programmatically
virtual_js.contents = """
function render({ model, el }) {
    el.innerHTML = "<h1>Updated Dynamic Content</h1>";
}
export default { render };
"""

# Use in widget
import anywidget

class VirtualWidget(anywidget.AnyWidget):
    _esm = "my-widget"  # References virtual file by name

widget = VirtualWidget()

Development Workflow with Live Reloading

import anywidget
import traitlets as t
from pathlib import Path

# Create development files
js_path = Path("./counter.js")
css_path = Path("./counter.css")

js_path.write_text("""
function render({ model, el }) {
    let count = () => model.get("value");
    let btn = document.createElement("button");
    btn.innerHTML = `Count: ${count()}`;
    btn.className = "counter-btn";
    
    btn.addEventListener("click", () => {
        model.set("value", count() + 1);
    });
    
    model.on("change:value", () => {
        btn.innerHTML = `Count: ${count()}`;
    });
    
    el.appendChild(btn);
}
export default { render };
""")

css_path.write_text("""
.counter-btn {
    background: #007cba;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
}
.counter-btn:hover {
    background: #005a8b;
}
""")

class CounterWidget(anywidget.AnyWidget):
    _esm = js_path        # FileContents automatically created
    _css = css_path       # FileContents automatically created  
    
    value = t.Int(0).tag(sync=True)

# Enable hot reloading
import os
os.environ["ANYWIDGET_HMR"] = "1"

widget = CounterWidget()
# Now edit counter.js or counter.css files - changes appear immediately in Jupyter

Custom File Content Processing

from anywidget._file_contents import FileContents
import json

class ConfigFileWatcher:
    def __init__(self, config_path):
        self.config_file = FileContents(config_path)
        self.config = {}
        self._load_config()
        
        # Watch for changes
        self.config_file.changed.connect(self._on_config_change)
    
    def _load_config(self):
        """Load and parse configuration"""
        try:
            self.config = json.loads(str(self.config_file))
        except json.JSONDecodeError:
            self.config = {}
            print("Warning: Invalid JSON in config file")
    
    def _on_config_change(self, new_contents):
        """Handle configuration file changes"""
        print("Configuration file updated, reloading...")
        self._load_config()
        self._apply_config()
    
    def _apply_config(self):
        """Apply configuration changes"""
        print(f"Applied config: {self.config}")

# Usage
config_watcher = ConfigFileWatcher("./app-config.json")
# Edit app-config.json - changes are automatically detected and processed

Integration with Widget Updates

import anywidget
import traitlets as t
from anywidget._file_contents import FileContents

class ConfigurableWidget(anywidget.AnyWidget):
    _esm = """
    function render({ model, el }) {
        let updateDisplay = () => {
            let config = model.get("config");
            el.innerHTML = `
                <div style="
                    background: ${config.background || '#f0f0f0'};
                    color: ${config.color || '#333'};
                    padding: ${config.padding || '10px'};
                    border-radius: ${config.borderRadius || '5px'};
                ">
                    <h3>${config.title || 'Default Title'}</h3>
                    <p>${config.message || 'Default message'}</p>
                </div>
            `;
        };
        
        model.on("change:config", updateDisplay);
        updateDisplay();
    }
    export default { render };
    """
    
    config = t.Dict({}).tag(sync=True)
    
    def __init__(self, config_file=None, **kwargs):
        super().__init__(**kwargs)
        
        if config_file:
            self.config_watcher = FileContents(config_file)
            self.config_watcher.changed.connect(self._update_from_file)
            self._update_from_file(str(self.config_watcher))
    
    def _update_from_file(self, contents):
        """Update widget config from file contents"""
        try:
            import json
            new_config = json.loads(contents)
            self.config = new_config
        except json.JSONDecodeError:
            print("Warning: Invalid JSON in config file")

# Create widget with external config file
widget = ConfigurableWidget(config_file="./widget-config.json")
# Edit widget-config.json to change widget appearance in real-time

Install with Tessl CLI

npx tessl i tessl/pypi-anywidget

docs

core-widget.md

experimental.md

file-management.md

index.md

ipython-integration.md

tile.json