PyPositron Docs

Internal Classes

PyPositron includes several internal classes that manage the execution context, exposed functions, and communication between Python and JavaScript. While these classes are primarily used internally, understanding them can help with advanced usage scenarios and debugging.

PositronContext

The PositronContext class manages the execution context for Python code and event handlers. It serves as the bridge between Python and JavaScript environments, handling variable scoping, code execution, and event management.

Attributes

  • window: The webview window object reference
  • globals: dict: Global variables for Python execution context, accessible in <py> tags and dynamic code execution
  • locals: dict: Local variables for Python execution context
  • exposed_functions: dict: Dictionary of functions exposed to JavaScript, mapping function names to callable objects
  • event_handlers: dict: Dictionary of registered event handlers, mapping event names to callback functions

Methods

  • execute(code: str) -> Tuple[bool, str]: Executes Python code within the context. Returns a tuple containing success status (boolean) and error message (string if failed, empty if successful)
  • register_event_handler(element_id: str, event_type: str, callback: Callable) -> str: Registers an event handler for a DOM element. Returns a unique identifier for the handler

Usage Examples

Basic Context Usage

import py_positron

def main(ui):
    # Access the context
    context = ui.context
    
    # Set global variables accessible in <py> tags
    context.globals['app_config'] = {
        'name': 'MyApp',
        'version': '1.0.0',
        'debug': True
    }
    
    context.globals['user_preferences'] = {
        'theme': 'dark',
        'language': 'en'
    }
    
    # Execute Python code dynamically
    code = """
app_title = f"{app_config['name']} v{app_config['version']}"
document.getElementById('title').innerText = app_title
"""
    
    success, error = context.execute(code)
    if success:
        print("Code executed successfully")
    else:
        print(f"Execution error: {error}")

py_positron.openUI("app.html", main=main)

Advanced Context Management

import py_positron
import json
import datetime

def main(ui):
    context = ui.context
    
    # Set up a comprehensive context
    setup_context_variables(context)
    setup_custom_handlers(context, ui)
    demonstrate_dynamic_execution(context, ui)

def setup_context_variables(context):
    """Set up global variables for the context"""
    
    # Application metadata
    context.globals['app_info'] = {
        'name': 'PyPositron Context Demo',
        'version': '2.1.0',
        'author': 'PyPositron Team',
        'build_date': datetime.datetime.now().isoformat()
    }
    
    # Utility functions available in <py> tags
    context.globals['utils'] = {
        'format_date': lambda d: datetime.datetime.fromtimestamp(d).strftime('%Y-%m-%d %H:%M:%S'),
        'to_json': lambda obj: json.dumps(obj, indent=2),
        'from_json': lambda s: json.loads(s)
    }
    
    # Application state
    context.globals['app_state'] = {
        'current_page': 'home',
        'user_logged_in': False,
        'notifications': []
    }

def setup_custom_handlers(context, ui):
    """Register custom event handlers"""
    
    def handle_page_change(page_name):
        context.globals['app_state']['current_page'] = page_name
        ui.htmlwindow.console.log(f"Page changed to: {page_name}")
        
        # Update UI based on page change
        code = f"""
document.querySelectorAll('.page').forEach(page => page.style.display = 'none')
current_page = document.getElementById('{page_name}')
if current_page:
    current_page.style.display = 'block'
"""
        context.execute(code)
    
    def handle_notification_add(message, type='info'):
        notification = {
            'id': len(context.globals['app_state']['notifications']),
            'message': message,
            'type': type,
            'timestamp': datetime.datetime.now().timestamp()
        }
        context.globals['app_state']['notifications'].append(notification)
        
        # Update notification UI
        code = f"""
notifications_container = document.getElementById('notifications')
if notifications_container:
    notification_div = document.createElement('div')
    notification_div.className = 'notification notification-{type}'
    notification_div.innerHTML = '''
        <span class="message">{message}</span>
        <span class="time">{datetime.datetime.now().strftime('%H:%M:%S')}</span>
    '''
    notifications_container.appendChild(notification_div)
"""
        context.execute(code)
    
    # Register handlers
    context.event_handlers['page_change'] = handle_page_change
    context.event_handlers['notification_add'] = handle_notification_add

def demonstrate_dynamic_execution(context, ui):
    """Demonstrate dynamic code execution capabilities"""
    
    # Set up code execution interface
    execute_btn = ui.document.getElementById("executeCodeBtn")
    code_input = ui.document.getElementById("codeInput")
    output_div = ui.document.getElementById("codeOutput")
    
    if execute_btn and code_input and output_div:
        def execute_user_code():
            user_code = code_input.value.strip()
            if not user_code:
                return
            
            # Add output capture to user code
            enhanced_code = f"""
import sys
from io import StringIO

# Capture output
old_stdout = sys.stdout
sys.stdout = captured_output = StringIO()

try:
    # User code
{user_code}
    
    # Get captured output
    output = captured_output.getvalue()
    if output:
        print("Output:", output)
        
finally:
    # Restore stdout
    sys.stdout = old_stdout
"""
            
            success, error = context.execute(enhanced_code)
            
            if success:
                output_div.style.color = "green"
                output_div.innerText = "Code executed successfully!"
                ui.htmlwindow.console.log("User code executed successfully")
            else:
                output_div.style.color = "red"
                output_div.innerText = f"Error: {error}"
                ui.htmlwindow.console.error(f"User code error: {error}")
        
        execute_btn.addEventListener("click", execute_user_code)

py_positron.openUI("context_advanced.html", main=main)

Context State Management

import py_positron

class ApplicationState:
    """A more sophisticated state management approach"""
    
    def __init__(self, context):
        self.context = context
        self._state = {}
        self._listeners = {}
        
        # Make state management functions available globally
        context.globals['get_state'] = self.get
        context.globals['set_state'] = self.set
        context.globals['on_state_change'] = self.on_change
    
    def get(self, key, default=None):
        return self._state.get(key, default)
    
    def set(self, key, value):
        old_value = self._state.get(key)
        self._state[key] = value
        
        # Notify listeners
        if key in self._listeners:
            for listener in self._listeners[key]:
                listener(value, old_value)
    
    def on_change(self, key, callback):
        if key not in self._listeners:
            self._listeners[key] = []
        self._listeners[key].append(callback)

def main(ui):
    # Set up sophisticated state management
    app_state = ApplicationState(ui.context)
    
    # Initialize state
    app_state.set('user', {'name': 'Guest', 'role': 'visitor'})
    app_state.set('theme', 'light')
    app_state.set('sidebar_open', False)
    
    # Set up state change listeners
    def on_theme_change(new_theme, old_theme):
        ui.htmlwindow.console.log(f"Theme changed from {old_theme} to {new_theme}")
        
        # Update CSS based on theme
        body = ui.document.body
        if new_theme == 'dark':
            body.classList.add('dark-theme')
            body.classList.remove('light-theme')
        else:
            body.classList.add('light-theme')
            body.classList.remove('dark-theme')
    
    def on_user_change(new_user, old_user):
        ui.htmlwindow.console.log(f"User changed: {new_user}")
        
        # Update user display
        user_display = ui.document.getElementById('userDisplay')
        if user_display:
            user_display.innerText = f"Welcome, {new_user['name']}!"
    
    app_state.on_change('theme', on_theme_change)
    app_state.on_change('user', on_user_change)
    
    # Set up UI controls that use state management
    theme_toggle = ui.document.getElementById('themeToggle')
    if theme_toggle:
        def toggle_theme():
            current_theme = app_state.get('theme')
            new_theme = 'dark' if current_theme == 'light' else 'light'
            app_state.set('theme', new_theme)
        
        theme_toggle.addEventListener('click', toggle_theme)

py_positron.openUI("state_management.html", main=main)

ExposedFunctions

The ExposedFunctions class represents Python functions that have been exposed to JavaScript. It provides a clean interface for calling Python functions from JavaScript code and <py> tags.

Methods

  • __init__(functions_dict: Dict[str, Callable]): Initializes with a dictionary of function names mapped to callable objects

Usage

Functions passed to the functions parameter of openUI() become available through this object:

Basic Function Exposure

import py_positron
import math
import random

# Define functions to expose
def calculate_area(radius):
    """Calculate the area of a circle"""
    return math.pi * radius ** 2

def generate_random_number(min_val=1, max_val=100):
    """Generate a random number within a range"""
    return random.randint(min_val, max_val)

def process_data(data_list):
    """Process a list of numbers"""
    if not data_list:
        return {'error': 'No data provided'}
    
    return {
        'sum': sum(data_list),
        'average': sum(data_list) / len(data_list),
        'min': min(data_list),
        'max': max(data_list),
        'count': len(data_list)
    }

def main(ui):
    # Functions are now available via ui.exposed
    
    # Example: Calculate area button
    calc_btn = ui.document.getElementById("calculateBtn")
    radius_input = ui.document.getElementById("radiusInput")
    result_div = ui.document.getElementById("result")
    
    if calc_btn and radius_input and result_div:
        def calculate():
            try:
                radius = float(radius_input.value)
                area = ui.exposed.calculate_area(radius)
                result_div.innerText = f"Area: {area:.2f}"
                result_div.style.color = "green"
            except ValueError:
                result_div.innerText = "Please enter a valid number"
                result_div.style.color = "red"
        
        calc_btn.addEventListener("click", calculate)
    
    # Example: Random number generator
    random_btn = ui.document.getElementById("randomBtn")
    random_display = ui.document.getElementById("randomDisplay")
    
    if random_btn and random_display:
        def generate_random():
            number = ui.exposed.generate_random_number(1, 1000)
            random_display.innerText = f"Random number: {number}"
        
        random_btn.addEventListener("click", generate_random)

# Launch with exposed functions
py_positron.openUI(
    "calculator.html", 
    main=main, 
    functions=[calculate_area, generate_random_number, process_data]
)

Advanced Function Exposure with Error Handling

import py_positron
import json
import sqlite3
import os

class DatabaseManager:
    """A more complex class with methods to expose"""
    
    def __init__(self, db_path="app.db"):
        self.db_path = db_path
        self.init_database()
    
    def init_database(self):
        """Initialize the database with required tables"""
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY,
                    name TEXT NOT NULL,
                    email TEXT UNIQUE NOT NULL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)
    
    def add_user(self, name, email):
        """Add a new user to the database"""
        try:
            with sqlite3.connect(self.db_path) as conn:
                conn.execute(
                    "INSERT INTO users (name, email) VALUES (?, ?)",
                    (name, email)
                )
                return {"success": True, "message": "User added successfully"}
        except sqlite3.IntegrityError:
            return {"success": False, "message": "Email already exists"}
        except Exception as e:
            return {"success": False, "message": str(e)}
    
    def get_users(self):
        """Get all users from the database"""
        try:
            with sqlite3.connect(self.db_path) as conn:
                conn.row_factory = sqlite3.Row
                cursor = conn.execute("SELECT * FROM users ORDER BY created_at DESC")
                users = [dict(row) for row in cursor.fetchall()]
                return {"success": True, "users": users}
        except Exception as e:
            return {"success": False, "message": str(e)}
    
    def delete_user(self, user_id):
        """Delete a user by ID"""
        try:
            with sqlite3.connect(self.db_path) as conn:
                cursor = conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
                if cursor.rowcount > 0:
                    return {"success": True, "message": "User deleted successfully"}
                else:
                    return {"success": False, "message": "User not found"}
        except Exception as e:
            return {"success": False, "message": str(e)}

def file_operations_save(filename, content):
    """Save content to a file"""
    try:
        with open(filename, 'w') as f:
            f.write(content)
        return {"success": True, "message": f"Saved to {filename}"}
    except Exception as e:
        return {"success": False, "message": str(e)}

def file_operations_load(filename):
    """Load content from a file"""
    try:
        if not os.path.exists(filename):
            return {"success": False, "message": "File not found"}
        
        with open(filename, 'r') as f:
            content = f.read()
        return {"success": True, "content": content}
    except Exception as e:
        return {"success": False, "message": str(e)}

def main(ui):
    # Initialize database manager
    db = DatabaseManager()
    
    # Set up user management interface
    setup_user_interface(ui, db)
    
    # Set up file operations interface
    setup_file_interface(ui)

def setup_user_interface(ui, db):
    """Set up user management UI"""
    
    # Add user form
    add_user_btn = ui.document.getElementById("addUserBtn")
    name_input = ui.document.getElementById("nameInput")
    email_input = ui.document.getElementById("emailInput")
    
    if add_user_btn and name_input and email_input:
        def add_user():
            name = name_input.value.strip()
            email = email_input.value.strip()
            
            if not name or not email:
                ui.htmlwindow.alert("Please fill in all fields")
                return
            
            result = db.add_user(name, email)
            if result["success"]:
                ui.htmlwindow.alert(result["message"])
                name_input.value = ""
                email_input.value = ""
                refresh_user_list()
            else:
                ui.htmlwindow.alert(f"Error: {result['message']}")
        
        add_user_btn.addEventListener("click", add_user)
    
    # Refresh user list
    def refresh_user_list():
        users_container = ui.document.getElementById("usersContainer")
        if not users_container:
            return
        
        result = db.get_users()
        if result["success"]:
            users_html = ""
            for user in result["users"]:
                users_html += f"""
                <div class="user-item" data-id="{user['id']}">
                    <span><strong>{user['name']}</strong> ({user['email']})</span>
                    <button onclick="deleteUser({user['id']})">Delete</button>
                </div>
                """
            users_container.innerHTML = users_html
        else:
            users_container.innerHTML = f"<p>Error loading users: {result['message']}</p>"
    
    # Delete user function (accessible from HTML)
    ui.context.globals['deleteUser'] = lambda user_id: delete_user_handler(ui, db, user_id)
    
    # Refresh button
    refresh_btn = ui.document.getElementById("refreshUsersBtn")
    if refresh_btn:
        refresh_btn.addEventListener("click", refresh_user_list)
    
    # Initial load
    refresh_user_list()

def delete_user_handler(ui, db, user_id):
    """Handle user deletion"""
    if ui.htmlwindow.confirm("Are you sure you want to delete this user?"):
        result = db.delete_user(user_id)
        ui.htmlwindow.alert(result["message"])
        if result["success"]:
            # Refresh the user list
            refresh_user_list()

def setup_file_interface(ui):
    """Set up file operations UI"""
    
    # Save file
    save_btn = ui.document.getElementById("saveFileBtn")
    filename_input = ui.document.getElementById("filenameInput")
    content_textarea = ui.document.getElementById("contentTextarea")
    
    if save_btn and filename_input and content_textarea:
        def save_file():
            filename = filename_input.value.strip()
            content = content_textarea.value
            
            if not filename:
                ui.htmlwindow.alert("Please enter a filename")
                return
            
            result = ui.exposed.file_operations_save(filename, content)
            ui.htmlwindow.alert(result["message"])
        
        save_btn.addEventListener("click", save_file)
    
    # Load file
    load_btn = ui.document.getElementById("loadFileBtn")
    
    if load_btn and filename_input and content_textarea:
        def load_file():
            filename = filename_input.value.strip()
            
            if not filename:
                ui.htmlwindow.alert("Please enter a filename")
                return
            
            result = ui.exposed.file_operations_load(filename)
            if result["success"]:
                content_textarea.value = result["content"]
                ui.htmlwindow.alert("File loaded successfully")
            else:
                ui.htmlwindow.alert(f"Error: {result['message']}")
        
        load_btn.addEventListener("click", load_file)

# Create database instance and expose its methods
db_instance = DatabaseManager()

py_positron.openUI(
    "database_app.html",
    main=main,
    title="Database Management App",
    width=1000,
    height=700,
    functions=[
        db_instance.add_user,
        db_instance.get_users, 
        db_instance.delete_user,
        file_operations_save,
        file_operations_load
    ]
)

Best Practices for Internal Classes

PositronContext Best Practices

  1. Don't overuse globals: Only put truly global data in context.globals
  2. Use descriptive names: Make variable names clear and unambiguous
  3. Handle execution errors: Always check the return value of context.execute()
  4. Clean up handlers: Remove event handlers when they're no longer needed
def main(ui):
    # Good: Essential configuration
    ui.context.globals['config'] = load_app_config()
    
    # Good: Utility functions for <py> tags
    ui.context.globals['helpers'] = {
        'format_currency': lambda x: f"${x:.2f}",
        'format_date': lambda d: d.strftime('%Y-%m-%d')
    }
    
    # Avoid: Storing complex application state
    # Use proper Python variables instead

ExposedFunctions Best Practices

  1. Return consistent data structures: Always return the same type of data
  2. Handle errors gracefully: Return error objects instead of raising exceptions
  3. Validate input parameters: Check and sanitize all inputs
  4. Keep functions focused: Each function should do one thing well
def safe_exposed_function(data):
    """Example of a well-designed exposed function"""
    try:
        # Validate input
        if not isinstance(data, (list, dict)):
            return {"success": False, "error": "Invalid input type"}
        
        # Process data
        result = process_the_data(data)
        
        # Return consistent structure
        return {"success": True, "data": result}
        
    except Exception as e:
        # Handle errors gracefully
        return {"success": False, "error": str(e)}

Related Documentation