Rebuild Sphinx documentation on changes, with hot reloading in the browser.
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.
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
"""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())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,
)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)The WebSocket protocol used for browser hot reloading is simple:
Connection:
/websocket-reload endpointMessages:
"refresh" - Triggers browser reloadConnection Lifecycle:
"refresh" messageThe browser client code (injected by middleware):
const ws = new WebSocket("ws://127.0.0.1:8000/websocket-reload");
ws.onmessage = () => window.location.reload();Uses the watchfiles library for efficient file system monitoring:
When files change:
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)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