Documentation
¶
Overview ¶
Package pyvenv runs a Python virtual environment as an in-process worker for a Go program. The Go side spawns one long-lived Python subprocess per Venv, talks to it over length-prefixed JSON frames on stdin/stdout, and exposes typed Call/Await into named Python functions.
The module is framework-agnostic and has no third-party Go dependencies. The intended consumer is any Go program that wants to call into Python libraries (PyTorch, pandas, scikit-learn, etc.) without reimplementing subprocess management, framing, and result tracking.
Call/Await are split so that a long-running Python computation can outlive the caller's context. Call enqueues a function invocation and returns a callID without waiting; the Python work runs in the worker until completion. The returned callID is the handle for Await(ctx, callID, &result), which blocks until the worker finishes or ctx expires. ctx expiry on Await does not cancel or consume the in-flight call — a subsequent Await(callID) by the same caller (or a retry of a workflow task that persisted the callID) re-enters the wait. A successful Await consumes the result; a subsequent Await on the same callID returns ErrUnknown.
Typical lifecycle:
Embed Python sources at compile time: //go:embed *.py var pyFiles embed.FS
Build a Venv and Start it: v, err := pyvenv.New(pyvenv.Config{ Sources: ReadAll(pyFiles), Requirements: []string{"pandas==2.2.0"}, MaxWorkers: 4, }) err = v.Start(ctx)
Call into Python and Await the result: callID, err := v.Call(ctx, "embed", map[string]any{"text": "hello"}) var out struct{ Vector []float64 } err = v.Await(ctx, callID, &out)
Or use the synchronous convenience helper: err = v.CallAndAwait(ctx, "embed", map[string]any{"text": "hello"}, &out)
Tear down: v.Close(ctx)
Index ¶
- Variables
- type Config
- type LivenessCallback
- type Logger
- type State
- type Venv
- func (v *Venv) Await(ctx context.Context, callID string, result any) error
- func (v *Venv) Call(ctx context.Context, funcName string, args any) (callID string, err error)
- func (v *Venv) CallAndAwait(ctx context.Context, funcName string, args, result any) error
- func (v *Venv) Close(ctx context.Context) error
- func (v *Venv) Ready() bool
- func (v *Venv) Result(ctx context.Context, callID string, result any) (ready bool, err error)
- func (v *Venv) Start(ctx context.Context) error
- func (v *Venv) TailStdErr() []byte
- func (v *Venv) TailStdOut() []byte
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotReady is returned when Call is invoked before Start has succeeded. ErrNotReady = errors.New("pyvenv: not started") // ErrClosed is returned when Call, Await, or Start is invoked after Close. ErrClosed = errors.New("pyvenv: closed") // ErrDied is returned when the Python subprocess exited unexpectedly. ErrDied = errors.New("pyvenv: python subprocess exited unexpectedly") // ErrUnknown is returned by Await or Result when the callID is not known to the Venv — // either because it was never issued, the result was already consumed by a prior Await, // or the result aged out of the cache. ErrUnknown = errors.New("pyvenv: unknown callID") )
Sentinel errors returned from Call, Await, Result, Start, and helpers.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// PythonInterpreter is the bootstrap interpreter used once at venv creation
// (`<interp> -m venv ...`). After bootstrap the venv has its own pinned interpreter.
// Default: "python3" resolved via $PATH.
PythonInterpreter string
// Sources is the list of Python source files to load into the worker, in order.
// The module concatenates them and sends one `define` frame; the caller decides
// ordering (typical convention: helpers first, then a main service.py last so it
// can reference earlier names).
Sources []string
// Requirements is the list of pip packages to install at Start time. Empty list
// skips the pip install pass. Identical to the lines of a requirements.txt; commas
// and version specifiers in entries are passed through to pip as-is.
Requirements []string
// MaxWorkers is the size of the Python ThreadPoolExecutor that serves concurrent
// Call requests. Default 1. Use higher values only when the Python code is
// thread-safe and benefits from concurrent dispatch.
MaxWorkers int
// BaseDir is the on-disk directory holding the venv tree (`<dir>/venv/`) and the
// worker output buffers (`<dir>/meta/`). Default: a temp directory created by the
// module on first Start.
BaseDir string
// OutputBufferKB sizes each of the two ring files per stream. Each Venv has
// 2 * stream-count files (stdout-a, stdout-b, stderr-a, stderr-b). Default 256.
OutputBufferKB int
// ResultCacheTTL caps how long a completed-but-not-yet-Awaited call result lingers
// before being evicted. Default 15 minutes. Set to a negative value to disable
// eviction entirely (results stay until consumed, the Venv dies, or it is Closed).
ResultCacheTTL time.Duration
// Logger receives diagnostic messages. nil → logs to stderr via [log.Default].
Logger Logger
// LivenessCallback observes async lifecycle transitions. nil disables the hook.
LivenessCallback LivenessCallback
}
Config configures a Venv. Zero values are sensible defaults.
type LivenessCallback ¶
LivenessCallback is invoked on async lifecycle transitions. The module invokes the callback from a goroutine it owns; do not block. A single Venv may invoke its callback multiple times across its lifetime (Ready then Died then Ready after a successful Start retry).
type Logger ¶
type Logger interface {
LogInfo(ctx context.Context, msg string, kv ...any)
LogError(ctx context.Context, msg string, kv ...any)
}
Logger is the destination for diagnostic logs (subprocess lifecycle, pip output, worker crashes). The minimal surface lets callers wrap any sink (slog, log, a framework's own logger) with a small adapter.
type State ¶
type State int
State identifies an async lifecycle transition observed by a LivenessCallback.
const ( // StateReady fires when [Venv.Start] has successfully spawned the worker and the worker // has emitted its initial ready frame. StateReady State = iota // StateDied fires when the subprocess exits unexpectedly. Subsequent [Venv.Call] returns // [ErrDied] until [Venv.Start] is called again or [Venv.Close] terminates the Venv. StateDied )
type Venv ¶
type Venv struct {
// contains filtered or unexported fields
}
Venv owns one long-lived Python subprocess and its on-disk virtualenv. Safe for concurrent Call from multiple goroutines.
func New ¶
New constructs a Venv. The Python subprocess is not spawned; the caller must call Venv.Start.
func (*Venv) Await ¶
Await blocks until the call identified by callID completes or ctx expires. On successful delivery the result is unmarshaled into *result and the entry is consumed — subsequent Awaits on the same callID return ErrUnknown. On Python failure, Await returns an error whose message includes the Python exception type and message and the entry is consumed. On ctx expiry, Await returns ctx.Err() without consuming the entry; a later Await may re-enter the wait.
Returns ErrUnknown when callID is not known to this Venv (never issued, already consumed, or evicted by the result cache's TTL).
Pass result = nil when the caller does not care about the unmarshaled value.
func (*Venv) Call ¶
Call asynchronously invokes a named Python function with the given JSON-marshaled args. Returns a callID that the caller passes to Venv.Await or Venv.Result to retrieve the result. Call does not block on Python execution; it validates state, registers the call, writes one frame, and returns.
The Python work runs to completion in the worker regardless of whether the caller's ctx expires before Await returns. A subsequent Await(callID) — including one from a separate goroutine or after a workflow-task retry that persisted the callID — re-enters the wait and receives the result once Python finishes.
May be called from multiple goroutines concurrently. Each in-flight Call consumes one slot in the Python Config.MaxWorkers thread pool; additional calls queue inside the worker.
func (*Venv) CallAndAwait ¶
CallAndAwait is the synchronous convenience shape: Call followed by Await on the same goroutine. Use it when the caller is happy to block for the duration of the Python work and does not need to hand the callID across a context boundary (e.g. a workflow task retry, or a separate progress-tracking goroutine).
func (*Venv) Close ¶
Close kills the subprocess, closes the ring writers, removes the on-disk venv directory, and marks the Venv terminal. In-flight Await returns ErrClosed. Idempotent.
func (*Venv) Result ¶
Result is the non-blocking sibling of Venv.Await. If a terminal result is available it is consumed and unmarshaled into *result (or returned via err when Python raised). If the call is still running, returns ready=false, err=nil without consuming. If callID is unknown, returns ready=false, err=[ErrUnknown].
As with Await, a successful delivery (ready=true) consumes the entry; subsequent Result or Await on the same callID returns ErrUnknown.
func (*Venv) Start ¶
Start creates the on-disk venv (skipping the create step if one is already present at BaseDir/venv), runs `pip install` if Config.Requirements is non-empty, spawns the worker, loads the embedded sources, and waits for the worker to emit its initial ready frame. Returns when the worker is ready or ctx expires. Safe to retry after StateDied: the on-disk venv is reused, pip install is skipped if the install marker is current, and a fresh worker is spawned.
func (*Venv) TailStdErr ¶
TailStdErr returns the most recent stderr from the worker.
func (*Venv) TailStdOut ¶
TailStdOut returns the most recent stdout from the worker, up to 2 * OutputBufferKB bytes. Pull-based; cheap. Returns nil before Start and after Close.