Source code for subsequence.live_client

"""Interactive REPL client for live coding a running Subsequence composition.

Usage::

    python -m subsequence.live_client
    python -m subsequence.live_client --port 5555

The client connects to a live server started by ``composition.live()`` and
provides an interactive Python prompt. Multi-line blocks are supported  - 
type a line ending with ``:`` and the client will wait for more input.

Press Ctrl+C to cancel the current input. Press Ctrl+D to quit.
"""

import argparse
import socket
import sys
import typing


SENTINEL = b"\x04"


[docs] class LiveClient: """TCP client that sends code to a running Subsequence live server.""" def __init__ (self) -> None: """Initialize with no connection.""" self._sock: typing.Optional[socket.socket] = None
[docs] def connect (self, host: str = "127.0.0.1", port: int = 5555) -> None: """Connect to the live server.""" self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((host, port))
[docs] def send (self, code: str) -> str: """Send code to the server and return the response.""" if self._sock is None: raise ConnectionError("Not connected") self._sock.sendall(code.encode("utf-8") + SENTINEL) chunks: typing.List[bytes] = [] while True: chunk = self._sock.recv(4096) if not chunk: raise ConnectionError("Server closed connection") if SENTINEL in chunk: before, _, _ = chunk.partition(SENTINEL) chunks.append(before) break chunks.append(chunk) return b"".join(chunks).decode("utf-8")
[docs] def close (self) -> None: """Close the connection.""" if self._sock is not None: self._sock.close() self._sock = None
def _is_incomplete (code: str) -> bool: """Return True if the code looks like an incomplete multi-line block.""" stripped = code.rstrip() if not stripped: return False # Trailing colon suggests a block header (def, if, for, etc.). if stripped.endswith(":"): return True # Unclosed brackets or parens. opens = sum(1 for c in code if c in "([{") closes = sum(1 for c in code if c in ")]}") if opens > closes: return True # Trailing backslash (line continuation). if stripped.endswith("\\"): return True return False
[docs] def main () -> None: """Run the interactive REPL loop.""" parser = argparse.ArgumentParser(description="Subsequence live coding client") parser.add_argument("--host", default="127.0.0.1", help="Server host (default: 127.0.0.1)") parser.add_argument("--port", type=int, default=5555, help="Server port (default: 5555)") args = parser.parse_args() client = LiveClient() try: client.connect(args.host, args.port) except ConnectionRefusedError: print(f"Could not connect to {args.host}:{args.port}") print("Is the composition running with composition.live() enabled?") sys.exit(1) print(f"Connected to Subsequence on {args.host}:{args.port}") # Fetch and display status header. try: info_response = client.send("composition.live_info()") print(info_response) except Exception: pass print() try: while True: try: line = input(">>> ") except KeyboardInterrupt: print() continue lines = [line] # Accumulate multi-line blocks. Once a block has started, only a # BLANK line ends it (standard REPL behaviour) — terminating when # the joined code merely "looked complete" cut every block off # after its first body line. in_block = _is_incomplete(line) while in_block or _is_incomplete("\n".join(lines)): try: continuation = input("... ") except KeyboardInterrupt: print() lines = [] break if in_block and continuation.strip() == "": break lines.append(continuation) if not lines: continue code = "\n".join(lines).strip() if not code: continue try: response = client.send(code) print(response) except ConnectionError: print("Connection lost.") break except EOFError: print() finally: client.close()
if __name__ == "__main__": main()