subsequence.live_reloader ========================= .. py:module:: subsequence.live_reloader .. autoapi-nested-parse:: 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 --------------- .. py:class:: LiveReloader(composition: subsequence.composition.Composition, path: Union[str, pathlib.Path], poll_interval: float = 0.25, skip_initial_exec: bool = False) 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. :param composition: The live ``Composition`` instance to reload into. :param path: Path to the Python file to watch. :param poll_interval: Seconds between ``st_mtime`` polls. Default 0.25 s gives a responsive feel for editor saves without busy-waiting. :param 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. .. py:method:: start() -> None Perform the initial synchronous load, then spawn the watcher thread. Raises :exc:`SyntaxError` or :exc:`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. .. py:method:: stop() -> None Signal the watcher thread to exit; safe to call multiple times. Joins the thread with a short timeout so shutdown is bounded.