diff --git a/CHANGES b/CHANGES index 5ff9a810f..658479269 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,16 @@ $ pip install --user --upgrade --pre libtmux - _Insert changes/features/fixes for next release here_ +### Tests and docs + +- Initial [doctests] examples stubbed out {issue}`#394` + + [doctests]: https://docs.python.org/3/library/doctest.html + +- Fix bug in `temp_window()` context manager, {issue}`#394` +- Pytest configuration `conftest.py` moved to `libtmux/conftest.py`, so doctest can + detect the fixtures {issue}`#394` + ## libtmux 0.13.0 (2022-08-05) ### What's new diff --git a/docs/conf.py b/docs/conf.py index 7264b730b..cb7f649e5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,7 @@ from libtmux import test # NOQA # Get the project root dir, which is the parent dir of this -cwd = Path.cwd() +cwd = Path(__file__).parent project_root = cwd.parent sys.path.insert(0, str(project_root)) @@ -18,7 +18,7 @@ # package data about: Dict[str, str] = {} -with open("../libtmux/__about__.py") as fp: +with open(project_root / "libtmux" / "__about__.py") as fp: exec(fp.read(), about) extensions = [ diff --git a/libtmux/conftest.py b/libtmux/conftest.py new file mode 100644 index 000000000..b6e76a8c9 --- /dev/null +++ b/libtmux/conftest.py @@ -0,0 +1,119 @@ +import logging +import os +import typing as t + +import pytest + +from _pytest.fixtures import SubRequest +from _pytest.monkeypatch import MonkeyPatch + +from libtmux import exc +from libtmux.common import which +from libtmux.server import Server +from libtmux.test import TEST_SESSION_PREFIX, get_test_session_name, namer + +if t.TYPE_CHECKING: + from libtmux.session import Session + +logger = logging.getLogger(__name__) + + +@pytest.fixture(autouse=True) +def clear_env(monkeypatch: MonkeyPatch) -> None: + """Clear out any unnecessary environment variables that could interrupt tests. + + tmux show-environment tests were being interrupted due to a lot of crazy env vars. + """ + for k, v in os.environ.items(): + if not any( + needle in k.lower() + for needle in [ + "window", + "tmux", + "pane", + "session", + "pytest", + "path", + "pwd", + "shell", + "home", + "xdg", + "disable_auto_title", + "lang", + "term", + ] + ): + monkeypatch.delenv(k) + + +@pytest.fixture(scope="function") +def server(request: SubRequest, monkeypatch: MonkeyPatch) -> Server: + + t = Server() + t.socket_name = "tmuxp_test%s" % next(namer) + + def fin() -> None: + t.kill_server() + + request.addfinalizer(fin) + + return t + + +@pytest.fixture(scope="function") +def session(request: SubRequest, server: Server) -> "Session": + session_name = "tmuxp" + + if not server.has_session(session_name): + server.cmd("new-session", "-d", "-s", session_name) + + # find current sessions prefixed with tmuxp + old_test_sessions = [] + for s in server._sessions: + old_name = s.get("session_name") + if old_name is not None and old_name.startswith(TEST_SESSION_PREFIX): + old_test_sessions.append(old_name) + + TEST_SESSION_NAME = get_test_session_name(server=server) + + try: + session = server.new_session(session_name=TEST_SESSION_NAME) + except exc.LibTmuxException as e: + raise e + + """ + Make sure that tmuxp can :ref:`test_builder_visually` and switches to + the newly created session for that testcase. + """ + session_id = session.get("session_id") + assert session_id is not None + + try: + server.switch_client(target_session=session_id) + except exc.LibTmuxException: + # server.attach_session(session.get('session_id')) + pass + + for old_test_session in old_test_sessions: + logger.debug("Old test test session %s found. Killing it." % old_test_session) + server.kill_session(old_test_session) + assert TEST_SESSION_NAME == session.get("session_name") + assert TEST_SESSION_NAME != "tmuxp" + + return session + + +@pytest.fixture(autouse=True) +def add_doctest_fixtures( + doctest_namespace: t.Dict[str, t.Any], + # usefixtures / autouse + clear_env: t.Any, + # Normal fixtures + server: "Server", + session: "Session", +) -> None: + if which("tmux"): + doctest_namespace["server"] = server + doctest_namespace["session"] = session + doctest_namespace["window"] = session.attached_window + doctest_namespace["pane"] = session.attached_pane diff --git a/libtmux/pane.py b/libtmux/pane.py index 2e93dda7b..191dadeb0 100644 --- a/libtmux/pane.py +++ b/libtmux/pane.py @@ -36,6 +36,20 @@ class Pane(TmuxMappingObject): ---------- window : :class:`Window` + Examples + -------- + >>> pane + Pane(%1 Window(@1 ...:..., Session($1 ...))) + + >>> pane in window.panes + True + + >>> pane.window + Window(@1 ...:..., Session($1 ...)) + + >>> pane.session + Session($1 ...) + Notes ----- @@ -119,8 +133,7 @@ def send_keys( suppress_history: t.Optional[bool] = True, literal: t.Optional[bool] = False, ) -> None: - """ - ``$ tmux send-keys`` to the pane. + r"""``$ tmux send-keys`` to the pane. A leading space character is added to cmd to avoid polluting the user's history. @@ -135,6 +148,22 @@ def send_keys( Don't add these keys to the shell history, default True. literal : bool, optional Send keys literally, default True. + + Examples + -------- + >>> pane = window.split_window(shell='sh') + >>> pane.capture_pane() + ['$'] + + >>> pane.send_keys('echo "Hello world"', suppress_history=False, enter=True) + + >>> pane.capture_pane() + ['$ echo "Hello world"', 'Hello world', '$'] + + >>> print('\n'.join(pane.capture_pane())) # doctest: +NORMALIZE_WHITESPACE + $ echo "Hello world" + Hello world + $ """ prefix = " " if suppress_history else "" diff --git a/libtmux/server.py b/libtmux/server.py index b31a3d061..beaa82ae0 100644 --- a/libtmux/server.py +++ b/libtmux/server.py @@ -47,6 +47,23 @@ class Server(TmuxRelationalObject["Session", "SessionDict"], EnvironmentMixin): config_file : str, optional colors : str, optional + Examples + -------- + >>> server + + + >>> server.sessions + [Session($1 ...)] + + >>> server.sessions[0].windows + [Window(@1 ...:..., Session($1 ...)] + + >>> server.sessions[0].attached_window + Window(@1 ...:..., Session($1 ...) + + >>> server.sessions[0].attached_pane + Pane(%1 Window(@1 ...:..., Session($1 ...))) + References ---------- .. [server_manual] CLIENTS AND SESSIONS. openbsd manpage for TMUX(1) diff --git a/libtmux/session.py b/libtmux/session.py index 1133c9934..e2d1b614d 100644 --- a/libtmux/session.py +++ b/libtmux/session.py @@ -43,6 +43,20 @@ class Session( ---------- server : :class:`Server` + Examples + -------- + >>> session + Session($1 ...) + + >>> session.windows + [Window(@1 ...:..., Session($1 ...)] + + >>> session.attached_window + Window(@1 ...:..., Session($1 ...) + + >>> session.attached_pane + Pane(%1 Window(@1 ...:..., Session($1 ...))) + References ---------- .. [session_manual] tmux session. openbsd manpage for TMUX(1). diff --git a/libtmux/test.py b/libtmux/test.py index 4af61b11e..19e9a8b29 100644 --- a/libtmux/test.py +++ b/libtmux/test.py @@ -16,6 +16,7 @@ if t.TYPE_CHECKING: from libtmux.session import Session + from libtmux.window import Window TEST_SESSION_PREFIX = "libtmux_" RETRY_TIMEOUT_SECONDS = int(os.getenv("RETRY_TIMEOUT_SECONDS", 8)) @@ -68,16 +69,17 @@ def retry_until( Examples -------- - >>> def f(): - ... p = w.attached_pane + >>> def fn(): + ... p = session.attached_window.attached_pane ... p.server._update_panes() - ... return p.current_path == pane_path - ... - ... retry(f) + ... return p.current_path is not None + + >>> retry_until(fn) + True In pytest: - >>> assert retry(f, raises=False) + >>> assert retry_until(fn, raises=False) """ ini = time.time() @@ -179,6 +181,7 @@ def temp_session( >>> with temp_session(server) as session: ... session.new_window(window_name='my window') + Window(@... ...:..., Session($... ...)) """ if "session_name" in kwargs: @@ -199,7 +202,7 @@ def temp_session( @contextlib.contextmanager def temp_window( session: "Session", *args: t.Any, **kwargs: t.Any -) -> t.Generator["Session", t.Any, t.Any]: +) -> t.Generator["Window", t.Any, t.Any]: """ Return a context manager with a temporary window. @@ -229,7 +232,13 @@ def temp_window( -------- >>> with temp_window(session) as window: - ... my_pane = window.split_window() + ... window + Window(@... ...:..., Session($... ...)) + + + >>> with temp_window(session) as window: + ... window.split_window() + Pane(%... Window(@... ...:..., Session($... ...))) """ if "window_name" not in kwargs: @@ -245,7 +254,7 @@ def temp_window( assert isinstance(window_id, str) try: - yield session + yield window finally: if session.find_where({"window_id": window_id}): window.kill_window() diff --git a/libtmux/window.py b/libtmux/window.py index addfae7e1..d38b8298c 100644 --- a/libtmux/window.py +++ b/libtmux/window.py @@ -39,6 +39,32 @@ class Window(TmuxMappingObject, TmuxRelationalObject["Pane", "PaneDict"]): ---------- session : :class:`Session` + Examples + -------- + >>> window = session.new_window('My project') + + >>> window + Window(@... ...:My project, Session($... ...)) + + Windows have panes: + + >>> window.panes + [Pane(...)] + + >>> window.attached_pane + Pane(...) + + Relations moving up: + + >>> window.session + Session(...) + + >>> window == session.attached_window + True + + >>> window in session.windows + True + References ---------- .. [window_manual] tmux window. openbsd manpage for TMUX(1). @@ -296,6 +322,17 @@ def rename_window(self, new_name: str) -> "Window": ---------- new_name : str name of the window + + Examples + -------- + + >>> window = session.attached_window + + >>> window.rename_window('My project') + Window(@1 ...:My project, Session($1 ...)) + + >>> window.rename_window('New name') + Window(@1 ...:New name, Session($1 ...)) """ import shlex diff --git a/setup.cfg b/setup.cfg index b2e5f38f1..7139e237f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,4 +19,5 @@ line_length = 88 [tool:pytest] filterwarnings = ignore:.* Use packaging.version.*:DeprecationWarning:: - +addopts = --tb=short --no-header --showlocals --doctest-modules +doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE diff --git a/tests/conftest.py b/tests/conftest.py index 2a6d87320..535e57934 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,102 +1 @@ -import logging -import os -import typing as t - -import pytest - -from _pytest.fixtures import SubRequest -from _pytest.monkeypatch import MonkeyPatch - -from libtmux import exc -from libtmux.server import Server -from libtmux.test import TEST_SESSION_PREFIX, get_test_session_name, namer - -if t.TYPE_CHECKING: - from libtmux.session import Session - -logger = logging.getLogger(__name__) - - -@pytest.fixture(autouse=True) -def clear_env(monkeypatch: MonkeyPatch) -> None: - """Clear out any unnecessary environment variables that could interrupt tests. - - tmux show-environment tests were being interrupted due to a lot of crazy env vars. - """ - for k, v in os.environ.items(): - if not any( - needle in k.lower() - for needle in [ - "window", - "tmux", - "pane", - "session", - "pytest", - "path", - "pwd", - "shell", - "home", - "xdg", - "disable_auto_title", - "lang", - "term", - ] - ): - monkeypatch.delenv(k) - - -@pytest.fixture(scope="function") -def server(request: SubRequest, monkeypatch: MonkeyPatch) -> Server: - - t = Server() - t.socket_name = "tmuxp_test%s" % next(namer) - - def fin() -> None: - t.kill_server() - - request.addfinalizer(fin) - - return t - - -@pytest.fixture(scope="function") -def session(request: SubRequest, server: Server) -> "Session": - session_name = "tmuxp" - - if not server.has_session(session_name): - server.cmd("new-session", "-d", "-s", session_name) - - # find current sessions prefixed with tmuxp - old_test_sessions = [] - for s in server._sessions: - old_name = s.get("session_name") - if old_name is not None and old_name.startswith(TEST_SESSION_PREFIX): - old_test_sessions.append(old_name) - - TEST_SESSION_NAME = get_test_session_name(server=server) - - try: - session = server.new_session(session_name=TEST_SESSION_NAME) - except exc.LibTmuxException as e: - raise e - - """ - Make sure that tmuxp can :ref:`test_builder_visually` and switches to - the newly created session for that testcase. - """ - session_id = session.get("session_id") - assert session_id is not None - - try: - server.switch_client(target_session=session_id) - except exc.LibTmuxException: - # server.attach_session(session.get('session_id')) - pass - - for old_test_session in old_test_sessions: - logger.debug("Old test test session %s found. Killing it." % old_test_session) - server.kill_session(old_test_session) - assert TEST_SESSION_NAME == session.get("session_name") - assert TEST_SESSION_NAME != "tmuxp" - - return session +from libtmux.conftest import * # noqa F40