Skip to content

Commit 3b158d0

Browse files
committed
ControlModeEngine(types): Introduce process protocol for test hooks
why: Remove mypy ignores and support injectable control-mode process factory. what: - Define _ControlProcess and _ProcessFactory protocols for control-mode transport - Type engine process and helper threads against protocol instead of Popen - Make close/restart logic rely on protocol methods without ignores
1 parent 68d0965 commit 3b158d0

File tree

1 file changed

+47
-15
lines changed

1 file changed

+47
-15
lines changed

src/libtmux/_internal/engines/control_mode.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,40 @@
2828
logger = logging.getLogger(__name__)
2929

3030

31+
class _ControlProcess(t.Protocol):
32+
"""Protocol for control-mode process handle (real or test fake)."""
33+
34+
stdin: t.TextIO | None
35+
stdout: t.Iterable[str] | None
36+
stderr: t.Iterable[str] | None
37+
38+
def terminate(self) -> None:
39+
...
40+
41+
def kill(self) -> None:
42+
...
43+
44+
def wait(self, timeout: float | None = None) -> t.Any:
45+
...
46+
47+
48+
class _ProcessFactory(t.Protocol):
49+
"""Protocol for constructing a control-mode process."""
50+
51+
def __call__(
52+
self,
53+
cmd: list[str],
54+
*,
55+
stdin: t.Any,
56+
stdout: t.Any,
57+
stderr: t.Any,
58+
text: bool,
59+
bufsize: int,
60+
errors: str,
61+
) -> _ControlProcess:
62+
...
63+
64+
3165
class ControlModeEngine(Engine):
3266
"""Engine that runs tmux commands via a persistent Control Mode process.
3367
@@ -46,7 +80,7 @@ def __init__(
4680
notification_queue_size: int = 4096,
4781
internal_session_name: str | None = None,
4882
attach_to: str | None = None,
49-
process_factory: t.Callable[[list[str]], subprocess.Popen[str]] | None = None,
83+
process_factory: _ProcessFactory | None = None,
5084
) -> None:
5185
"""Initialize control mode engine.
5286
@@ -69,12 +103,12 @@ def __init__(
69103
.. warning::
70104
Attaching to user sessions can cause notification spam from
71105
pane output. Use for advanced scenarios only.
72-
process_factory : Callable[[list[str]], subprocess.Popen], optional
106+
process_factory : _ProcessFactory, optional
73107
Test hook to override how the tmux control-mode process is created.
74108
When provided, it receives the argv list and must return an object
75109
compatible with ``subprocess.Popen`` (stdin/stdout/stderr streams).
76110
"""
77-
self.process: subprocess.Popen[str] | None = None
111+
self.process: _ControlProcess | None = None
78112
self._lock = threading.Lock()
79113
self._server_args: tuple[str | int, ...] | None = None
80114
self.command_timeout = command_timeout
@@ -98,15 +132,11 @@ def close(self) -> None:
98132
return
99133

100134
try:
101-
if hasattr(proc, "terminate"):
102-
proc.terminate() # type: ignore[call-arg]
103-
if hasattr(proc, "wait"):
104-
proc.wait(timeout=1) # type: ignore[call-arg]
135+
proc.terminate()
136+
proc.wait(timeout=1)
105137
except subprocess.TimeoutExpired:
106-
if hasattr(proc, "kill"):
107-
proc.kill() # type: ignore[call-arg]
108-
if hasattr(proc, "wait"):
109-
proc.wait() # type: ignore[call-arg]
138+
proc.kill()
139+
proc.wait()
110140
finally:
111141
self.process = None
112142
self._server_args = None
@@ -361,8 +391,10 @@ def _start_process(self, server_args: tuple[str | int, ...]) -> None:
361391
]
362392

363393
logger.debug("Starting Control Mode process: %s", cmd)
364-
popen_factory = self._process_factory or subprocess.Popen
365-
self.process = popen_factory( # type: ignore[arg-type]
394+
popen_factory: _ProcessFactory = (
395+
self._process_factory or subprocess.Popen # type: ignore[assignment]
396+
)
397+
self.process = popen_factory(
366398
cmd,
367399
stdin=subprocess.PIPE,
368400
stdout=subprocess.PIPE,
@@ -416,7 +448,7 @@ def _write_line(
416448
msg = "control mode process unavailable"
417449
raise exc.ControlModeConnectionError(msg) from None
418450

419-
def _reader(self, process: subprocess.Popen[str]) -> None:
451+
def _reader(self, process: _ControlProcess) -> None:
420452
assert process.stdout is not None
421453
try:
422454
for raw in process.stdout:
@@ -426,7 +458,7 @@ def _reader(self, process: subprocess.Popen[str]) -> None:
426458
finally:
427459
self._protocol.mark_dead("EOF from tmux")
428460

429-
def _drain_stderr(self, process: subprocess.Popen[str]) -> None:
461+
def _drain_stderr(self, process: _ControlProcess) -> None:
430462
if process.stderr is None:
431463
return
432464
for err_line in process.stderr:

0 commit comments

Comments
 (0)