Skip to content

Commit 06ec0c1

Browse files
committed
!squash docs and doctest
1 parent a0f1e66 commit 06ec0c1

File tree

1 file changed

+287
-8
lines changed

1 file changed

+287
-8
lines changed

src/libtmux/test/waiter.py

+287-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22
33
This module provides utilities for waiting on tmux pane content in tests.
44
Inspired by Playwright's sync API for waiting on page content.
5+
6+
The main class is :class:`PaneWaiter` which provides methods to wait for specific
7+
content to appear in a tmux pane. This is particularly useful for testing shell
8+
commands and their output.
9+
10+
Examples
11+
--------
12+
>>> from libtmux.test.waiter import PaneWaiter
13+
>>> # Create a new window and get its pane
14+
>>> window = session.new_window(window_name="test_waiter")
15+
>>> pane = window.active_pane
16+
>>> # Create a waiter for the pane
17+
>>> waiter = PaneWaiter(pane)
18+
>>> # Wait for a specific prompt
19+
>>> result = waiter.wait_for_prompt("$ ")
20+
>>> result.success
21+
True
22+
>>> # Send a command and wait for its output
23+
>>> pane.send_keys("echo 'Hello World'")
24+
>>> result = waiter.wait_for_text("Hello World")
25+
>>> result.success
26+
True
27+
>>> "Hello World" in result.value
28+
True
29+
30+
The waiter also handles timeouts and errors gracefully:
31+
32+
>>> # Wait for text that won't appear (times out)
33+
>>> result = waiter.wait_for_text("this won't appear", timeout_seconds=0.1)
34+
>>> result.success
35+
False
36+
>>> isinstance(result.error, WaiterTimeoutError)
37+
True
538
"""
639

740
from __future__ import annotations
@@ -23,28 +56,140 @@
2356

2457

2558
class WaiterError(LibTmuxException):
26-
"""Base exception for waiter errors."""
59+
"""Base exception for waiter errors.
60+
61+
This is the parent class for all waiter-specific exceptions.
62+
"""
2763

2864

2965
class WaiterTimeoutError(WaiterError):
30-
"""Exception raised when waiting for content times out."""
66+
"""Exception raised when waiting for content times out.
67+
68+
This exception is raised when the content being waited for does not appear
69+
within the specified timeout period.
70+
71+
Examples
72+
--------
73+
>>> waiter = PaneWaiter(pane, timeout=0.1) # Short timeout
74+
>>> result = waiter.wait_for_text("won't appear")
75+
>>> isinstance(result.error, WaiterTimeoutError)
76+
True
77+
>>> str(result.error)
78+
"Text 'won't appear' not found in pane"
79+
"""
3180

3281

3382
class WaiterContentError(WaiterError):
34-
"""Exception raised when there's an error getting or checking content."""
83+
r"""Exception raised when there's an error getting or checking content.
84+
85+
This exception is raised when there's an error accessing or reading the
86+
pane content, for example if the pane is no longer available.
87+
88+
Examples
89+
--------
90+
>>> # Example of handling content errors
91+
>>> try:
92+
... content = "\\n".join(pane.capture_pane())
93+
... except Exception as e:
94+
... error = WaiterContentError("Error capturing pane content")
95+
... error.__cause__ = e
96+
... raise error from e
97+
"""
3598

3699

37100
@dataclass
38101
class WaitResult(t.Generic[T]):
39-
"""Result of a wait operation."""
102+
"""Result of a wait operation.
103+
104+
This class encapsulates the result of a wait operation, including whether it
105+
succeeded, the value found (if any), and any error that occurred.
106+
107+
Parameters
108+
----------
109+
success : bool
110+
Whether the wait operation succeeded
111+
value : T | None
112+
The value found, if any
113+
error : Exception | None
114+
The error that occurred, if any
115+
116+
Examples
117+
--------
118+
>>> # Successful wait result
119+
>>> result = WaitResult[str](success=True, value="found content")
120+
>>> result.success
121+
True
122+
>>> result.value
123+
'found content'
124+
>>> result.error is None
125+
True
126+
127+
>>> # Failed wait result with error
128+
>>> error = WaiterTimeoutError("Timed out")
129+
>>> result = WaitResult[str](success=False, error=error)
130+
>>> result.success
131+
False
132+
>>> result.value is None
133+
True
134+
>>> isinstance(result.error, WaiterTimeoutError)
135+
True
136+
"""
40137

41138
success: bool
42139
value: T | None = None
43140
error: Exception | None = None
44141

45142

46143
class PaneWaiter:
47-
"""Utility class for waiting on tmux pane content."""
144+
"""Utility class for waiting on tmux pane content.
145+
146+
This class provides methods to wait for specific content to appear in a tmux pane.
147+
It supports waiting for exact text matches, prompts, and custom predicates.
148+
149+
Parameters
150+
----------
151+
pane : Pane
152+
The tmux pane to wait on
153+
timeout : float, optional
154+
Default timeout in seconds, by default 2.0
155+
156+
Examples
157+
--------
158+
Basic usage with text:
159+
160+
>>> waiter = PaneWaiter(pane)
161+
>>> pane.send_keys("echo 'test'")
162+
>>> result = waiter.wait_for_text("test")
163+
>>> result.success
164+
True
165+
>>> "test" in result.value
166+
True
167+
168+
Waiting for a prompt:
169+
170+
>>> waiter = PaneWaiter(pane)
171+
>>> result = waiter.wait_for_prompt("$ ")
172+
>>> result.success
173+
True
174+
>>> "$ " in result.value
175+
True
176+
177+
Custom predicate:
178+
179+
>>> waiter = PaneWaiter(pane)
180+
>>> result = waiter.wait_for_content(lambda content: "error" not in content.lower())
181+
>>> result.success
182+
True
183+
184+
Handling timeouts:
185+
186+
>>> waiter = PaneWaiter(pane, timeout=0.1) # Short timeout
187+
>>> result = waiter.wait_for_text("won't appear")
188+
>>> result.success
189+
False
190+
>>> isinstance(result.error, WaiterTimeoutError)
191+
True
192+
"""
48193

49194
def __init__(self, pane: Pane, timeout: float = 2.0) -> None:
50195
"""Initialize PaneWaiter.
@@ -66,6 +211,9 @@ def _check_content(
66211
) -> bool:
67212
"""Check pane content against predicate.
68213
214+
This internal method captures the pane content and checks it against
215+
the provided predicate function.
216+
69217
Parameters
70218
----------
71219
predicate : Callable[[str], bool]
@@ -82,6 +230,16 @@ def _check_content(
82230
------
83231
WaiterContentError
84232
If there's an error capturing pane content
233+
234+
Examples
235+
--------
236+
>>> waiter = PaneWaiter(pane)
237+
>>> result = WaitResult[str](success=False)
238+
>>> success = waiter._check_content(lambda c: "test" in c, result)
239+
>>> success # True if "test" is found in pane content
240+
True
241+
>>> result.value is not None
242+
True
85243
"""
86244
try:
87245
content = "\n".join(self.pane.capture_pane())
@@ -101,7 +259,56 @@ def wait_for_content(
101259
interval_seconds: float | None = None,
102260
error_message: str | None = None,
103261
) -> WaitResult[str]:
104-
"""Wait for content in the pane to match a predicate."""
262+
"""Wait for content in the pane to match a predicate.
263+
264+
This is the core waiting method that other methods build upon. It repeatedly
265+
checks the pane content against a predicate function until it returns True
266+
or times out.
267+
268+
Parameters
269+
----------
270+
predicate : Callable[[str], bool]
271+
Function that takes pane content as string and returns bool
272+
timeout_seconds : float | None, optional
273+
Maximum time to wait in seconds, by default None (uses instance timeout)
274+
interval_seconds : float | None, optional
275+
Time between checks in seconds, by default None (uses 0.05)
276+
error_message : str | None, optional
277+
Custom error message for timeout, by default None
278+
279+
Returns
280+
-------
281+
WaitResult[str]
282+
Result of the wait operation
283+
284+
Examples
285+
--------
286+
>>> waiter = PaneWaiter(pane)
287+
>>> # Wait for content containing "success" but not "error"
288+
>>> result = waiter.wait_for_content(
289+
... lambda content: "success" in content and "error" not in content
290+
... )
291+
>>> result.success
292+
True
293+
294+
>>> # Wait with custom timeout and interval
295+
>>> result = waiter.wait_for_content(
296+
... lambda content: "test" in content,
297+
... timeout_seconds=5.0,
298+
... interval_seconds=0.1,
299+
... )
300+
>>> result.success
301+
True
302+
303+
>>> # Wait with custom error message
304+
>>> result = waiter.wait_for_content(
305+
... lambda content: False, # Never succeeds
306+
... timeout_seconds=0.1,
307+
... error_message="Custom timeout message",
308+
... )
309+
>>> str(result.error)
310+
'Custom timeout message'
311+
"""
105312
result = WaitResult[str](success=False, value=None, error=None)
106313
try:
107314
# Give the shell a moment to be ready
@@ -134,7 +341,40 @@ def wait_for_prompt(
134341
timeout_seconds: float | None = None,
135342
error_message: str | None = None,
136343
) -> WaitResult[str]:
137-
"""Wait for a specific prompt to appear in the pane."""
344+
"""Wait for a specific prompt to appear in the pane.
345+
346+
This method waits for a specific shell prompt to appear in the pane.
347+
It ensures the prompt is at the end of non-empty content.
348+
349+
Parameters
350+
----------
351+
prompt : str
352+
The prompt text to wait for
353+
timeout_seconds : float | None, optional
354+
Maximum time to wait in seconds, by default None (uses instance timeout)
355+
error_message : str | None, optional
356+
Custom error message for timeout, by default None
357+
358+
Returns
359+
-------
360+
WaitResult[str]
361+
Result of the wait operation
362+
363+
Examples
364+
--------
365+
>>> waiter = PaneWaiter(pane)
366+
>>> # Wait for bash prompt
367+
>>> result = waiter.wait_for_prompt("$ ")
368+
>>> result.success
369+
True
370+
>>> "$ " in result.value
371+
True
372+
373+
>>> # Wait for custom prompt
374+
>>> result = waiter.wait_for_prompt("my_prompt> ")
375+
>>> result.success
376+
True
377+
"""
138378
return self.wait_for_content(
139379
lambda content: prompt in content and len(content.strip()) > 0,
140380
timeout_seconds=timeout_seconds,
@@ -148,7 +388,46 @@ def wait_for_text(
148388
interval_seconds: float | None = None,
149389
error_message: str | None = None,
150390
) -> WaitResult[str]:
151-
"""Wait for text to appear in the pane."""
391+
"""Wait for text to appear in the pane.
392+
393+
This method waits for specific text to appear anywhere in the pane content.
394+
395+
Parameters
396+
----------
397+
text : str
398+
The text to wait for
399+
timeout_seconds : float | None, optional
400+
Maximum time to wait in seconds, by default None (uses instance timeout)
401+
interval_seconds : float | None, optional
402+
Time between checks in seconds, by default None (uses 0.05)
403+
error_message : str | None, optional
404+
Custom error message for timeout, by default None
405+
406+
Returns
407+
-------
408+
WaitResult[str]
409+
Result of the wait operation
410+
411+
Examples
412+
--------
413+
>>> waiter = PaneWaiter(pane)
414+
>>> # Send a command and wait for its output
415+
>>> pane.send_keys("echo 'Hello World'")
416+
>>> result = waiter.wait_for_text("Hello World")
417+
>>> result.success
418+
True
419+
>>> "Hello World" in result.value
420+
True
421+
422+
>>> # Wait with custom timeout
423+
>>> result = waiter.wait_for_text(
424+
... "test output",
425+
... timeout_seconds=5.0,
426+
... error_message="Failed to find test output",
427+
... )
428+
>>> result.success
429+
True
430+
"""
152431
if error_message is None:
153432
error_message = f"Text '{text}' not found in pane"
154433
return self.wait_for_content(

0 commit comments

Comments
 (0)