Repository: tmux-python/libtmux

Issue:
Doctest examples
We should have doctest examples everywhere we can. Even if tests are 4x as long

Comments:

Patch:
diff --git a/CHANGES b/CHANGES
index 5ff9a81..6584792 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 7264b73..cb7f649 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,7 +10,7 @@ import libtmux  # NOQA
 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 @@ sys.path.insert(0, str(cwd / "_ext"))
 
 # 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/pane.py b/libtmux/pane.py
index 2e93dda..191dade 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 @@ class Pane(TmuxMappingObject):
         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 @@ class Pane(TmuxMappingObject):
             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 b31a3d0..beaa82a 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
+    <libtmux.server.Server object at ...>
+
+    >>> 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 1133c99..e2d1b61 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/window.py b/libtmux/window.py
index addfae7..d38b829 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 @@ class Window(TmuxMappingObject, TmuxRelationalObject["Pane", "PaneDict"]):
         ----------
         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 b2e5f38..7139e23 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


Test patch:
diff --git a/libtmux/conftest.py b/libtmux/conftest.py
new file mode 100644
index 0000000..b6e76a8
--- /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/test.py b/libtmux/test.py
index 4af61b1..19e9a8b 100644
--- a/libtmux/test.py
+++ b/libtmux/test.py
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
 
 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/tests/conftest.py b/tests/conftest.py
index 2a6d873..535e579 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,102 +1,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