CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-sphinx-autobuild

Rebuild Sphinx documentation on changes, with hot reloading in the browser.

Overview
Eval results
Files

server.mddocs/

File Watching & Server

The server component provides file watching capabilities and WebSocket-based hot reloading functionality. It monitors file system changes and communicates with browser clients to trigger automatic page reloads.

Capabilities

RebuildServer Class

The main server class that handles file watching and WebSocket connections for hot reloading.

class RebuildServer:
    def __init__(
        self,
        paths: list[os.PathLike[str]],
        ignore_filter: IgnoreFilter,
        change_callback: Callable[[Sequence[Path]], None]
    ) -> None:
        """
        Initialize file watching server.
        
        Parameters:
        - paths: list[os.PathLike[str]] - Directories to watch for changes
        - ignore_filter: IgnoreFilter - Filter for ignoring specific files/patterns
        - change_callback: Callable[[Sequence[Path]], None] - Function called when changes detected
        """
    
    @asynccontextmanager
    async def lifespan(self, _app) -> AbstractAsyncContextManager[None]:
        """
        ASGI lifespan context manager for server startup/shutdown.
        
        Manages the file watching task lifecycle:
        - Starts file watching on application startup
        - Stops file watching on application shutdown
        
        Parameters:
        - _app: ASGI application instance (unused)
        
        Yields:
        - None - Context for application lifespan
        """
    
    async def main(self) -> None:
        """
        Main server event loop.
        
        Coordinates file watching and shutdown handling:
        - Creates file watching task
        - Creates shutdown waiting task  
        - Cancels pending tasks on completion
        
        Returns:
        - None
        """
    
    async def watch(self) -> None:
        """
        File watching loop using watchfiles.
        
        Monitors configured directories for changes:
        - Filters changes through ignore_filter
        - Calls change_callback with changed paths
        - Runs callback in ProcessPoolExecutor for isolation
        - Sets internal flag to notify WebSocket clients
        
        Returns:
        - None
        """
    
    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        """
        ASGI application callable for WebSocket connections.
        
        Handles WebSocket connections from browser clients:
        - Accepts WebSocket connections
        - Manages client connection lifecycle
        - Coordinates reload notifications and disconnect handling
        
        Parameters:
        - scope: ASGI scope dict with connection info
        - receive: ASGI receive callable for messages
        - send: ASGI send callable for responses
        
        Returns:
        - None
        """
    
    async def watch_reloads(self, ws: WebSocket) -> None:
        """
        Send reload notifications to WebSocket client.
        
        Waits for file change events and sends reload messages:
        - Blocks until file changes detected
        - Sends "refresh" message to browser client
        - Resets change flag for next iteration
        
        Parameters:
        - ws: WebSocket - Connected client WebSocket
        
        Returns:
        - None
        """
    
    @staticmethod
    async def wait_client_disconnect(ws: WebSocket) -> None:
        """
        Wait for WebSocket client to disconnect.
        
        Monitors WebSocket for client disconnection:
        - Iterates over incoming messages until connection closes
        - Provides graceful handling of client disconnects
        
        Parameters:
        - ws: WebSocket - Connected client WebSocket
        
        Returns:
        - None
        """

Usage Examples

Basic File Watching Setup

import asyncio
from pathlib import Path
from sphinx_autobuild.server import RebuildServer
from sphinx_autobuild.filter import IgnoreFilter

def build_callback(changed_paths):
    """Called when files change."""
    print(f"Files changed: {[str(p) for p in changed_paths]}")
    # Trigger rebuild logic here

# Setup file watching
watch_dirs = [Path('docs'), Path('source')]
ignore_filter = IgnoreFilter(['.git', '__pycache__'], [r'\.tmp$'])
server = RebuildServer(watch_dirs, ignore_filter, build_callback)

# Run the file watcher
async def run_watcher():
    await server.main()

# asyncio.run(run_watcher())

ASGI Application Integration

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount, WebSocketRoute
from starlette.staticfiles import StaticFiles
from sphinx_autobuild.server import RebuildServer
from sphinx_autobuild.middleware import JavascriptInjectorMiddleware

# Create server instance
server = RebuildServer(watch_dirs, ignore_filter, build_callback)

# Create ASGI application
app = Starlette(
    routes=[
        WebSocketRoute("/websocket-reload", server, name="reload"),
        Mount("/", app=StaticFiles(directory="_build/html", html=True), name="static"),
    ],
    middleware=[Middleware(JavascriptInjectorMiddleware, ws_url="127.0.0.1:8000")],
    lifespan=server.lifespan,
)

Custom Change Handling

from pathlib import Path
from sphinx_autobuild.server import RebuildServer
from sphinx_autobuild.filter import IgnoreFilter

class CustomChangeHandler:
    def __init__(self):
        self.change_count = 0
    
    def __call__(self, changed_paths):
        """Custom change callback with logging and filtering."""
        self.change_count += 1
        
        # Filter to only .rst and .md files
        docs_changes = [
            p for p in changed_paths 
            if p.suffix in ['.rst', '.md', '.py']
        ]
        
        if docs_changes:
            print(f"Build #{self.change_count}: {len(docs_changes)} documentation files changed")
            # Trigger actual build
            self.rebuild_docs(docs_changes)
        else:
            print(f"Change #{self.change_count}: Non-documentation files changed, skipping build")
    
    def rebuild_docs(self, paths):
        # Custom build logic
        pass

# Use custom handler
handler = CustomChangeHandler()
server = RebuildServer([Path('docs')], IgnoreFilter([], []), handler)

WebSocket Protocol

Client-Server Communication

The WebSocket protocol used for browser hot reloading is simple:

Connection:

  • Browser connects to /websocket-reload endpoint
  • Server accepts connection and starts monitoring for changes

Messages:

  • Server → Client: "refresh" - Triggers browser reload
  • Client → Server: Any message keeps connection alive

Connection Lifecycle:

  1. Browser JavaScript establishes WebSocket connection
  2. Server waits for file changes or client disconnect
  3. On file change: server sends "refresh" message
  4. Browser receives message and reloads page
  5. New page establishes fresh WebSocket connection

JavaScript Client Code

The browser client code (injected by middleware):

const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
ws.onmessage = () => window.location.reload();

File Watching Details

Watchfiles Integration

Uses the watchfiles library for efficient file system monitoring:

  • Cross-platform: Works on Windows, macOS, Linux
  • Efficient: Uses native OS file watching APIs
  • Recursive: Automatically watches subdirectories
  • Filter Integration: Applies IgnoreFilter to all detected changes

Change Processing

When files change:

  1. Detection: watchfiles detects filesystem events
  2. Filtering: IgnoreFilter determines if changes should be processed
  3. Callback Execution: change_callback runs in ProcessPoolExecutor for isolation
  4. Notification: WebSocket clients receive reload message

Performance Characteristics

  • Asynchronous: Non-blocking file watching and WebSocket handling
  • Process Isolation: Build callbacks run in separate process to prevent blocking
  • Concurrent Clients: Supports multiple browser connections simultaneously
  • Efficient Filtering: File filtering happens before expensive operations

Error Handling

File Watching Errors

  • Permission Denied: Graceful handling of inaccessible directories
  • Directory Not Found: Validates watch paths during initialization
  • Filesystem Errors: Continues watching other directories on isolated errors

WebSocket Errors

  • Connection Failures: Individual client disconnects don't affect server
  • Message Errors: Malformed messages are ignored
  • Protocol Errors: WebSocket protocol violations handled gracefully

Callback Errors

  • Exception Isolation: Errors in change_callback don't crash server
  • Process Boundaries: ProcessPoolExecutor contains callback failures
  • Continued Operation: Server continues watching after callback errors

Integration Points

With Build System

from sphinx_autobuild.build import Builder

# Builder as change callback
builder = Builder(sphinx_args, url_host='127.0.0.1:8000', 
                 pre_build_commands=[], post_build_commands=[])
server = RebuildServer([Path('docs')], ignore_filter, builder)

With ASGI Framework

from starlette.applications import Starlette

app = Starlette(
    routes=[WebSocketRoute("/websocket-reload", server)],
    lifespan=server.lifespan  # Manages file watching lifecycle
)

Install with Tessl CLI

npx tessl i tessl/pypi-sphinx-autobuild

docs

build.md

cli.md

filtering.md

index.md

middleware.md

server.md

utils.md

tile.json