subsequence.live_reloader

Watch a Python file and re-exec it on save into a live composition.

Provides LiveReloader, the engine behind Composition.watch(path). Together they enable file-based live coding: edit a Python file in your normal editor, save, and the running composition picks up the changes without stopping the clock.

How it works ────────────

Composition.watch(path) constructs a LiveReloader and calls start().

start() performs an initial synchronous load — reads the file and delegates to Composition.load_patterns(), which compiles and execs the source into a namespace that has composition and subsequence in scope. This is the first chance for @composition.pattern decorators in the file to register with the composition. If the initial load fails (SyntaxError, missing file), the exception propagates — the user should know immediately if their entry point is broken.

A daemon thread is then spawned that polls the file’s st_mtime every poll_interval seconds. When the mtime changes, the thread schedules _reload_async() onto the composition’s event loop via asyncio.run_coroutine_threadsafe(), so mutation happens on the event loop thread (where the rest of the sequencer lives).

_reload_async() reads + compiles the file content, then delegates to Composition._apply_source_async() for exec, pattern activation, and diff-and-unregister against the running set. Errors from any phase are logged but do not abort the watcher.

Existing patterns hot-swap in place via the decorator path: when the same function name is re-decorated while _is_live=True, the running pattern’s _builder_fn is replaced and the next rebuild uses the new logic. The pattern’s channel, mirrors, device, and cycle counter are preserved — only the build logic changes.

Error handling ──────────────

SyntaxError during a reload — log a warning and skip the reload entirely. Previous state is preserved. The user fixes the file and saves again; the next mtime tick retries.

Runtime error during exec() (e.g. NameError, ImportError) — treated the same way: log a warning and skip the rest of the reload. Composition._apply_source_async re-raises exec failures specifically so this catch can suppress the diff-and-unregister phase, which would otherwise tear down patterns the broken file failed to reach. Note that decorators that already side-effect’d before the error fired cannot be rolled back — those builders will run their new bodies on the next reschedule.

File missing or unreadable mid-poll — log a warning, skip, retry next tick. Editor “atomic save” (write-temp-then-rename) is handled by catching OSError around the read.

Module-level state in the watched file ──────────────────────────────────────

Each reload uses a fresh namespace dict. Module-level objects in the watched file (e.g. state = MelodicState(...)) are recreated on every reload — long-lived state belongs on composition.data or in the wrapper script (the file that calls composition.watch()), not in the live file itself.

Security note ─────────────

This module calls exec() on arbitrary Python by design. Treat the watched file like any other source file in your project; never point it at untrusted content.

Module Contents

class subsequence.live_reloader.LiveReloader(composition: subsequence.composition.Composition, path: str | pathlib.Path, poll_interval: float = 0.25, skip_initial_exec: bool = False)[source]

Watch a Python file and re-exec it on save into a live composition.

Constructed by Composition.watch(path); users do not instantiate this class directly. Owns a daemon thread that polls the file’s modification time and a reference back to the composition for scheduling reloads onto its event loop.

Initialise the reloader in a stopped state.

Parameters:
  • composition – The live Composition instance to reload into.

  • path – Path to the Python file to watch.

  • poll_interval – Seconds between st_mtime polls. Default 0.25 s gives a responsive feel for editor saves without busy-waiting.

  • skip_initial_exec – When True, start() skips the compile + exec phase of the initial load and only records _last_mtime. Set by Composition.watch() when it detects a self-watch (the file calling watch() is the file being watched), since the outer Python script execution will already run the patterns at the module level — a second exec via _load_initial would double-register every one.

start() None[source]

Perform the initial synchronous load, then spawn the watcher thread.

Raises SyntaxError or FileNotFoundError if the file cannot be loaded — better to fail loudly here than to leave the user wondering why no patterns are running.

Safe to call once. A second call while the watcher is already running is a no-op.

stop() None[source]

Signal the watcher thread to exit; safe to call multiple times.

Joins the thread with a short timeout so shutdown is bounded.