Uh oh!
There was an error while loading. Please reload this page.
How-to guides a reader can paste, with the checks off the page - #736
Open
tony wants to merge 18 commits into
Open
How-to guides a reader can paste, with the checks off the page#736tony wants to merge 18 commits into
tony wants to merge 18 commits into
Conversation
why: Prose examples under docs/ are doctests: testable, but a reader cannot paste `>>> server.new_session()` into a script. A how-to guide needs the opposite — plain Python a reader copies verbatim — and gp-libs collects a fence only when it contains `>>>`, so a bare ```python block ships untested with no warning and no failure. what: - Add tests/docs/howto_harness.py, a pytest plugin that parses a page with MyST's own parser and runs its backtick-fenced ```python blocks in document order in one shared namespace, so block 2 sees what block 1 bound - Pair each page with a sidecar module holding the hidden checks; a missing sidecar, a missing check_N, or a check_N with no block are all hard failures rather than skips - Point tmux at a private TMUX_TMPDIR and HOME from the runner, not from each page's setup(), so isolation cannot be forgotten - Kill only sockets found under that private directory, and only while the environment still points at it - Reject a colon fence that is not a directive: `:::python` renders a copy-pasteable block the harness would never execute - Replace the aggregated collector list for how-to pages so gp-libs does not also claim them
why: Checking whether a server is alive is the first thing anyone does against a Server handle, and it is the page that proves a visible block can branch without a visible assert: both sides of the branch are exercised, neither is shown. what: - Add docs/howto/index.md with a card grid and a hidden toctree, and register it in the root toctree and the front-page grid - Add docs/howto/check-if-tmux-is-running.md covering is_alive() for the branch and raise_if_dead() for the reason, and noting that an empty sessions list means "no sessions or no server" - Warn that `with Server()` kills the server it was asked about, since Server.__exit__ takes it down with no opt-out - Add tests/docs/howto/check_if_tmux_is_running.py, which runs the visible block against a live private server and then re-runs it in a throwaway namespace with the server killed
why: Fanning a command across a split window is the first task that needs state to survive from one block to the next, so it is the page the shared namespace exists for. It is also where the obvious example is quietly wrong: capture_pane returns the command tmux echoed onto the pane, so a substring test for the answer is true before any shell has run, and a fixed sleep is either dead time or a false negative. what: - Add docs/howto/send-keys-to-every-pane.md: three blocks that build a three-pane window, type into every pane, and wait for each answer - Poll against a deadline in the visible block, and compare whole captured lines, explaining in prose why a substring test lies - Put the window on Server(socket_name="libtmux-howto") with kill_session=True, so the example cannot reach an existing tmux and survives being pasted twice - Have each pane echo $TMUX_PANE, so the output proves the command landed in three panes rather than three times in one - Add tests/docs/howto/send_keys_to_every_pane.py, which verifies the result of the page's own wait rather than repeating it
why: The harness only enforces its contract for pages it is asked to
run, so it cannot see itself being unplugged. Nor can it see a page
that is reachable by nobody: an orphan page and a stale anchor are
Sphinx warnings, and the docs build does not fail on warnings.
what:
- Add tests/docs/test_howto_harness.py asserting the tree is non-empty,
every page's contract resolves, and every page the run was pointed at
produced a harness item
- Require each page to open with the (howto-<stem>)= anchor its
filename implies, so a rename breaks here rather than silently
orphaning inbound {ref} links
- Require each page to appear in the toctree of the index beside itwhy: Three of the promises a how-to page makes are properties of the rendered page, not of running the code: that the block holds nothing but the reader's own script, that the copy button returns it verbatim, and that every block a reader can copy is one something tests. All three held only by inspection. what: - Add tests/docs/test_howto_rendering.py rejecting assertions, pytest imports, harness names and doctest prompts in a visible block, and naming the sidecar the check belongs in instead - Reject any line the sphinx-copybutton prompt pattern matches, since it also copies only prompted lines: a block opening with a column-zero `# comment` copies as its comments alone - Read that pattern from docs/conf.py, so the test tracks the theme's configuration rather than a copy of it - Capture each rendered block with the language Sphinx tagged it with, and allow only ```python, which the harness runs, and ```console, which the reader runs; every other spelling is rejected by name, since ```py renders as highlight-py and a bare ``` as highlight-default, and the harness executes neither - Add an integration-marked build asserting Sphinx renders each page's python blocks exactly as written, scoped to the how-to pages to stay affordable enough for the ordinary test run
why: A check on a page that types into a pane can pass for the wrong reason. capture_pane returns the command tmux echoed onto the pane, so a containment test for the answer is satisfied when the keystrokes land — before any shell runs, and equally if none ever will. The page then reports green while teaching a technique that does not work, and the mistake is invisible in review. what: - Add tests/docs/test_howto_mutations.py, which re-runs each page that types a command and reads the answer back against a libtmux whose send_keys never presses Enter, and requires the page's own checks to fail - Select those pages by inspecting their blocks, so a page added later is covered without anyone remembering this file exists, and assert the selection is non-empty - Extract run_page() from HowtoPageItem.runtest so a test can drive a page without going through collection
why: docs/AGENTS.md tells authors that every code block on a page is an independent doctest with a fresh namespace and a fresh server. That is the opposite of how docs/howto/ works, so an author following it would write a page of self-contained snippets and lose the thing the section exists for — code a reader pastes in sequence. what: - Carve out docs/howto/ in docs/AGENTS.md: plain ```python fences, no scaffolding on the page, one shared namespace in document order, and the sidecar contract with its failure modes - State what visible code owes a reader who runs it verbatim: a named socket where anything is created, kill_session=True, a deadline poll rather than a sleep, whole-line matching, and no column-zero `# ` - Point the root AGENTS.md doctest rules at the exception
why: A broken cross-reference, a page in no toctree, and a directive option Sphinx could not parse are all warnings it builds through, so the docs build reported problems nobody read. The how-to guards catch those cases inside docs/howto/ only; -W covers the rest of the site. what: - Pass -W to sphinx-build from docs/justfile, which every builder there already shares
why: The section is about to hold fifteen recipes. A single flat grid makes a reader scan every card to find the one about servers, and gives no hint where a new page belongs. what: - Split the how-to index grid into headed sections, starting with Servers and Driving panes
why: Starting a server, running several at once, and pointing a handle at one somebody else started are the questions that come before every other recipe, and each has a trap that costs an afternoon. what: - Add start-a-server: new_session() is what boots the daemon, and every server started from Python is already detached because a script has no terminal to attach to. start_server() leaves nothing running -- exit-empty is on by default, so a sessionless daemon exits before the next command reaches its socket - Add run-multiple-servers: a socket, not a session, is tmux's isolation boundary. socket_name never populates socket_path, and Server.__eq__ compares both, so two handles for one daemon built different ways are not equal - Add connect-to-an-existing-server: tmux leaves the socket file behind when a daemon exits, so a directory listing is a list of candidates, not of servers; and because sessions is lenient, a dead socket reads exactly like a healthy server with nothing running on it
why: Finding a session, window or pane that already exists is the other half of connecting to a server, and QueryList's two failure modes are not what a reader expects from a dict-like get(). what: - Add connect-to-an-existing-session: get(default=...) covers absence but not ambiguity -- MultipleObjectsReturned is raised even with a default -- and an empty sessions list means "no sessions or no server", so is_alive() is what separates them - Add connect-to-an-existing-window: a window's index is its position and its name is a label, neither is unique across sessions, and window_index compares as a string, so windows.get(window_index=1) quietly finds nothing - Add connect-to-an-existing-pane: window.panes is ordered by pane index, which is layout position rather than creation order, so the pane you just split is not reliably the last one
why: Code running inside a pane -- a hook, an agent, a script launched in a split -- starts holding no handle at all. tmux already answered the question in the environment, and the four from_env() calls read it back, but the reader has to know they exist before they go searching the server for themselves. what: - Add detect-you-are-inside-tmux: branch on whether your process was spawned by a pane, and note that from_env() reads the environment without touching tmux, so it proves ancestry rather than reachability - Add find-the-session-youre-in: Session.from_env(), and why parsing the session id out of $TMUX yourself is wrong -- tmux writes it once at pane spawn and it goes stale when the window moves - Add find-the-window-youre-in: Window.from_env(), and that a window can belong to more than one session - Add find-the-pane-youre-in: Pane.from_env(), and why it is not active_pane, which means whichever pane has the focus
why: Typing into a pane and splitting one are the two things most libtmux code does, and both have a trap that a working-looking example hides: reading output back can match the shell's echo of the command, and pane order is layout position rather than creation order. what: - Add send-keys: type into one pane, stage a command with enter=False, and read the answer back by polling to a deadline and matching whole lines rather than substrings - Add create-panes: split a window into a layout, keep the panes split returns rather than looking them up again, and note that panes are listed in layout order - Add create-a-floating-pane: new_pane() hovers a pane over the layout on tmux 3.7+, gate on has_gte_version so the version error arrives before the pane is half-built, and drive it like any other pane
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@## master #736 +/- ##
==========================================
+ Coverage 52.37% 57.31% +4.94%
==========================================
Files 26 42 +16 Lines 3729 4203 +474 Branches 747 778 +31 ==========================================
+ Hits 1953 2409 +456 - Misses 1472 1482 +10 - Partials 304 312 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: The how-to section is a new documentation surface a reader chooses to visit, so it needs an entry that says what is there and why the examples can be trusted. what: - Add a Documentation deliverable naming the section, the ground it covers, and that every page runs as part of the suite
why: tmux 3.4 answers `split-window -p40` with `size missing`, so
passing percentage= to Pane.split or Window.split raised
LibTmuxException there. tmux taught -l to take a percentage in 3.1 and
deprecated -p in the same release, so -l is the one spelling that
works across every version libtmux supports.
what:
- Emit `-l{percentage}%` instead of `-p{percentage}`
- Drop test_split_percentage's 3.5 skip, which existed to route around
the tmux 3.4 regression and would otherwise hide this fix on the
versions it repairs
- Stop naming the tmux flag in the parameter docs, since the mapping is
no longer one-to-onewhy: A guide to a version-specific feature cannot run everywhere. The floating-pane page opens by refusing to go on without tmux 3.7, which is the behaviour it teaches -- and on an older tmux that refusal came back as a suite failure rather than as the page being inapplicable. what: - Read an optional MIN_TMUX_VERSION from a page's sidecar and skip the page below it, naming the page, the floor and the tmux in use - Check it after the page/sidecar contract resolves, so a missing check or a stray one still fails at every tmux version; only running the examples is skipped - Declare 3.7 on the floating-pane sidecar The declaration sits on the sidecar rather than the page because the page carries no scaffolding: a reader copying the block should meet the feature, not a marker addressed at the test suite.
why: tmux says `no space for new pane` through 3.6 and `size or position no space for a new pane` from 3.7. The page quoted the newer form as if it were the wording, and its check asserted on it, so both were wrong for a reader on anything older. what: - Assert on the fragment every supported version shares - Describe the failure in prose rather than quoting one release's text, and tell a reader who branches on the message to match loosely
why: The percentage argument shipped in 0.56 and has never worked on tmux 3.4, so anyone on that version hit it. what: - Add a Fixes entry for percentage sizing on Pane.split and Window.split
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 freeto 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
docs/howto/section: fifteen task-shaped guides whose visible code is plain Python a reader copies into a file and runs — no>>>prompts, no assertions, no test markers.TMUX_TMPDIRandHOMEfrom the runner rather than from page setup, so an example cannot reach the tmux of whoever runs the suite.Prose pages under
docs/are doctests, which are testable but not copyable — a reader cannot paste>>> server.new_session()into a script. A how-to guide needs the opposite, and the doctest collector only claims a fence containing>>>, so a bare```pythonblock would ship untested with no warning and no failure.Changes by area
The harness
tests/docs/howto_harness.py: parses a page with MyST's own parser, collects its```pythonfences in document order, runs them in one namespace, and calls the sidecar'scheck_N(ctx)after block N. Itspytest_collect_filewrapper replaces the aggregated collector list for how-to pages so the doctest plugin does not also claim them.myst_enable_extensionsfromdocs/conf.pyrather than restating it. Whether a run of backticks becomes a fence depends on the enabled extension set, so a copy would drift the first time the site's config changed — and drift means a block that renders on the page but never runs.ctx.run_block(n, namespace={})re-runs a block in a fresh world, which is how a page showingif server.is_alive():gets both branches checked from one visible block.The guides
Fifteen pages, grouped in the index by what you are driving:
The guards
tests/docs/test_howto_harness.py: the tree is non-empty, every page's contract resolves, every page the run was pointed at produced an item, each page opens with the anchor its filename implies, and each appears in the toctree beside it.tests/docs/test_howto_rendering.py: rejects assertions, pytest imports, harness names and prompts in a visible block; rejects any line the sphinx-copybutton prompt pattern matches, read fromdocs/conf.py; and allows onlypythonandconsoleas rendered languages.tests/docs/test_howto_mutations.py: re-runs each page that types a command and reads the answer back against a libtmux whosesend_keysnever presses Enter, and requires the page's own checks to fail.Conventions and build
docs/AGENTS.md: carves outdocs/howto/from the doctest rules that govern the rest ofdocs/, and states what visible code owes a reader who runs it verbatim.docs/justfile: passes-Wtosphinx-build.Design decisions
The page carries no test scaffolding at all. The pages are the product; an assertion in the block is an assertion in the reader's paste. Everything that makes a page testable lives in
tests/docs/howto/<slug>.py, named after the page.Blocks share one namespace, which inverts the rule for the rest of
docs/. Topic pages give every block a fresh namespace and a fresh server, because each is an independent illustration. A how-to page is one script broken into steps, so its second block must see the first block's names.Isolation belongs to the runner, not the page. A private
TMUX_TMPDIRandHOMEare set before the sidecar is called, and only sockets under that directory are killed, only while the environment still points at it. A sidecar that had to ask for isolation is a sidecar that can forget.A mutation probe, because a passing check is not evidence.
capture_panereturns the command tmux echoed onto the pane, so a containment test for the answer is satisfied when the keystrokes land — before any shell runs, and equally if none ever will. Without a probe the page reports green while teaching a technique that does not work.Visible code polls to a deadline and compares whole lines. A fixed sleep is either dead time or a false negative, and a substring test is true before the shell has run. The pages teach the correct shape because a reader runs them verbatim.
Verification
No page carries a doctest prompt — expect no matches:
$ rg '^>>> ' docs/howto/No page carries an assertion — expect no matches:
$ rg '^\s*assert ' docs/howto/No colon fence, which renders copy-pasteable but executes nothing — expect no matches:
$ rg '^:::python' docs/howto/Every guide has a sidecar — both counts are fifteen:
$ fd -e md . docs/howto | rg -v 'index.md'| wc -l$ fd -e py . tests/docs/howto | rg -v '__init__'| wc -lTest plan
uv run pytest --reruns 0— full suite greenuv run ruff check .anduv run ruff format --check .— cleanuv run mypy src tests— cleanjust build-docs— builds under-Wwith no warning and no broken cross-referencetest_howto_mutations— every page that reads output back fails whensend_keysstops pressing Enter, proving the hidden checks are load-bearingtest_howto_harness— a page with no sidecar, a missingcheck_N, and an orphancheck_Neach fail rather than skiptest_rendered_blocks_match_the_page_source— Sphinx renders each page's python blocks exactly as writtenpytest -n auto, matching CI's distribution