tmux-python / tmux-python/libtmux

`retry_until`'s zero-arg predicate forces a loop-variable closure that ruff `B023` and mypy disagree about

Open
#726 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.2k
Forks
127
Avg merge
2h 13m
Merged PRs (30d)
1

Description

Filed against tmux-python/libtmux v0.62.0. Found while writing tested documentation examples that wait on several panes. Related to but distinct from #376, which is about retry_until's failure message rather than its signature.

What happens

retry_until takes a zero-argument predicate, so waiting on anything from a loop forces a closure over the loop variable. That closure is then rejected by this repo's own lint configuration, and the standard fix for it is rejected by this repo's own type checker. Every such call site ends up carrying a # type: ignore[misc].

def retry_until(
    fun: Callable[[], bool],
    seconds: float = RETRY_TIMEOUT_SECONDS,
    *,
    interval: float = RETRY_INTERVAL_SECONDS,
    raises: bool | None = True,
) -> bool:

Recreation

Write the natural thing:

from __future__ import annotations

from libtmux.pane import Pane
from libtmux.test.retry import retry_until


def wait_for_each(panes: list[Pane]) -> None:
    for pane in panes:
        retry_until(lambda: any(pane.capture_pane()))

uv run ruff checkB023, which is enabled repo-wide via extend-select = ["B", ...] in pyproject.toml:

B023 Function definition does not bind loop variable `pane`
    retry_until(lambda: any(pane.capture_pane()))
                             ^^^^

Apply the documented fix for B023, a default-argument binding:

        retry_until(lambda pane=pane: any(pane.capture_pane()))

uv run mypy --strict:

error: Cannot infer type of lambda  [misc]

So the lint-clean form does not type-check and the type-clean form does not lint. The only way out at the call site is # type: ignore[misc], which is what the two call sites in tests/docs/howto/send_keys_to_every_pane.py on master currently carry.

Note that in this particular shape B023 is a false positive — retry_until calls the predicate synchronously before the next iteration, so late binding is harmless. But B023 cannot know that from the signature, and neither can a reader.

What a fix needs

The cause is that a zero-argument predicate has nowhere to put the thing being waited on. Options, in descending order of preference:

  1. Accept the subject as an argument. Add an optional args/*args forwarded to fun, so retry_until(lambda p: any(p.capture_pane()), args=(pane,)) needs no closure. B023 does not fire, mypy infers the lambda from the declared parameter type, and no call site needs a suppression.
  2. Ship the common waits as functions, so most callers never write a predicate: something like pane.wait_until_output() / pane.wait_for_line(...) covering "this pane has produced anything" and "this pane printed this whole line". Most in-repo uses of retry_until are one of those two. This is the larger change but removes the sharpest edge for users, and would let the how-to guides stop teaching a hand-rolled poll loop.
  3. At minimum, document the interaction — the B023-versus-mypy conflict and the # type: ignore[misc] it forces — in the retry_until docstring, so the next person does not rediscover it.

Whatever is chosen, the two # type: ignore[misc] suppressions in tests/docs/howto/send_keys_to_every_pane.py should come out as part of it; they are markers for this issue.

Related: #376 (retry_until(): improve pytest error message via rewriting) — the same function, a different complaint. Fixing signature and message together would be reasonable.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/libtmux/test/retry.py and its retry_until signature, then inspect the B023 configuration in pyproject.toml and the two suppressions in tests/docs/howto/send_keys_to_every_pane.py. Compare the proposed API and convenience-method options with existing call sites, and run uv run ruff check, uv run mypy --strict, and the relevant tests. Done means the chosen approach removes those suppressions without introducing lint or type-checking errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, tooling
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.