Skip to content

API reference

The complete, per-symbol reference for the public processkit surface — every class, function, protocol, type alias, and exception exported by the package, plus the processkit.testing submodule.

It is generated from the type stub (processkit/_processkit.pyi) and the docstrings, the same source your IDE and mypy read, so it cannot drift from the real API. The narrative guides explain how the pieces compose; this page is the exhaustive index. Both surfaces are covered together: the synchronous verbs and their a-prefixed asyncio twins.

Building & running commands

Construct a command and run it — capturing everything, or checking for success — synchronously or with the a-prefixed asyncio twins. CliClient binds a program to reusable defaults; Pipeline chains commands shell-free; RunningProcess is the live handle a started child hands back.

Command

A command builder. Builder methods return a new Command.

program property

program: str

arguments property

arguments: list[str]

__init__

__init__(program: StrPath, args: Args | None = ...) -> None

arg

arg(arg: StrPath) -> Command

args

args(args: Args) -> Command

cwd

cwd(path: StrPath) -> Command

prefer_local

prefer_local(dir: StrPath) -> Command

Search this directory before PATH when resolving a bare-name program. Repeated calls accumulate in priority order, path-form programs are unchanged, and the child's own PATH is not rewritten.

env

env(key: str, value: str) -> Command

envs

envs(vars: Mapping[str, str]) -> Command

env_remove

env_remove(key: str) -> Command

env_clear

env_clear() -> Command

inherit_env

inherit_env(names: Sequence[str]) -> Command

stdin_bytes

stdin_bytes(data: ReadableBuffer) -> Command

stdin_text

stdin_text(text: str) -> Command

stdin_file

stdin_file(path: StrPath) -> Command

keep_stdin_open

keep_stdin_open() -> Command

timeout

timeout(seconds: float) -> Command

timeout_grace

timeout_grace(seconds: float) -> Command

timeout_signal

timeout_signal(name: SignalName | int) -> Command

The signal sent first on a graceful timeout (default "term"): a name (term/kill/int/hup/quit/usr1/usr2) or a raw platform signal number (Unix only). A raw number is validated as a real, deliverable signal — on Unix 1..=SIGRTMAX (0, the existence probe that delivers nothing, negatives, and out-of-range raise ValueError); on Windows a raw number raises Unsupported (only "kill" is deliverable there). A bool raises TypeError — it is an int subtype that would otherwise silently become raw signal 1/0.

no_timeout

no_timeout() -> Command

timeout_opt

timeout_opt(seconds: float | None) -> Command

cancel_on

cancel_on(token: CancellationToken) -> Command

success_codes

success_codes(codes: Sequence[int]) -> Command

retry

retry(
    retry_if: RetryIf,
    *,
    max_retries: int | None = ...,
    initial_backoff: float | None = ...,
    multiplier: float | None = ...,
    max_backoff: float | None = ...,
    jitter: bool | None = ...,
) -> Command

retry_never

retry_never() -> Command

stdout

stdout(mode: Literal['pipe', 'inherit', 'null']) -> Command

stderr

stderr(mode: Literal['pipe', 'inherit', 'null']) -> Command

encoding

encoding(label: str) -> Command

stdout_encoding

stdout_encoding(label: str) -> Command

stderr_encoding

stderr_encoding(label: str) -> Command

line_terminator

line_terminator(mode: LineTerminatorName) -> Command

Choose where the line pump splits both streams into lines. "newline" (the default) splits on \n only; "carriage_return" also splits on a bare \r (one not immediately followed by \n), delivered live — for curl/pip/apt-style \r-redrawn progress output that would otherwise pile up into a single line until EOF. A \r\n pair still counts as one terminator. Shared by stdout_lines()/output_events(), the per-line handlers (on_stdout_line/on_stderr_line), stdout_tee/stderr_tee, and output_string alike; set both streams here or independently with stdout_line_terminator/stderr_line_terminator. Unknown preset raises ValueError.

stdout_line_terminator

stdout_line_terminator(mode: LineTerminatorName) -> Command

Choose where the line pump splits stdout into lines (see line_terminator); stderr framing is left untouched.

stderr_line_terminator

stderr_line_terminator(mode: LineTerminatorName) -> Command

Choose where the line pump splits stderr into lines (see line_terminator); stdout framing is left untouched. Handy when progress output lands on stderr while stdout stays newline-structured.

stdout_tee

stdout_tee(
    sink: StrPath | SupportsWrite, *, append: bool = ...
) -> Command

Tee every decoded stdout line (line + \n) to sink as it is produced, while the run also keeps capturing the full output (the sink does not steal from ProcessResult.stdout).

sink is either a file path (str / os.PathLike[str]) or a Python writer — any object with a callable write() (an io.StringIO, sys.stderr, a text-mode file, a logger wrapper), picked apart by whether it exposes write (neither str nor pathlib.Path does).

  • File path: teed as raw UTF-8 bytes, opened at build time (not at run) — created if absent and truncated, or append mode when append=True; an unopenable path raises the matching OSError subclass right here.
  • Writer: each decoded line (then "\n") is passed to write() as a str (a text sink — a binary writer whose write(str) raises TypeError is the wrong object here). Every write is dispatched to a blocking thread and awaited on the pump, so a slow write() applies backpressure without blocking the event loop; the object is not closed for you. append is meaningless for a writer — passing append=True with one raises ValueError.

A write error disables the tee for the rest of the run (a tracing warning under enable_logging()) while the run and its captured result continue unaffected; a writer's write() exception is additionally reported via sys.unraisablehook. Inert unless stdout is piped through the line pump — a no-op under stdout("inherit") / stdout("null") and under output_bytes() (raw capture).

stderr_tee

stderr_tee(
    sink: StrPath | SupportsWrite, *, append: bool = ...
) -> Command

Tee every decoded stderr line to sink. Same contract as stdout_tee — a file path (opened at build time, truncate by default or append) or a Python writer object with a callable write() (fed each decoded line as a str via the same blocking-pool async-write bridge, never closed for you), coexisting with capture, inert unless stderr is piped through the line pump.

on_stdout_line

on_stdout_line(callback: Callable[[str], None]) -> Command

Call callback with every decoded stdout line as it is produced — the way to give the synchronous surface (.output()/.run()) live progress observation during an otherwise-blocking call, without losing the full capture: callback observes the same decoded lines that land in ProcessResult.stdout, it does not replace them. Also fires on the async verbs and on a streamed run (start()/ astart() + stdout_lines()/output_events()) — one callback, every path.

callback is infallible: an exception raised inside it is reported via sys.unraisablehook rather than propagated — it never derails the run or alters the captured result. At most one handler per stream — a repeat call replaces the previous one (builder semantics, like timeout()); compose inside one callable to fan out.

Inert under stdout("inherit")/stdout("null") (no pump runs) and under output_bytes() (stdout is captured raw there, bypassing the line pump).

on_stderr_line

on_stderr_line(callback: Callable[[str], None]) -> Command

Call callback with every decoded stderr line as it is produced. Same contract as on_stdout_line — full capture unaffected, fires on sync/async/streamed paths alike, infallible (a raising callback goes to sys.unraisablehook, never propagates), at most one handler per stream.

Inert under stderr("inherit")/stderr("null"). Unlike on_stdout_line, not silenced by output_bytes(): that verb only bypasses the stdout line pump for its raw-bytes capture — stderr still decodes through the line pump exactly as under output(), so this callback still fires.

kill_on_parent_death

kill_on_parent_death() -> Command

create_no_window

create_no_window() -> Command

uid

uid(uid: int) -> Command

gid

gid(gid: int) -> Command

groups

groups(gids: Sequence[int]) -> Command

setsid

setsid() -> Command

umask

umask(mask: int) -> Command

priority

priority(level: Priority) -> Command

output_limit

output_limit(
    *,
    max_bytes: int | None = ...,
    max_lines: int | None = ...,
    on_overflow: Literal[
        "drop_oldest", "drop_newest", "error"
    ] = ...,
) -> Command

output

output() -> ProcessResult

output_bytes

output_bytes() -> BytesResult

run

run() -> str

exit_code

exit_code() -> int

probe

probe() -> bool

aoutput async

aoutput() -> ProcessResult

aoutput_bytes async

aoutput_bytes() -> BytesResult

arun async

arun() -> str

aexit_code async

aexit_code() -> int

aprobe async

aprobe() -> bool

start

start() -> RunningProcess

astart async

astart() -> RunningProcess

unchecked_in_pipe

unchecked_in_pipe() -> Command

command_line

command_line() -> str

pipe

pipe(other: Command) -> Pipeline

__or__

__or__(other: Command) -> Pipeline

__repr__

__repr__() -> str

CliClient

A program bound to default timeout/env/retry, run with the real Runner by default or an injected runner= (a ScriptedRunner and friends, for testable code with no real spawns). The verbs take just the per-call arguments.

__init__

__init__(
    program: StrPath,
    *,
    default_timeout: float | None = ...,
    default_env: Mapping[str, str] | None = ...,
    default_env_remove: Sequence[str] | None = ...,
    default_env_fn: Mapping[str, Callable[[], str]]
    | None = ...,
    default_retry_if: RetryIf | None = ...,
    default_max_retries: int | None = ...,
    default_initial_backoff: float | None = ...,
    default_multiplier: float | None = ...,
    default_max_backoff: float | None = ...,
    default_jitter: bool | None = ...,
    default_cancel_on: CancellationToken | None = ...,
    runner: RunnerLike | None = ...,
) -> None

command

command(args: Args) -> Command

A Command for program <args>, the client's defaults pre-applied — chain more builders, then pass it to a verb below instead of a plain arg list. An explicit setting on it always wins over the default.

run

run(call: Args | Command) -> str

output

output(call: Args | Command) -> ProcessResult

output_bytes

output_bytes(call: Args | Command) -> BytesResult

exit_code

exit_code(call: Args | Command) -> int

probe

probe(call: Args | Command) -> bool

arun async

arun(call: Args | Command) -> str

aoutput async

aoutput(call: Args | Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(call: Args | Command) -> BytesResult

aexit_code async

aexit_code(call: Args | Command) -> int

aprobe async

aprobe(call: Args | Command) -> bool

__repr__

__repr__() -> str

Pipeline

A shell-free pipeline a | b | c.

By design, no start/astart — see Command.pipe()'s stub/binding comment: a pipeline is a whole-chain verb, with no natural "handle to a live chain" to hand back. Stream an individual stage by start()ing that one Command directly instead.

pipe

pipe(other: Command) -> Pipeline

__or__

__or__(other: Command) -> Pipeline

timeout

timeout(seconds: float) -> Pipeline

cancel_on

cancel_on(token: CancellationToken) -> Pipeline

output

output() -> ProcessResult

output_bytes

output_bytes() -> BytesResult

run

run() -> str

exit_code

exit_code() -> int

probe

probe() -> bool

aoutput async

aoutput() -> ProcessResult

aoutput_bytes async

aoutput_bytes() -> BytesResult

arun async

arun() -> str

aexit_code async

aexit_code() -> int

aprobe async

aprobe() -> bool

__repr__

__repr__() -> str

RunningProcess

A handle to a started process: stream output, write stdin, wait for exit.

Usable as a (async) context manager — exiting the block tears the process down (a hard kill of the whole private tree for a standalone start()/astart() handle). stdout_lines() / output_events() / take_stdin() / kill() are synchronous setup calls; the iterator / handle they return is what you await.

Every consuming verb — outcome/finish/output/output_bytes/ profile/shutdown — comes in a sync/async pair, like everywhere else in this library: the bare name blocks the calling thread (via the same interruptible driver as Command.output()), the a-prefixed twin is a coroutine (outcome/aoutcome rather than the unusable wait/ await, since await is a reserved word). Either member of a pair consumes the handle — afterwards it is spent (pid and the other getters return None, and every consuming verb raises). Use whichever matches your calling code, regardless of whether the handle came from start() or astart().

pid property

pid: int | None

elapsed_seconds property

elapsed_seconds: float | None

cpu_time_seconds property

cpu_time_seconds: float | None

peak_memory_bytes property

peak_memory_bytes: int | None

stdout_line_count property

stdout_line_count: int | None

stderr_line_count property

stderr_line_count: int | None

owns_group property

owns_group: bool | None

__enter__

__enter__() -> RunningProcess

__exit__

__exit__(
    exc_type: type[BaseException] | None = ...,
    exc_value: BaseException | None = ...,
    traceback: TracebackType | None = ...,
) -> Literal[False]

__aenter__ async

__aenter__() -> RunningProcess

__aexit__ async

__aexit__(
    exc_type: type[BaseException] | None = ...,
    exc_value: BaseException | None = ...,
    traceback: TracebackType | None = ...,
) -> Literal[False]

stdout_lines

stdout_lines() -> StdoutLines

output_events

output_events() -> OutputEvents

take_stdin

take_stdin() -> ProcessStdin

The writable stdin handle. Raises ProcessError if stdin was not kept open (build the Command with keep_stdin_open()) or was already taken — so a missing setup fails here, not with a later AttributeError.

kill

kill() -> None

Begin tearing the tree down without waiting (like subprocess.Popen.kill(): fire-and-forget).

outcome

outcome() -> Outcome

aoutcome async

aoutcome() -> Outcome

finish

finish() -> Finished

afinish async

afinish() -> Finished

output

output() -> ProcessResult

aoutput async

aoutput() -> ProcessResult

output_bytes

output_bytes() -> BytesResult

aoutput_bytes async

aoutput_bytes() -> BytesResult

profile

profile(every_seconds: float) -> RunProfile

aprofile async

aprofile(every_seconds: float) -> RunProfile

shutdown

shutdown(grace_seconds: float) -> Outcome

Graceful teardown (signal -> wait grace_seconds -> hard kill), returning the Outcome; consumes the handle. Only for a standalone start()/astart() handle — a handle from ProcessGroup.start() raises Unsupported; tear such a child down via the group (or kill()). Named to match ProcessGroup.shutdown()/ashutdown().

ashutdown async

ashutdown(grace_seconds: float) -> Outcome

Async counterpart of shutdown.

__repr__

__repr__() -> str

Results & outcomes

What a finished (or streamed) run reports back. A non-zero exit, a timeout, and a signal-kill are all data on these types — never raised by the capturing verbs.

ProcessResult

The captured result of a finished run. A non-zero exit, a timeout, and a signal-kill are all reported as data here — never raised by output().

Value semantics: ==/hash() compare every field (program/stdout/stderr/ outcome/success codes — not the incidental duration_seconds/truncated). Not picklable: equality also spans the configured timeout and accepted success_codes, which processkit exposes no accessor to read back, so a pickled result could not reconstruct them and would compare unequal to its original for any command that set .timeout(...)/.success_codes(...); pickling raises TypeError. Pickle result.outcome (an Outcome, which round-trips exactly — e.g. to return it from a concurrent.futures.ProcessPoolExecutor worker), or persist result.stdout/.stderr/.code yourself, to cross a process boundary.

stdout property

stdout: str

stderr property

stderr: str

code property

code: int | None

is_success property

is_success: bool

timed_out property

timed_out: bool

signal property

signal: int | None

program property

program: str

duration_seconds property

duration_seconds: float

truncated property

truncated: bool

combined property

combined: str

diagnostic property

diagnostic: str | None

The best human-facing message: stderr if it carries text, otherwise stdout, otherwise None if both are blank — the same preference order as NonZeroExit/Timeout/Signalled.diagnostic.

outcome property

outcome: Outcome

The full run outcome (code / signal / timed_out), the same value RunProfile.outcome and the checking-verb exceptions expose.

ensure_success

ensure_success() -> ProcessResult

Raise the same exception a checking verb would if this result's exit isn't in success_codes; returns self unchanged otherwise, so it composes: cmd.output().ensure_success().stdout.

__repr__

__repr__() -> str

__eq__

__eq__(value: object) -> bool

__hash__

__hash__() -> int

BytesResult

The captured result of a run with raw-bytes stdout (Command.output_bytes()); stderr stays decoded text. A non-zero exit, a timeout, and a signal-kill are all data here, never raised.

Value semantics: ==/hash() compare every field, same as ProcessResult. Not picklable — raw stdout may not be valid UTF-8 and processkit has no way to reconstruct one from arbitrary bytes outside a real run; pickling raises TypeError. Pickle a ProcessResult (Command.output()) instead, or persist the fields you need yourself.

stdout property

stdout: bytes

stderr property

stderr: str

code property

code: int | None

is_success property

is_success: bool

timed_out property

timed_out: bool

signal property

signal: int | None

program property

program: str

duration_seconds property

duration_seconds: float

truncated property

truncated: bool

Whether captured output was truncated by an output_limit(...) cap — the line-captured stderr under any cap, and (since processkit 2.1.0) the raw stdout too when an output_limit(max_bytes=...) byte ceiling bounds it to a head/tail. A max_lines cap never truncates raw stdout (bytes have no line count); only a max_bytes cap does.

diagnostic property

diagnostic: str | None

See ProcessResult.diagnostic. Raw stdout is lossily decoded to text for this message when stderr is blank.

outcome property

outcome: Outcome

See ProcessResult.outcome.

ensure_success

ensure_success() -> BytesResult

See ProcessResult.ensure_success().

__repr__

__repr__() -> str

__eq__

__eq__(value: object) -> bool

__hash__

__hash__() -> int

Outcome

How a process ended.

There is no is_success here on purpose: an Outcome carries no success_codes context, so it cannot give the command's own success verdict the way ProcessResult.is_success does. Use exited_zero for the literal "exit code 0" test, or compare code against your accepted set.

Value semantics: ==/hash() compare code/signal/timed_out (equivalently, which variant this is and its payload); picklable.

code property

code: int | None

signal property

signal: int | None

timed_out property

timed_out: bool

exited_zero property

exited_zero: bool

__repr__

__repr__() -> str

__eq__

__eq__(value: object) -> bool

__hash__

__hash__() -> int

Finished

A process's outcome plus captured stderr (stdout was streamed).

Mirrors Outcome's code, exited_zero, timed_out, and signal directly (in addition to the nested outcome), so callers don't need to reach through .outcome for fields they already use on Outcome. Like Outcome, it exposes exited_zero (literal "exit code 0"), not an is_success that would falsely imply success_codes were considered.

Value semantics: ==/hash() compare outcome/stderr; picklable.

outcome property

outcome: Outcome

stderr property

stderr: str

code property

code: int | None

exited_zero property

exited_zero: bool

timed_out property

timed_out: bool

signal property

signal: int | None

__repr__

__repr__() -> str

__eq__

__eq__(value: object) -> bool

__hash__

__hash__() -> int

RunProfile

A resource-usage profile sampled across a run (RunningProcess.profile), plus the run's outcomeprofile() is a superset of outcome().

Value semantics: ==/hash() compare every field (outcome/ duration_seconds/cpu_time_seconds/peak_memory_bytes/samples; all exact underneath, though two are exposed here as float). Not picklable — it reports live OS resource-sampling telemetry that processkit has no way to reconstruct outside an actual monitored run; pickling raises TypeError.

code property

code: int | None

signal property

signal: int | None

timed_out property

timed_out: bool

outcome property

outcome: Outcome

duration_seconds property

duration_seconds: float

cpu_time_seconds property

cpu_time_seconds: float | None

peak_memory_bytes property

peak_memory_bytes: int | None

samples property

samples: int

avg_cpu_cores property

avg_cpu_cores: float | None

__repr__

__repr__() -> str

__eq__

__eq__(value: object) -> bool

__hash__

__hash__() -> int

Streaming & interactive I/O

The live handles a started RunningProcess hands out: async iterators over its output (line by line, or as interleaved stdout/stderr events) and a writable stdin.

StdoutLines

Async iterator over a process's stdout, line by line.

__aiter__

__aiter__() -> AsyncIterator[str]

__anext__ async

__anext__() -> str

OutputEvents

Async iterator over stdout + stderr as interleaved OutputEvents.

__aiter__

__aiter__() -> AsyncIterator[OutputEvent]

__anext__ async

__anext__() -> OutputEvent

OutputEvent

One captured line and the stream it came from.

stream property

stream: Literal['stdout', 'stderr']

is_stderr property

is_stderr: bool

text property

text: str

__repr__

__repr__() -> str

ProcessStdin

A writable handle to a running process's stdin (all methods awaitable).

write async

write(data: ReadableBuffer) -> None

write_line async

write_line(line: str) -> None

send_control async

send_control(control: str) -> None

Write one mapped control byte, e.g. "c" -> Ctrl-C (\x03).

This writes a byte to the child's stdin pipe, not a terminal signal; real SIGINT/SIGTSTP delivery requires a pseudoterminal.

flush async

flush() -> None

close async

close() -> None

Process groups

Kill-on-drop containment for a whole process tree — start children into it, signal or suspend the group, and reap the entire tree (grandchildren included) on exit.

ProcessGroup

A kill-on-drop container for a process tree; use as a (async) context manager. Also a ProcessRunner in its own right (see _RunnerVerbs): its run verbs run command as a shared member of this group (not a standalone tree) — the same verb surface Runner/ScriptedRunner/… expose (not an extract_runner target, though — see runner.rs).

mechanism property

mechanism: Literal[
    "job_object", "cgroup_v2", "process_group", "unknown"
]

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

start

start(command: Command) -> RunningProcess

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

astart async

astart(command: Command) -> RunningProcess

__init__

__init__(
    *,
    max_memory: int | None = ...,
    max_processes: int | None = ...,
    cpu_quota: float | None = ...,
    shutdown_grace: float | None = ...,
    escalate_to_kill: bool | None = ...,
) -> None

Resource limits need a Windows Job Object or a Linux cgroup-v2 root: max_memory is bytes (whole tree), max_processes a count, and cpu_quota a fraction of a single core (0.5 = half a core, 2.0 = two cores) — not a share of all cores. shutdown_grace is the seconds to wait after signalling before escalating to a hard kill; escalate_to_kill (default on) is whether that hard kill follows once the grace elapses — set False to leave any survivors instead of force-killing them.

__enter__

__enter__() -> ProcessGroup

__exit__

__exit__(
    exc_type: type[BaseException] | None = ...,
    exc_value: BaseException | None = ...,
    traceback: TracebackType | None = ...,
) -> Literal[False]

__aenter__ async

__aenter__() -> ProcessGroup

__aexit__ async

__aexit__(
    exc_type: type[BaseException] | None = ...,
    exc_value: BaseException | None = ...,
    traceback: TracebackType | None = ...,
) -> Literal[False]

members

members() -> list[int]

signal

signal(name: SignalName | int) -> None

Send a signal to every process in the tree: a name (term/kill/int/hup/quit/usr1/usr2) or a raw platform signal number (Unix only). On Windows a Job Object has no POSIX signals, so only "kill" is deliverable and any other name/number raises Unsupported. A raw number is validated as a real, deliverable signal (1..=SIGRTMAX on Unix); 0 (the existence probe), negatives, and out-of-range values raise ValueError instead of a silent no-op, and a bool raises TypeError.

suspend

suspend() -> None

resume

resume() -> None

kill_all

kill_all() -> None

stats

stats() -> ProcessGroupStats

shutdown

shutdown() -> None

ashutdown async

ashutdown() -> None

__repr__

__repr__() -> str

ProcessGroupStats

A snapshot of a ProcessGroup's resource usage.

active_process_count property

active_process_count: int

peak_memory_bytes property

peak_memory_bytes: int | None

total_cpu_time_seconds property

total_cpu_time_seconds: float | None

__repr__

__repr__() -> str

Supervision

Keep a command alive: restart it per a policy, with backoff and jitter, until a stop condition is met.

Supervisor

Keep a command alive: restart per policy with backoff until a stop condition.

__init__

__init__(
    command: Command,
    *,
    restart: Literal["always", "on_crash", "never"]
    | None = ...,
    max_restarts: int | None = ...,
    backoff_initial: float | None = ...,
    backoff_factor: float | None = ...,
    max_backoff: float | None = ...,
    jitter: bool | None = ...,
    stop_when: Callable[[ProcessResult], bool] | None = ...,
    give_up_when: Callable[
        [ProcessResult | ProcessError], bool
    ]
    | None = ...,
    storm_pause: float | None = ...,
    failure_threshold: float | None = ...,
    failure_decay: float | None = ...,
    capture_max_bytes: int | None = ...,
    capture_max_lines: int | None = ...,
    capture_on_overflow: Literal[
        "drop_oldest", "drop_newest", "error"
    ]
    | None = ...,
    runner: RunnerLike | None = ...,
) -> None

run

run() -> SupervisionOutcome

arun async

arun() -> SupervisionOutcome

SupervisionOutcome

The result of a Supervisor.run().

Value semantics: ==/hash() compare every field (final_result via ProcessResult's own comparison, plus restarts/stopped/storm_pauses). Not picklable: its identity includes final_result (a ProcessResult), which cannot be faithfully reconstructed from a pickle (its timeout/ success_codes have no accessor to read back), so pickling raises TypeError. Read the fields you need, or pickle final_result.outcome (an Outcome, which round-trips exactly), to cross a process boundary.

final_result property

final_result: ProcessResult

restarts property

restarts: int

stopped property

stopped: Literal[
    "policy_satisfied",
    "predicate",
    "restarts_exhausted",
    "gave_up",
    "unknown",
]

storm_pauses property

storm_pauses: int

__repr__

__repr__() -> str

__eq__

__eq__(value: object) -> bool

__hash__

__hash__() -> int

Cancellation

A portable cancel switch, wired into a run via Command.cancel_on(), Pipeline.cancel_on(), or CliClient's default_cancel_on=.

CancellationToken

A cancel switch: fire it to tear down every run wired to it via Command.cancel_on() / CliClient's default_cancel_on= / Pipeline.cancel_on() — surfacing Cancelled. Cheap to clone/share: every clone refers to the same underlying state, so cancelling any clone cancels every run wired to it. A cancelled token stays cancelled forever.

child_token() derives a separate, scoped token: it is cancelled automatically when this one is, but cancelling it back does NOT propagate to this token or to its other children — cancellation only flows parent-to-child, never child-to-parent or between siblings.

__init__

__init__() -> None

cancel

cancel() -> None

is_cancelled

is_cancelled() -> bool

child_token

child_token() -> CancellationToken

A new token that is cancelled automatically when this one is, but can also be cancelled independently — cancelling the child does not affect this token or its other children.

__repr__

__repr__() -> str

Batch execution

Run many commands with bounded concurrency, returning each result — or a ProcessError for a spawn/I/O failure — in input order.

output_all

output_all(
    commands: Sequence[Command],
    *,
    concurrency: int | None = ...,
    runner: RunnerLike | None = ...,
) -> list[ProcessResult | ProcessError]

output_all_bytes

output_all_bytes(
    commands: Sequence[Command],
    *,
    concurrency: int | None = ...,
    runner: RunnerLike | None = ...,
) -> list[BytesResult | ProcessError]

aoutput_all async

aoutput_all(
    commands: Sequence[Command],
    *,
    concurrency: int | None = ...,
    runner: RunnerLike | None = ...,
) -> list[ProcessResult | ProcessError]

aoutput_all_bytes async

aoutput_all_bytes(
    commands: Sequence[Command],
    *,
    concurrency: int | None = ...,
    runner: RunnerLike | None = ...,
) -> list[BytesResult | ProcessError]

Readiness helpers

Asyncio helpers that wait for a condition — a matching output line, an open TCP port, a filesystem path, or any polled predicate — bounded by a deadline.

wait_until async

wait_until(
    predicate: Callable[[], bool | Awaitable[bool]],
    *,
    timeout: float,
    interval: float = 0.05,
) -> None

Poll predicate until it returns true, or timeout seconds elapse.

(Named wait_until, not wait_for — the latter would collide with asyncio.wait_for, whose semantics differ: it bounds one awaitable, not a polled predicate.)

predicate may be synchronous or return an awaitable. Polls every interval seconds; raises WaitTimeout (also a TimeoutError) if the deadline passes first. A synchronous predicate runs on the event loop, so keep it non-blocking — use an async predicate for anything that does I/O. If predicate's awaitable is already a asyncio.Future/asyncio.Task you own, note it is never cancelled by this helper on timeout — only abandoned, so cancel or await it yourself afterwards if that matters.

timeout<=0 contract (shared with wait_for_port / wait_for_line): at timeout=0, predicate is still evaluated (at least once) before any deadline check, so an already-true predicate succeeds instead of failing before it was ever checked. A negative timeout is rejected outright — raises ValueError, same as NaN — rather than being treated as "expired" or silently accepted.

wait_for_line async

wait_for_line(
    lines: AsyncIterator[str],
    predicate: str,
    *,
    timeout: float,
) -> str
wait_for_line(
    lines: AsyncIterator[_Item],
    predicate: Callable[[_Item], bool],
    *,
    timeout: float,
) -> _Item
wait_for_line(
    lines: AsyncIterator[Any],
    predicate: str | Callable[[Any], bool],
    *,
    timeout: float,
) -> Any

Consume from an async iterator until predicate matches an item.

predicate is either a callable (predicate(item) -> bool) or, for a str-yielding iterator only, a plain str — a shorthand for "the item contains this substring" (predicate in item). Not just for StdoutLines: any async iterator works (e.g. OutputEvents, with a callable predicate over its OutputEvent items).

Returns the matching item. Raises WaitTimeout (also a TimeoutError, carrying timeout_seconds) if nothing matches within timeout seconds, or propagates whatever predicate or the iterator itself raised (a ProcessError if the stream ends first) untouched — never masked behind the timeout. Items read before the match are consumed; iteration may continue afterward only when a match was found — on a WaitTimeout, exactly how far the iterator advanced past the last inspected item is unspecified (cancellation of the internal scan races the iterator's own advancement), so don't rely on its position after a timeout.

timeout<=0 contract (shared with wait_until / wait_for_port): at timeout=0, the iterator is still scanned (at least one tick), so an item that already matches (already sitting in the iterator) succeeds instead of failing before it was ever inspected. A negative timeout is rejected outright — raises ValueError, same as NaN — rather than being treated as "expired" or silently accepted.

wait_for_port async

wait_for_port(
    host: str,
    port: int,
    *,
    timeout: float,
    interval: float = 0.05,
) -> None

Wait until a TCP connection to (host, port) succeeds.

Polls every interval seconds until the port accepts a connection or timeout seconds elapse, in which case WaitTimeout (also a TimeoutError) is raised — carrying host/port — chained from the last connection attempt's exception (e.g. a DNS failure survives as the cause instead of being silently dropped).

timeout<=0 contract (shared with wait_until / wait_for_line): at timeout=0, a connection attempt is still made (at least one), so an already-ready port succeeds instead of failing before a connection was ever tried — this first attempt is not cut short by the already-expired deadline. It IS bounded, though: to a short, fixed event-loop tick (or a smaller caller-supplied interval), not left uncapped — an unresolvable/blackhole address would otherwise be free to block on the OS's own (much longer, or absent) connect/DNS timeout well past the caller's requested deadline. A negative timeout is rejected outright — raises ValueError, same as NaN — rather than being treated as "expired" or silently accepted.

wait_for_path async

wait_for_path(
    path: StrPath, *, timeout: float, interval: float = 0.05
) -> None

Wait until path exists on the filesystem.

Polls every interval seconds until path.exists() returns true or timeout seconds elapse, in which case WaitTimeout (also a TimeoutError) is raised, carrying path. A unix-socket, a pid file, or any other marker file a daemon creates once ready are all typical uses — for a TCP port or an arbitrary predicate, see wait_for_port / wait_until instead (wait_until(lambda: path.exists(), ...) is exactly what this helper does, named for readability and given the same WaitTimeout discipline as its siblings).

timeout<=0 contract (shared with wait_until / wait_for_port / wait_for_line): at timeout=0, path is still checked (at least once) before any deadline check, so an already-existing path succeeds instead of failing before it was ever checked. A negative timeout is rejected outright — raises ValueError, same as NaN — rather than being treated as "expired" or silently accepted.

WaitTimeout

A readiness helper (wait_until / wait_for_line / wait_for_port / wait_for_path) didn't succeed within its deadline.

Also a builtin TimeoutError, so except TimeoutError catches it too — the same convention a run's own .timeout() uses (see Timeout). Always carries timeout_seconds; wait_for_port additionally sets host / port, and wait_for_path sets path (all None for wait_until / wait_for_line, which have none of these) and chains the last connection attempt's exception as __cause__ (wait_for_port only).

timeout_seconds instance-attribute

timeout_seconds = timeout_seconds

host instance-attribute

host = host

port instance-attribute

port = port

path instance-attribute

path = path

__init__

__init__(
    message: str,
    *,
    timeout_seconds: float,
    host: str | None = None,
    port: int | None = None,
    path: StrPath | None = None,
) -> None

Observability

Opt-in bridging of the core's per-run tracing events to Python logging.

enable_logging

enable_logging() -> bool

The runner seam

The dependency-injection seam: annotate your code against a protocol, inject the real Runner in production and a test double (see the Testing section) in tests. ProcessRunner is the capture/check verbs; StreamingRunner adds start/astart.

ProcessRunner

The capture/check run verbs as a structural type: output/output_bytes/ run/exit_code/probe and their a-prefixed async twins — no streaming.

Every built-in runner satisfies this (and the wider StreamingRunner). Prefer this narrower protocol when your own code only calls these verbs — a hand-rolled double then only needs to implement five verbs (times two for the async twins), not the full runner surface. CliClient also satisfies ProcessRunner: each capture/check verb accepts either per-call Args (which it combines with its bound program) or a Command (whose explicit settings win over client defaults). It is not a StreamingRunner, because it has no start/astart verbs.

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

StreamingRunner

ProcessRunner plus start/astart — the full runner verb surface, for code that also needs a live RunningProcess handle to stream.

Runner, ScriptedRunner, RecordReplayRunner, and RecordingRunner all satisfy it. A hand-rolled double can implement the capture/check verbs easily, but start/astart must return a RunningProcess, which has no public constructor — and the built-in runners are @final, so a fully-conforming custom runner in practice means wrapping one (delegating start/astart to it; use ScriptedRunner for streaming doubles).

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

start

start(command: Command) -> RunningProcess

astart async

astart(command: Command) -> RunningProcess

Runner

The real process runner — inject it for testable code.

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

start

start(command: Command) -> RunningProcess

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

astart async

astart(command: Command) -> RunningProcess

__init__

__init__() -> None

__repr__

__repr__() -> str

Exceptions

Every error raised by the package descends from ProcessError, so a single except ProcessError catches them all. Timeout, ProcessNotFound, and PermissionDenied also subclass a builtin (TimeoutError / FileNotFoundError / PermissionError, each itself an OSError), so the stdlib except clauses catch them too.

ProcessError

Base class for every error raised by this package.

NonZeroExit

run() / exit_code() got a non-zero exit.

program instance-attribute

program: str

code instance-attribute

code: int

stdout instance-attribute

stdout: str

stderr instance-attribute

stderr: str

stdout_bytes instance-attribute

stdout_bytes: bytes | None

diagnostic instance-attribute

diagnostic: str | None

Timeout

A run exceeded its configured timeout.

Also a builtin TimeoutError, so except TimeoutError catches it too — and since TimeoutError is itself an OSError subclass (as of Python 3.3), except OSError catches it as well (the same is true of ProcessNotFound/FileNotFoundError and PermissionDenied/PermissionError below — all three dual-base exceptions are transitively OSError).

program instance-attribute

program: str

timeout_seconds instance-attribute

timeout_seconds: float | None

stdout instance-attribute

stdout: str

stderr instance-attribute

stderr: str

stdout_bytes instance-attribute

stdout_bytes: bytes | None

diagnostic instance-attribute

diagnostic: str | None

Signalled

A run was killed by a signal.

program instance-attribute

program: str

signal instance-attribute

signal: int | None

stdout instance-attribute

stdout: str

stderr instance-attribute

stderr: str

stdout_bytes instance-attribute

stdout_bytes: bytes | None

diagnostic instance-attribute

diagnostic: str | None

ProcessNotFound

The program could not be found / spawned.

Also a builtin FileNotFoundError (what subprocess raises), so except FileNotFoundError catches it too.

program instance-attribute

program: str

searched instance-attribute

searched: str | None

PermissionDenied

The program could not be spawned because of insufficient permissions (e.g. a non-executable file), or a permission-denied OS error surfaced from elsewhere in the run (e.g. a group signal the OS refused).

Also a builtin PermissionError, so except PermissionError catches it too.

program instance-attribute

program: str | None

ResourceLimit

A resource limit (memory / processes / CPU) was invalid or could not be enforced by the active containment mechanism. The reason is the exception message (str(exc)); it carries no extra structured field.

Unsupported

The operation is not supported on this platform.

operation instance-attribute

operation: str

OutputTooLarge

Captured output hit an output_limit(..., on_overflow="error") ceiling.

program instance-attribute

program: str

max_lines instance-attribute

max_lines: int | None

max_bytes instance-attribute

max_bytes: int | None

total_lines instance-attribute

total_lines: int

total_bytes instance-attribute

total_bytes: int

Cancelled

The run was deliberately cancelled via a CancellationToken wired with Command.cancel_on() / CliClient's default_cancel_on= / Pipeline.cancel_on(). Terminal — never retried by Command.retry() or restarted by Supervisor (the token stays cancelled forever, so another attempt could only fail the same way).

program instance-attribute

program: str

Type aliases

Exported so your own wrappers can annotate against the same types the API accepts.

Args module-attribute

Args = (
    list[str]
    | list[Path]
    | list[os.PathLike[str]]
    | tuple[StrPath, ...]
)

LineTerminatorName module-attribute

LineTerminatorName = Literal['newline', 'carriage_return']

Priority module-attribute

Priority = Literal[
    "idle", "below_normal", "normal", "above_normal", "high"
]

ReadableBuffer module-attribute

ReadableBuffer = bytes | bytearray | memoryview

RetryIf module-attribute

RetryIf = Literal['transient', 'transient_or_timeout']

SignalName module-attribute

SignalName = Literal[
    "term", "kill", "int", "hup", "quit", "usr1", "usr2"
]

StrPath module-attribute

StrPath = str | os.PathLike[str]

Testing

Runner test doubles, in the processkit.testing submodule. Inject one in tests — all satisfy the ProcessRunner protocol — so the code under test spawns no real processes.

ScriptedRunner

A scripted test double for Runner.

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

start

start(command: Command) -> RunningProcess

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

astart async

astart(command: Command) -> RunningProcess

__init__

__init__() -> None

on

on(prefix: Args, reply: Reply) -> None

on_sequence

on_sequence(prefix: Args, replies: Sequence[Reply]) -> None

when

when(
    predicate: Callable[[Command], bool], reply: Reply
) -> None

fallback

fallback(reply: Reply) -> None

__repr__

__repr__() -> str

RecordReplayRunner

Records real runs to a cassette file (record) and replays them without spawning (replay); shares the Runner run-verb surface.

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

start

start(command: Command) -> RunningProcess

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

astart async

astart(command: Command) -> RunningProcess

record staticmethod

record(path: StrPath) -> RecordReplayRunner

replay staticmethod

replay(path: StrPath) -> RecordReplayRunner

save

save() -> None

__repr__

__repr__() -> str

RecordingRunner

A recording test double: replies to every command with a canned Reply and records each call, so a test can assert on what its code ran. Shares the Runner run-verb surface; inspect calls with calls() / only_call().

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

start

start(command: Command) -> RunningProcess

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

astart async

astart(command: Command) -> RunningProcess

replying staticmethod

replying(reply: Reply) -> RecordingRunner

new staticmethod

new(inner: RunnerLike) -> RecordingRunner

calls

calls() -> list[Invocation]

only_call

only_call() -> Invocation

__repr__

__repr__() -> str

DryRunRunner

A dry-run test double: never spawns a process. Every verb renders the command to its display-quoted line (like Command.command_line()) and returns a synthetic successful result — the seam behind a tool's own --dry-run/--echo mode. Shares the Runner run-verb surface; inspect the rendered lines with commands() / only_command(), or stream them live with on_invocation().

output

output(command: Command) -> ProcessResult

output_bytes

output_bytes(command: Command) -> BytesResult

run

run(command: Command) -> str

exit_code

exit_code(command: Command) -> int

probe

probe(command: Command) -> bool

start

start(command: Command) -> RunningProcess

aoutput async

aoutput(command: Command) -> ProcessResult

aoutput_bytes async

aoutput_bytes(command: Command) -> BytesResult

arun async

arun(command: Command) -> str

aexit_code async

aexit_code(command: Command) -> int

aprobe async

aprobe(command: Command) -> bool

astart async

astart(command: Command) -> RunningProcess

__init__

__init__() -> None

on_invocation

on_invocation(callback: Callable[[str], None]) -> None

commands

commands() -> list[str]

only_command

only_command() -> str

__repr__

__repr__() -> str

Reply

A canned reply for a ScriptedRunner rule.

ok staticmethod

ok(stdout: str) -> Reply

fail staticmethod

fail(code: int, stderr: str) -> Reply

timeout staticmethod

timeout() -> Reply

signalled staticmethod

signalled(signal: int | None = ...) -> Reply

pending staticmethod

pending() -> Reply

lines staticmethod

lines(lines: Sequence[str]) -> Reply

with_stdout

with_stdout(stdout: str) -> Reply

with_stderr

with_stderr(stderr: str) -> Reply

with_line_delay

with_line_delay(seconds: float) -> Reply

__repr__

__repr__() -> str

Invocation

One call captured by a RecordingRunner: the program, args, cwd, env overrides, and whether stdin was supplied. Values are inspectable for assertions; the repr stays redacted (program, arg count, cwd, env names, has_stdin — never argv or env values).

program property

program: str

args property

args: list[str]

cwd property

cwd: str | None

env property

env: dict[str, str | None]

has_stdin property

has_stdin: bool

env_is

env_is(name: str, value: str) -> bool

has_env

has_env(name: str) -> bool

has_flag

has_flag(flag: str) -> bool

__repr__

__repr__() -> str