Add experimental typed chain API for tmux command sequences#685
Open
tony wants to merge 7 commits into
Open
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #685 +/- ##
==========================================
+ Coverage 51.29% 56.83% +5.54%
==========================================
Files 25 36 +11
Lines 3488 4008 +520
Branches 686 716 +30
==========================================
+ Hits 1789 2278 +489
- Misses 1404 1425 +21
- Partials 295 305 +10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ca3eded to
b782ba7
Compare
chain API for tmux command sequences
Member
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code |
d188c73 to
f12f139
Compare
why: Promote the converged chainable-commands design from the PR #684 research into a documented, typed experimental API, beginning with the argv IR substrate. Establishes the _experimental package as a home for in-progress designs (mirroring _internal/docs/internals), explicitly outside the versioning policy. what: - Add src/libtmux/_experimental/ + chainable_commands subpackage - Add ir.py: CommandCall, CommandChain (argv/argvs/then/>>/run), CommandSpec, CommandRunner/CommandResultLike protocols, and ;-escaping, all with doctests - Add tests/_experimental/chainable_commands/test_ir.py (pure argv + live tmux one-dispatch and stop-on-error) - Add docs/experiment/ landing + IR autodoc page; wire into docs/index.md toctree; mark _experimental not-public in public-api.md
…ive adapters why: Add the headline layer of the chainable-commands design -- a typed, target-safe deferred query that compiles to one native tmux command sequence -- plus the live-tmux bridge so plans resolve and dispatch against a real server in a single invocation. what: - Add plan.py: typed PaneTarget/WindowTarget/SessionTarget, command values (SendKeys/ResizePane/SelectLayout), PaneRef rows with bound .cmd/.window namespaces, lazy PaneQuery (filter/order_by/limit/all/first/map/commands), CommandPlan with pure to_chain(snapshot) and one-dispatch run() - Reuse ir.CommandChain (resolve the lab's duplicate-CommandChain collision) - Add adapters.py: snapshot_from_session() and SessionPlanRunner (PlanRunner over a live Session); cast Server to CommandRunner for clean mypy + ty - Add tests/_experimental tests: pure plan semantics + live snapshot/dispatch - Add plan + adapters autodoc pages; grow docs/experiment/index with deferred-plan examples and toctree entries
…nc adapter why: An async host (e.g. an MCP server) is the real awaitable boundary for this design. Add an async facade so snapshot resolution and dispatch are awaitable while command construction stays synchronous, preserving the one-plan = one-native-dispatch guarantee, plus a live async adapter over the sync core. what: - Add aio.py: async PaneQuery/MappedPaneQuery/CommandPlan wrapping the sync engine; to_chain reuses the sync compile path so one plan still yields one ir.CommandChain; run() dispatches via an async runner - Add adapters.AsyncSessionPlanRunner (AsyncPlanRunner over a live Session via asyncio.to_thread) - pyproject: add pytest-asyncio (dev + testing), asyncio_mode="auto", and asyncio_default_fixture_loop_scope="function" (matching pytest-asyncio's own config and sibling projects) - pytest_plugin/test_pytest_plugin: the plugin's pytester-based doctests/tests spawn sync inner pytest sessions; pass `-p no:asyncio` so the now-installed pytest-asyncio plugin does not load there and emit its loop-scope deprecation - Add tests/_experimental/test_aio.py: async plan semantics (pytest-asyncio auto) plus live async snapshot/dispatch integration - Add aio autodoc page; grow docs/experiment/index with an async example, card, and toctree entry
why: A tmux command sequence is dispatched once, so a command may only fold into a chain when its output is not consumed mid-chain. Wire the static and dynamic halves of that rule together so a chain compiler has one place to decide what may merge. what: - Add chainability.py: COMMAND_SPECS registry + is_chainable() (static half, via CommandSpec.chainable); DeferredCommandResult raising DeferredOutputUnavailable on output access (dynamic half); ChainabilityError for non-chainable commands - Add tests/_experimental/test_chainability.py covering the static flags and deferred output rejection - Export the chainability surface from the package; add the chainability autodoc page, grid card, and toctree entry
…on names why: Use plain libtmux/Python vernacular for the experimental docs so the layers read clearly to newcomers, while keeping every link, example, and toctree entry. what: - Rename the five sections: Command IR -> Intermediate representation, Deferred plan -> Expressions, Async facade -> Async, Live-tmux adapters -> Connecting to live tmux sessions, Chainability contract -> Chainability - Reorder the layer bullets and grid cards to match the toctree; tighten the landing prose and adopt "expression" vocabulary in the worked examples - Update each api page heading to the new section name
why: Minimal installs should import the experimental chain package without the dev or testing dependency groups present. what: - Add a python -S subprocess import of libtmux._experimental.chain to test_chainability.py, asserting the package loads with only stdlib
why: Object-level cmd wrappers add target context, so only Server should be documented as directly safe for raw command sequences. what: - Limit CommandRunner direct-dispatch guidance to Server - Point object-level usage to session.server or SessionPlanExecutor
3914df4 to
6021f9c
Compare
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
libtmux._experimental.chain, an experimental typed API for composing an ordered set of tmux commands that runs as one nativetmux ... \; ...invocation instead of one subprocess per command. Promotes the converged design from the Explore typed chainable tmux command APIs #684 research survey into real, documented, typed modules. Refs Typed command descriptors for native tmux command chains #683.Sessionin one invocation — sync (SessionPlanExecutor) and async (AsyncSessionPlanExecutor, viaasyncio.to_thread).pytest-asyncio, configured the way pytest-asyncio itself and sibling projects do (asyncio_mode = "auto",asyncio_default_fixture_loop_scope = "function").docs/experiment/tree (mirroringdocs/internals/) with runnable doctest examples; the package is explicitly outside the versioning policy and not re-exported from top-levellibtmux.Changes by area
Experimental package —
src/libtmux/_experimental/chain/ir.pyCommandCall,CommandSequence(argv/argvs/>>/run),CommandSpec, runner protocols,;-escapingplan.pyPaneRefrows with bound.cmd/.window, lazyPaneQuerywith.map(data-only rows) and.commands(one-or-more commands per row),CommandPlan(pureto_sequence(snapshot)+ one-dispatchrun)_async.pyCommandSequence_connection.pysnapshot_from_session,SessionPlanExecutor,AsyncSessionPlanExecutorbatch.pyCOMMAND_SPECS/is_chainable+DeferredCommandResult/ChainabilityErrorTooling —
pyproject.tomlpytest-asyncioto thedevandtestinggroups; setasyncio_mode = "auto"andasyncio_default_fixture_loop_scope = "function".Plugin doctests —
src/libtmux/pytest_plugin.py,tests/test_pytest_plugin.py-p no:asyncioto the plugin'spytester-based inner pytest runs. Those inner sessions are synchronous; disabling the now-installed async plugin there keeps them from emitting pytest-asyncio's loop-scope deprecation. Edits live in hidden doctest blocks, so the rendered plugin docs are unchanged.Docs —
docs/experiment/,docs/index.md,docs/project/public-api.mdexperiment/indexinto the toctree; marklibtmux._experimental.*not-public.Design decisions
ir.CommandSequence, so the IR, expressions, async, and connection layers all share one dispatch path and one set of guarantees.capture-pane,show-option) stay individual typed calls rather than being forced into a chain._asyncreuses the syncto_sequence, so construction stays synchronous and one expression still compiles to exactly one invocation;AsyncSessionPlanExecutoroffloads the sync core viaasyncio.to_thread.PaneRefrows carryPaneTarget/WindowTarget/SessionTarget, so a row-bound command cannot mis-target.PaneQuerykeeps.mapfor data-only row transforms and.commands(mapper)for the deferred, one-or-more-commands-per-row plan. The verb names what the call yields — aCommandPlan— and pairs with.mapso the data path and the command path read distinctly at the call site (panes().filter(active=True).commands(lambda p: p.cmd.send_keys(...))).irfor the intermediate representation (mypymypyc/ir, polarsplans/ir),planfor the deferred form (datafusionLogicalPlan),batchfor fold-into-one-dispatch (dask), and_connection+*Executorfor the live bridge (djangodb/backends, dagsterExecutor.execute(plan), stdlibconcurrent.futures.Executor) rather thanadapter— which conventionally means interface/value conversion.-p no:asyncioover a warnings filter: inner pytester sessions are configured to not load the async plugin (matching how the ecosystem scopes inner runs) instead of muting a deprecation globally.Test plan
uv run ruff check . --fix --show-fixes— cleanuv run ruff format .— cleanuv run mypy— clean (strict, oversrc+tests)uvx ty check src/libtmux/_experimental tests/_experimental— cleanuv run pytest --reruns 0— passes (incl. new doctests, async tests, and live-tmux integration)just build-docs— builds (experiment toctree + autodoc resolve)Refs #683. Builds on the API survey in #684.