Add brush-py: PyO3 Python bindings for brush (fork prototype)#2
Open
hartsock wants to merge 2 commits into
Open
Add brush-py: PyO3 Python bindings for brush (fork prototype)#2hartsock wants to merge 2 commits into
hartsock wants to merge 2 commits into
Conversation
Introduce brush-py, a Python extension module that embeds the brush shell via PyO3 over the published brush-core 0.5 and brush-builtins 0.2 crates. Zero-core-change design: brush-py makes NO modifications to any existing brush crate or the root Cargo.toml. It is its own independent cargo workspace (empty [workspace] table) so it is not pulled into the parent brush workspace, and it depends on the crates.io releases rather than path deps, insulating it from in-tree API churn. Upstreaming later only requires switching the two deps to path deps and adding it to the root members list. The binding exposes a stateful Shell (variables, exported env, cwd, and functions persist across calls) and a CompletedCommand result carrying stdout/stderr/exit_code/success. Output is captured through tempfiles (std::fs::File -> OpenFile::File) to avoid the ~64KB OS-pipe-buffer deadlock a naive pipe capture would hit, and external commands are captured too. Each async brush-core call is driven on an owned multi-thread Tokio runtime via block_on, wrapped in Python::allow_threads to release the GIL. The pyclass is #[pyclass(unsendable)] since one Shell is driven from one thread. Builds abi3-py39 wheels (one per platform, CPython 3.9+) via maturin; CI workflow publishes to PyPI on brush-py-v* tags using trusted publishing. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the initial brush-py crate with the API surface and packaging polish needed for real use. Still ZERO changes to any existing brush crate. - New Shell methods: run_c() (bash -c semantics; process-safe -- returns a result rather than terminating) and call_function() (invoke a defined shell function by name, with output capture). Verified against the PUBLISHED brush-core 0.5.0 API (invoke_function takes &ExecutionParameters, which differs from the local cap-hook checkout). - Mixed Rust/Python layout for type stubs: the compiled extension is now `brush._brush` and a pure-Python `brush` package re-exports it, carrying PEP 561 stubs (python/brush/__init__.pyi + py.typed). `import brush` is unchanged; mypy/pyright now type-check the API (verified with mypy). - pytest regression suite (tests/) covering exit codes, stdout/stderr capture, combine_stderr (2>&1), state persistence, env export to children, non-exported vars, cd/cwd, >50k-line output (no pipe deadlock), run_c, call_function, run_script, and the syntax-error contract. 18/18 pass. - Corrected the syntax-error contract in docs/stubs/comments: brush reports parse errors bash-style (exit_code 2 + message on stderr), it does NOT raise. Verified empirically against brush-core 0.5.0. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
brush-py— PyO3 Python bindings that let you drive the brush shell from Python (pip install→import brush→ run commands). Fork-first prototype (Phase 1); zero changes to any existing brush crate.[workspace]; depends on the publishedbrush-core = "0.5"+brush-builtins = "0.2"(immune to in-tree API churn).Shell(vars, exported env, cwd, functions persist across calls).CompletedCommandcarriesstdout/stderr/exit_code/success.run,run_c,run_script,call_function,setenv,getenv,cd,cwd,last_exit_status.__init__.pyi+py.typed) via a mixed Rust/Python layout (compiledbrush._brush, re-exported by abrushpackage).Key design decisions (verified against brush-core 0.5.0 source + at runtime)
block_ons each call insidePython::allow_threads(GIL released while the shell blocks on child processes/IO).AsyncPipeReader) ispub(crate)/unreachable, and naive pipe capture deadlocks past the 64 KB OS buffer. Tempfiles also capture external commands. Verified with 50 000 lines, no deadlock.echo/cd/export; the binding callsdefault_builtins(BuiltinSet::BashMode).exit_code == 2, message on stderr), not as exceptions (verified empirically; the design's initial assumption was corrected).Test plan
cargo check/cargo build— green against published crates (pyo3 0.24.2).maturin build→ abi3 manylinux wheel →pip install→python example.py— full end-to-end (capture, exit codes, env export to children,2>&1,cd/cwd,run_c,call_function).pytest tests/— 18/18 pass (exit codes, capture, combine_stderr, state persistence, exported vs non-exported env, large output, run_c, call_function, run_script, syntax-error contract).mypyagainst the stubs — accepts valid usage, flags type errors (stubs work).Notes / follow-ups
brush-shell(the namebrushis taken; import name staysbrush).brush_corepublic structs? reubeno/brush#454) settles; the maintainer has endorsed the embedding direction on Add an exec/open interception hook toShellExtensionsso embedders can confine commands in-process reubeno/brush#1183/feat(core): add CommandInterceptor exec/open hook to ShellExtensions reubeno/brush#1184. The one tiny core helper worth proposing then isrun_string_and_capture(would retire the tempfile workaround).🤖 Generated with Claude Code