feat(python): expose Bash class with Monty Python execution and external function handler#760
Conversation
|
Hey @chaliy , I've been using bashkit in a Python agent project and needed to expose the Monty Python execution and external function handler from the Rust side to Python. I made the changes on my fork and they're working well for my use case. I wanted to check if you'd be open to merging this back upstream? Happy to iterate on the implementation if anything needs adjusting. |
f65ff8b to
82f10ce
Compare
…nal function handler Extends the PyO3 bindings to support Monty Python execution and programmatic tool calling from within Bash scripts: - Add `python=True` param to `Bash.__init__` — enables `python`/`python3` builtins (Monty embedded interpreter) - Add `external_functions` + `external_handler` params — wires `python_with_external_handler` so async Python callbacks are invoked when Monty code calls registered external functions - Validate: `external_functions` requires handler; handler requires `python=True`; handler must be callable; handler must be async (checked via `inspect.iscoroutinefunction`) - Raise `RuntimeError` in `execute_sync` when `external_handler` is set — async handlers require a running event loop; use `execute()` instead - Fix `reset()` to preserve python mode and external handler config - Extract `make_external_handler` + `apply_python_config` helpers shared between `new()` and `reset()` - Fix `py_to_monty`: isinstance guards for float/bytes; PyTuple, PyInt+BigInt, PySet, PyFrozenSet branches; BigInt parse errors propagated - Fix `monty_to_py`: BigInt → Python int; Tuple → PyTuple; NamedTuple → dict with field names; document Set/FrozenSet → list as known limitation; Path documented as known limitation (becomes str) - Pin `num-bigint = "=0.4.6"` to match bashkit core (verified in Cargo.lock) - Add `ExternalHandler` Protocol with `async def __call__` to `_bashkit.pyi` - 19 tests added; all 336 tests pass Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
82f10ce to
50f2e68
Compare
|
@shubhlohiya yep, sounds totally missed feature. I will look at it in next couple of hous and if all good, will merge it. Thank you! |
There was a problem hiding this comment.
Pull request overview
This PR extends the crates/bashkit-python PyO3 bindings so Python users can configure the core Bash interpreter to enable Monty embedded Python execution (python=True) and optionally register external function names that dispatch to an async Python handler.
Changes:
- Extend
Bash(PyBash) constructor/reset to supportpython,external_functions, andexternal_handler. - Add MontyObject ⇄ Python conversion helpers, including BigInt round-tripping.
- Update Python stubs and add tests covering Python mode + external handler behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| crates/bashkit-python/src/lib.rs | Adds Python/external-handler configuration and conversion glue for Monty interop. |
| crates/bashkit-python/tests/test_bashkit.py | Adds tests for python execution, external handler dispatch, reset behavior, and BigInt conversion. |
| crates/bashkit-python/bashkit/_bashkit.pyi | Updates type stubs for new Bash constructor options, ExternalHandler Protocol, and cancel(). |
| crates/bashkit-python/Cargo.toml | Enables bashkit’s python feature and adds num-bigint dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| builder = builder.limits(limits); | ||
| builder = apply_python_config(builder, python, external_functions, handler_clone); | ||
| *bash = builder.build(); | ||
| Ok(()) |
There was a problem hiding this comment.
Fixed — reset() now clears the shared cancellation flag after rebuilding the interpreter so cancel() does not erroneously abort post-reset executions.
There was a problem hiding this comment.
Rechecked on the current head (36dd822) and this is still reproducible. Clearing the old flag avoids an immediate abort, but cancel() is still wired to the pre-reset token rather than the rebuilt Bash instance. Minimal repro on the updated extension:
bash = Bash(python=True)
bash.reset()
bash.cancel()
result = await bash.execute("echo hi")After reset(), that returns exit_code == 0 with stdout == "hi\n"; without reset(), the same pre-cancelled execute returns execution cancelled. So the binding still needs to refresh self.cancelled (or otherwise route cancel() to the current inner interpreter token), and the new test only checks that cancel() does not raise.
There was a problem hiding this comment.
Fixed properly in 9617a09 — the cancellation token is now wrapped in Arc<RwLock<Arc<AtomicBool>>> so reset() swaps it to the new interpreter's token and cancel() always reads the current one. Applies to both Bash and BashTool.
Verified with your repro:
bash = Bash(python=True)
bash.reset()
bash.cancel()
result = await bash.execute("echo hi")
# exit_code=1, stderr="execution cancelled" ✓Also added tests for both directions (cancel-then-reset clears, reset-then-cancel targets new interpreter). 342 tests pass.
crates/bashkit-python/Cargo.toml
Outdated
| serde_json = { workspace = true } | ||
|
|
||
| # Big-integer support for py_to_monty BigInt extraction | ||
| num-bigint = "=0.4.6" # must match the version pulled in by bashkit core (see Cargo.lock) |
There was a problem hiding this comment.
Fair point — relaxed to ^0.4.6 to allow compatible patch bumps.
| async def execute(self, commands: str) -> ExecResult: ... | ||
| def execute_sync(self, commands: str) -> ExecResult: ... | ||
| def cancel(self) -> None: ... | ||
| def reset(self) -> None: ... |
There was a problem hiding this comment.
Added — cancel() was already implemented on BashTool in Rust, just missing from the stub. Drive-by fix.
| [dependencies] | ||
| # Bashkit core | ||
| bashkit = { path = "../bashkit", features = ["scripted_tool"] } | ||
| bashkit = { path = "../bashkit", features = ["scripted_tool", "python"] } |
There was a problem hiding this comment.
The built wheel is ~3.5 MB with the python feature enabled. I think the size/build-time impact is small enough that it is not worth the added complexity of a conditional Cargo feature, but happy to revisit if the maintainer feels differently.
| .iter() | ||
| .map(|v| monty_to_py(py, v)) | ||
| .collect::<PyResult<Vec<_>>>()?; | ||
| Ok(PyList::new(py, &py_items)?.into_any().unbind()) |
There was a problem hiding this comment.
This bridge rewrites both MontyObject::Set and MontyObject::FrozenSet to PyList, so a Python external_handler sees a different type than the Monty caller passed. I reproduced this on the built extension with echo_type({1, 2}): the handler received list, not set. That silently changes semantics for supported Monty container types instead of preserving them or failing explicitly when an element is unhashable.
There was a problem hiding this comment.
Fixed — monty_to_py now produces PySet/PyFrozenSet directly and propagates the error if items are unhashable, rather than silently falling back to a list.
- Broaden iscoroutinefunction check to also inspect __call__ so objects with async def __call__ (matching ExternalHandler Protocol) are accepted - Reset cancelled flag after reset() so cancel() doesn't erroneously abort post-reset executions - Relax num-bigint pin from =0.4.6 to ^0.4.6 to avoid resolution failures - Add cancel() stub to BashTool in _bashkit.pyi (drive-by fix) - Convert Set/FrozenSet to PySet/PyFrozenSet in monty_to_py with fallback to PyList when items are unhashable - Convert NamedTuple to dict (mapping field_names to values) instead of list - Document Path -> str as known limitation in monty_to_py - Add tests: async callable object validation, set type preservation, cancel after reset, sync handler rejection, BigInt value equality - All 339 tests pass Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
9cd6538 to
7ee7412
Compare
The set test was incorrectly accepting list as a valid fallback. With the Set/FrozenSet -> PySet/PyFrozenSet fix in monty_to_py, Monty set literals now correctly arrive as Python set in the handler. Updated the test to assert isinstance(set) strictly and verify exact value. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
One more issue on the current head ( Repro against the updated extension: tool = BashTool()
tool.reset()
tool.cancel()
result = await tool.execute("echo hi")That still succeeds with |
…rpreter Both Bash and BashTool stored the cancellation token as a plain Arc<AtomicBool> obtained at construction time. After reset() rebuilt the inner interpreter, cancel() still wrote to the stale token — the new interpreter never saw it. Fix: wrap the token in Arc<RwLock<Arc<AtomicBool>>> so reset() can swap it to the new interpreter's token and cancel() always reads the current one. Applies to both Bash and BashTool. Tests cover both directions: - cancel() then reset() clears cancellation (execution succeeds) - reset() then cancel() targets the new interpreter (execution cancelled) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
a7a55a8 to
9617a09
Compare
Summary
The
crates/bashkit-pythonPyO3 bindings exposeBashToolandScriptedToolbut not theBashbuilder API. The Rustpythonfeature (Monty embedded interpreter) andpython_with_external_handlerare available at the Rust level but unreachable from Python.This PR adds a
BashPython class that exposes both, making it possible to:python3/pythonbuiltins (Monty embedded interpreter)to an async Python handler
Usage
Changes
Cargo.toml— enablepythonfeature; addnum-bigintdep forBigIntround-tripsrc/lib.rs:PyBashconstructor with three new optional params:python: bool,external_functions: list[str] | None,external_handler: AsyncCallable | Nonemake_external_handlerandapply_python_confighelpers shared betweennew()andreset()reset()to preserve python mode and external handler config (previously dropped)ValueErrorwhenexternal_functionsis supplied withoutexternal_handlermonty_to_py/py_to_montyconversion helpers covering allMontyObjectvariants,including correct
BigInt↔ Pythonintround-trip andPyTuplesupport_bashkit.pyi— update stub with new constructor params,ExternalHandlerProtocolfor precise type checking, and
cancel()methodtests/test_bashkit.py— 19 new tests; all 336 tests passAll existing
BashToolandScriptedToolbehaviour is unchanged.🤖 Generated with Claude Code