From f3c6a01b7ade02936384d6c17a0e2c8d47a97cd6 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Thu, 18 Jun 2026 16:41:30 -0400 Subject: [PATCH] yield drain() after telnetlib3-client stdout.write() (#145) When integrating with downstream [telix](https://github.com/jquast/telix), which uses a pretty novel technique of delegating all of telnet client code to telnetlib3-client, except to use a modified object for the ``stdout.write()`` calls. This is sort of asynchronous by accident, that the OS/PTY buffers and chops up the write calls into multiple chunks of syscalls. However, telix needs some signal to know about the "edge boundary" -- when a write call begins and ends, so that it is able to interject and perform read/write calls of its own. --- docs/history.rst | 3 +++ pyproject.toml | 2 +- telnetlib3/accessories.py | 2 +- telnetlib3/client_shell.py | 2 ++ telnetlib3/tests/test_client_shell.py | 5 +++-- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index bb9c2680..e38664e1 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,8 @@ History ======= +4.0.5 + * enhancement: ``telnetlib3-client`` client shell now drains stdout. + 4.0.4 * bugfix: servers using ``robot_check=True`` with ``encoding=False`` raised ``TypeError: buf expected bytes, got ``. ``telnetlib3-server`` now also accepts ``--encoding=False`` diff --git a/pyproject.toml b/pyproject.toml index 8efca6d0..9d4f1316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "telnetlib3" -version = "4.0.4" # Keep in sync with telnetlib3/accessories.py::get_version ! +version = "4.0.5" # Keep in sync with telnetlib3/accessories.py::get_version ! description = " Python Telnet server and client CLI and Protocol library" readme = "README.rst" license = "ISC" diff --git a/telnetlib3/accessories.py b/telnetlib3/accessories.py index 6233f0cc..27de148d 100644 --- a/telnetlib3/accessories.py +++ b/telnetlib3/accessories.py @@ -42,7 +42,7 @@ def get_version() -> str: """Return the current version of telnetlib3.""" - return "4.0.4" # keep in sync with pyproject.toml ! + return "4.0.5" # keep in sync with pyproject.toml ! def encoding_from_lang(lang: str) -> Optional[str]: diff --git a/telnetlib3/client_shell.py b/telnetlib3/client_shell.py index 353aad34..6fd8025b 100644 --- a/telnetlib3/client_shell.py +++ b/telnetlib3/client_shell.py @@ -579,6 +579,8 @@ async def _raw_event_loop( if raw_mode is None and want_repl(): state.reactivate_repl = True stdout.write(out.encode()) + if hasattr(stdout, 'drain'): + await stdout.drain() _ts_file = telnet_writer.ctx.typescript_file if _ts_file is not None: _ts_file.write(out) diff --git a/telnetlib3/tests/test_client_shell.py b/telnetlib3/tests/test_client_shell.py index 968bd641..9124df9b 100644 --- a/telnetlib3/tests/test_client_shell.py +++ b/telnetlib3/tests/test_client_shell.py @@ -760,8 +760,7 @@ def at_eof(self) -> bool: term = _make_term(writer) term.check_auto_mode = lambda switched_to_raw, last_will_echo: None - stdout = mock.Mock() - stdout.write = mock.Mock() + stdout = mock.Mock(spec=["write"]) close_calls: list[str] = [] @@ -823,6 +822,7 @@ def at_eof(self) -> bool: stdout = mock.Mock() stdout.write = mock.Mock() + stdout.drain = mock.AsyncMock() state = _RawLoopState( switched_to_raw=True, last_will_echo=False, local_echo=False, linesep="\r\n" @@ -839,6 +839,7 @@ def at_eof(self) -> bool: want_repl=lambda: False, ) assert "hello world" in ts_buf.getvalue() + stdout.drain.assert_called_once() @pytest.mark.asyncio