Skip to content

Commit fe31aad

Browse files
authored
tests: Add basic support for doctests in libtmux (#394)
See also: https://docs.python.org/3/library/doctest.html
2 parents e3895d0 + 83e0e4b commit fe31aad

File tree

10 files changed

+251
-116
lines changed

10 files changed

+251
-116
lines changed

CHANGES

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ $ pip install --user --upgrade --pre libtmux
1212

1313
- _Insert changes/features/fixes for next release here_
1414

15+
### Tests and docs
16+
17+
- Initial [doctests] examples stubbed out {issue}`#394`
18+
19+
[doctests]: https://docs.python.org/3/library/doctest.html
20+
21+
- Fix bug in `temp_window()` context manager, {issue}`#394`
22+
- Pytest configuration `conftest.py` moved to `libtmux/conftest.py`, so doctest can
23+
detect the fixtures {issue}`#394`
24+
1525
## libtmux 0.13.0 (2022-08-05)
1626

1727
### What's new

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
from libtmux import test # NOQA
1111

1212
# Get the project root dir, which is the parent dir of this
13-
cwd = Path.cwd()
13+
cwd = Path(__file__).parent
1414
project_root = cwd.parent
1515

1616
sys.path.insert(0, str(project_root))
1717
sys.path.insert(0, str(cwd / "_ext"))
1818

1919
# package data
2020
about: Dict[str, str] = {}
21-
with open("../libtmux/__about__.py") as fp:
21+
with open(project_root / "libtmux" / "__about__.py") as fp:
2222
exec(fp.read(), about)
2323

2424
extensions = [

libtmux/conftest.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import logging
2+
import os
3+
import typing as t
4+
5+
import pytest
6+
7+
from _pytest.fixtures import SubRequest
8+
from _pytest.monkeypatch import MonkeyPatch
9+
10+
from libtmux import exc
11+
from libtmux.common import which
12+
from libtmux.server import Server
13+
from libtmux.test import TEST_SESSION_PREFIX, get_test_session_name, namer
14+
15+
if t.TYPE_CHECKING:
16+
from libtmux.session import Session
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
@pytest.fixture(autouse=True)
22+
def clear_env(monkeypatch: MonkeyPatch) -> None:
23+
"""Clear out any unnecessary environment variables that could interrupt tests.
24+
25+
tmux show-environment tests were being interrupted due to a lot of crazy env vars.
26+
"""
27+
for k, v in os.environ.items():
28+
if not any(
29+
needle in k.lower()
30+
for needle in [
31+
"window",
32+
"tmux",
33+
"pane",
34+
"session",
35+
"pytest",
36+
"path",
37+
"pwd",
38+
"shell",
39+
"home",
40+
"xdg",
41+
"disable_auto_title",
42+
"lang",
43+
"term",
44+
]
45+
):
46+
monkeypatch.delenv(k)
47+
48+
49+
@pytest.fixture(scope="function")
50+
def server(request: SubRequest, monkeypatch: MonkeyPatch) -> Server:
51+
52+
t = Server()
53+
t.socket_name = "tmuxp_test%s" % next(namer)
54+
55+
def fin() -> None:
56+
t.kill_server()
57+
58+
request.addfinalizer(fin)
59+
60+
return t
61+
62+
63+
@pytest.fixture(scope="function")
64+
def session(request: SubRequest, server: Server) -> "Session":
65+
session_name = "tmuxp"
66+
67+
if not server.has_session(session_name):
68+
server.cmd("new-session", "-d", "-s", session_name)
69+
70+
# find current sessions prefixed with tmuxp
71+
old_test_sessions = []
72+
for s in server._sessions:
73+
old_name = s.get("session_name")
74+
if old_name is not None and old_name.startswith(TEST_SESSION_PREFIX):
75+
old_test_sessions.append(old_name)
76+
77+
TEST_SESSION_NAME = get_test_session_name(server=server)
78+
79+
try:
80+
session = server.new_session(session_name=TEST_SESSION_NAME)
81+
except exc.LibTmuxException as e:
82+
raise e
83+
84+
"""
85+
Make sure that tmuxp can :ref:`test_builder_visually` and switches to
86+
the newly created session for that testcase.
87+
"""
88+
session_id = session.get("session_id")
89+
assert session_id is not None
90+
91+
try:
92+
server.switch_client(target_session=session_id)
93+
except exc.LibTmuxException:
94+
# server.attach_session(session.get('session_id'))
95+
pass
96+
97+
for old_test_session in old_test_sessions:
98+
logger.debug("Old test test session %s found. Killing it." % old_test_session)
99+
server.kill_session(old_test_session)
100+
assert TEST_SESSION_NAME == session.get("session_name")
101+
assert TEST_SESSION_NAME != "tmuxp"
102+
103+
return session
104+
105+
106+
@pytest.fixture(autouse=True)
107+
def add_doctest_fixtures(
108+
doctest_namespace: t.Dict[str, t.Any],
109+
# usefixtures / autouse
110+
clear_env: t.Any,
111+
# Normal fixtures
112+
server: "Server",
113+
session: "Session",
114+
) -> None:
115+
if which("tmux"):
116+
doctest_namespace["server"] = server
117+
doctest_namespace["session"] = session
118+
doctest_namespace["window"] = session.attached_window
119+
doctest_namespace["pane"] = session.attached_pane

libtmux/pane.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ class Pane(TmuxMappingObject):
3636
----------
3737
window : :class:`Window`
3838
39+
Examples
40+
--------
41+
>>> pane
42+
Pane(%1 Window(@1 ...:..., Session($1 ...)))
43+
44+
>>> pane in window.panes
45+
True
46+
47+
>>> pane.window
48+
Window(@1 ...:..., Session($1 ...))
49+
50+
>>> pane.session
51+
Session($1 ...)
52+
3953
Notes
4054
-----
4155
@@ -119,8 +133,7 @@ def send_keys(
119133
suppress_history: t.Optional[bool] = True,
120134
literal: t.Optional[bool] = False,
121135
) -> None:
122-
"""
123-
``$ tmux send-keys`` to the pane.
136+
r"""``$ tmux send-keys`` to the pane.
124137
125138
A leading space character is added to cmd to avoid polluting the
126139
user's history.
@@ -135,6 +148,22 @@ def send_keys(
135148
Don't add these keys to the shell history, default True.
136149
literal : bool, optional
137150
Send keys literally, default True.
151+
152+
Examples
153+
--------
154+
>>> pane = window.split_window(shell='sh')
155+
>>> pane.capture_pane()
156+
['$']
157+
158+
>>> pane.send_keys('echo "Hello world"', suppress_history=False, enter=True)
159+
160+
>>> pane.capture_pane()
161+
['$ echo "Hello world"', 'Hello world', '$']
162+
163+
>>> print('\n'.join(pane.capture_pane())) # doctest: +NORMALIZE_WHITESPACE
164+
$ echo "Hello world"
165+
Hello world
166+
$
138167
"""
139168
prefix = " " if suppress_history else ""
140169

libtmux/server.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ class Server(TmuxRelationalObject["Session", "SessionDict"], EnvironmentMixin):
4747
config_file : str, optional
4848
colors : str, optional
4949
50+
Examples
51+
--------
52+
>>> server
53+
<libtmux.server.Server object at ...>
54+
55+
>>> server.sessions
56+
[Session($1 ...)]
57+
58+
>>> server.sessions[0].windows
59+
[Window(@1 ...:..., Session($1 ...)]
60+
61+
>>> server.sessions[0].attached_window
62+
Window(@1 ...:..., Session($1 ...)
63+
64+
>>> server.sessions[0].attached_pane
65+
Pane(%1 Window(@1 ...:..., Session($1 ...)))
66+
5067
References
5168
----------
5269
.. [server_manual] CLIENTS AND SESSIONS. openbsd manpage for TMUX(1)

libtmux/session.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ class Session(
4343
----------
4444
server : :class:`Server`
4545
46+
Examples
47+
--------
48+
>>> session
49+
Session($1 ...)
50+
51+
>>> session.windows
52+
[Window(@1 ...:..., Session($1 ...)]
53+
54+
>>> session.attached_window
55+
Window(@1 ...:..., Session($1 ...)
56+
57+
>>> session.attached_pane
58+
Pane(%1 Window(@1 ...:..., Session($1 ...)))
59+
4660
References
4761
----------
4862
.. [session_manual] tmux session. openbsd manpage for TMUX(1).

libtmux/test.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
if t.TYPE_CHECKING:
1818
from libtmux.session import Session
19+
from libtmux.window import Window
1920

2021
TEST_SESSION_PREFIX = "libtmux_"
2122
RETRY_TIMEOUT_SECONDS = int(os.getenv("RETRY_TIMEOUT_SECONDS", 8))
@@ -68,16 +69,17 @@ def retry_until(
6869
Examples
6970
--------
7071
71-
>>> def f():
72-
... p = w.attached_pane
72+
>>> def fn():
73+
... p = session.attached_window.attached_pane
7374
... p.server._update_panes()
74-
... return p.current_path == pane_path
75-
...
76-
... retry(f)
75+
... return p.current_path is not None
76+
77+
>>> retry_until(fn)
78+
True
7779
7880
In pytest:
7981
80-
>>> assert retry(f, raises=False)
82+
>>> assert retry_until(fn, raises=False)
8183
"""
8284
ini = time.time()
8385

@@ -179,6 +181,7 @@ def temp_session(
179181
180182
>>> with temp_session(server) as session:
181183
... session.new_window(window_name='my window')
184+
Window(@... ...:..., Session($... ...))
182185
"""
183186

184187
if "session_name" in kwargs:
@@ -199,7 +202,7 @@ def temp_session(
199202
@contextlib.contextmanager
200203
def temp_window(
201204
session: "Session", *args: t.Any, **kwargs: t.Any
202-
) -> t.Generator["Session", t.Any, t.Any]:
205+
) -> t.Generator["Window", t.Any, t.Any]:
203206
"""
204207
Return a context manager with a temporary window.
205208
@@ -229,7 +232,13 @@ def temp_window(
229232
--------
230233
231234
>>> with temp_window(session) as window:
232-
... my_pane = window.split_window()
235+
... window
236+
Window(@... ...:..., Session($... ...))
237+
238+
239+
>>> with temp_window(session) as window:
240+
... window.split_window()
241+
Pane(%... Window(@... ...:..., Session($... ...)))
233242
"""
234243

235244
if "window_name" not in kwargs:
@@ -245,7 +254,7 @@ def temp_window(
245254
assert isinstance(window_id, str)
246255

247256
try:
248-
yield session
257+
yield window
249258
finally:
250259
if session.find_where({"window_id": window_id}):
251260
window.kill_window()

libtmux/window.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,32 @@ class Window(TmuxMappingObject, TmuxRelationalObject["Pane", "PaneDict"]):
3939
----------
4040
session : :class:`Session`
4141
42+
Examples
43+
--------
44+
>>> window = session.new_window('My project')
45+
46+
>>> window
47+
Window(@... ...:My project, Session($... ...))
48+
49+
Windows have panes:
50+
51+
>>> window.panes
52+
[Pane(...)]
53+
54+
>>> window.attached_pane
55+
Pane(...)
56+
57+
Relations moving up:
58+
59+
>>> window.session
60+
Session(...)
61+
62+
>>> window == session.attached_window
63+
True
64+
65+
>>> window in session.windows
66+
True
67+
4268
References
4369
----------
4470
.. [window_manual] tmux window. openbsd manpage for TMUX(1).
@@ -296,6 +322,17 @@ def rename_window(self, new_name: str) -> "Window":
296322
----------
297323
new_name : str
298324
name of the window
325+
326+
Examples
327+
--------
328+
329+
>>> window = session.attached_window
330+
331+
>>> window.rename_window('My project')
332+
Window(@1 ...:My project, Session($1 ...))
333+
334+
>>> window.rename_window('New name')
335+
Window(@1 ...:New name, Session($1 ...))
299336
"""
300337

301338
import shlex

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ line_length = 88
1919
[tool:pytest]
2020
filterwarnings =
2121
ignore:.* Use packaging.version.*:DeprecationWarning::
22-
22+
addopts = --tb=short --no-header --showlocals --doctest-modules
23+
doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE

0 commit comments

Comments
 (0)