diff --git a/docs/history.rst b/docs/history.rst index e38664e..989140e 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,8 @@ History ======= +4.0.6 + * bugfix: default GMCP modules requested are now in lowercase instead of titlecase + 4.0.5 * enhancement: ``telnetlib3-client`` client shell now drains stdout. diff --git a/pyproject.toml b/pyproject.toml index 9d4f131..d1e4ccb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "telnetlib3" -version = "4.0.5" # Keep in sync with telnetlib3/accessories.py::get_version ! +version = "4.0.6" # 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 27de148..078fb1b 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.5" # keep in sync with pyproject.toml ! + return "4.0.6" # keep in sync with pyproject.toml ! def encoding_from_lang(lang: str) -> Optional[str]: diff --git a/telnetlib3/client.py b/telnetlib3/client.py index 52b504a..9eb0c83 100755 --- a/telnetlib3/client.py +++ b/telnetlib3/client.py @@ -26,14 +26,14 @@ #: Sub-modules are listed explicitly because not all servers treat #: top-level subscriptions as wildcards. _DEFAULT_GMCP_MODULES = [ - "Char 1", - "Char.Vitals 1", - "Char.Items 1", - "Room 1", - "Room.Info 1", - "Comm 1", - "Comm.Channel 1", - "Group 1", + "char 1", + "char.vitals 1", + "char.items 1", + "room 1", + "room.info 1", + "comm 1", + "comm.channel 1", + "group 1", ] @@ -197,8 +197,9 @@ def _send_gmcp_hello(self) -> None: from telnetlib3.accessories import get_version self.writer.send_gmcp("Core.Hello", {"client": "telnetlib3", "version": get_version()}) - self.writer.send_gmcp("Core.Supports.Set", self._gmcp_modules) - self.log.info("GMCP handshake: Core.Hello + Core.Supports.Set %s", self._gmcp_modules) + wire_modules = [m.lower() for m in self._gmcp_modules] + self.writer.send_gmcp("Core.Supports.Set", wire_modules) + self.log.info("GMCP handshake: Core.Hello + Core.Supports.Set %s", wire_modules) def on_gmcp(self, package: str, data: Any) -> None: """Store incoming GMCP data on ``writer.ctx``, merging dict updates.""" diff --git a/telnetlib3/tests/test_client_unit.py b/telnetlib3/tests/test_client_unit.py index 70cce62..3082260 100644 --- a/telnetlib3/tests/test_client_unit.py +++ b/telnetlib3/tests/test_client_unit.py @@ -2,6 +2,7 @@ import sys import types import asyncio +from unittest import mock # 3rd party import pytest @@ -516,6 +517,52 @@ async def test_on_gmcp_merges_dicts_on_writer_ctx(): assert client.writer.ctx.gmcp_data["Char.Vitals"] == {"hp": 63, "maxhp": 100} +def test_default_gmcp_modules_are_lowercase(): + for spec in cl._DEFAULT_GMCP_MODULES: + module_part = spec.rsplit(" ", 1)[0] + assert module_part == module_part.lower() + + +@pytest.mark.asyncio +async def test_send_gmcp_hello_lowercases_default_modules(): + client = _make_client() + calls: list[tuple[str, object]] = [] + mock_writer = mock.Mock() + mock_writer.send_gmcp.side_effect = lambda pkg, data: calls.append((pkg, data)) + client.writer = mock_writer + client._send_gmcp_hello() + assert client._gmcp_hello_sent is True + assert len(calls) == 2 + pkg_name, supports_set = calls[1] + assert pkg_name == "Core.Supports.Set" + for spec in supports_set: + assert spec == spec.lower() + + +@pytest.mark.asyncio +async def test_send_gmcp_hello_lowercases_consumer_modules(): + client = _make_client(gmcp_modules=["Room.Info 1", "Char 1"]) + calls: list[tuple[str, object]] = [] + mock_writer = mock.Mock() + mock_writer.send_gmcp.side_effect = lambda pkg, data: calls.append((pkg, data)) + client.writer = mock_writer + client._send_gmcp_hello() + _, supports_set = calls[1] + assert "room.info 1" in supports_set + assert "char 1" in supports_set + + +@pytest.mark.asyncio +async def test_send_gmcp_hello_idempotent(): + client = _make_client() + mock_writer = mock.Mock() + client.writer = mock_writer + client._send_gmcp_hello() + call_count = mock_writer.send_gmcp.call_count + client._send_gmcp_hello() + assert mock_writer.send_gmcp.call_count == call_count + + def test_fingerprint_main_oserror(monkeypatch): async def _bad_fp(): raise OSError("connection refused")