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
Compositioninstance to reload into.path – Path to the Python file to watch.
poll_interval – Seconds between
st_mtimepolls. 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 byComposition.watch()when it detects a self-watch (the file callingwatch()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_initialwould double-register every one.
- start() None[source]¶
Perform the initial synchronous load, then spawn the watcher thread.
Raises
SyntaxErrororFileNotFoundErrorif 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.