diff --git a/pyproject.toml b/pyproject.toml index 261a80d..b4fa720 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "a telnet mud engine" requires-python = ">=3.12" dependencies = [ - "telnetlib3", + "telnetlib3 @ file:///home/jtm/src/telnetlib3", ] [dependency-groups] @@ -19,6 +19,9 @@ dev = [ requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.metadata] +allow-direct-references = true + [tool.hatch.build.targets.wheel] packages = ["src/mudlib"] diff --git a/src/mudlib/server.py b/src/mudlib/server.py index 07f0c9b..d256726 100644 --- a/src/mudlib/server.py +++ b/src/mudlib/server.py @@ -13,8 +13,6 @@ async def shell( writer: telnetlib3.TelnetWriter | telnetlib3.TelnetWriterUnicode, ) -> None: """Shell callback that greets the player and echoes their input.""" - # Cast to unicode variants since we're using encoding="utf8" - _reader = cast(telnetlib3.TelnetReaderUnicode, reader) _writer = cast(telnetlib3.TelnetWriterUnicode, writer) _writer.write("Welcome to the MUD!\r\n") @@ -25,8 +23,10 @@ async def shell( _writer.write("mud> ") await _writer.drain() - inp = await _reader.readline() - if not inp: + # readline_async reads char-by-char, handles echo via writer.echo(), + # and supports backspace editing + inp = await telnetlib3.readline_async(reader, writer) + if inp is None: break command = inp.strip() diff --git a/tests/test_server.py b/tests/test_server.py index 873cf59..557ca13 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,7 +1,7 @@ """Tests for the server module.""" import asyncio -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -9,84 +9,69 @@ from mudlib import server def test_port_constant(): - """Test that PORT is defined correctly.""" assert server.PORT == 6789 assert isinstance(server.PORT, int) def test_shell_exists(): - """Test that the shell callback exists and is callable.""" assert callable(server.shell) assert asyncio.iscoroutinefunction(server.shell) def test_run_server_exists(): - """Test that run_server exists and is callable.""" assert callable(server.run_server) assert asyncio.iscoroutinefunction(server.run_server) @pytest.mark.asyncio async def test_shell_greets_and_echoes(): - """Test that shell greets the player and echoes input.""" reader = AsyncMock() writer = MagicMock() writer.is_closing.return_value = False writer.drain = AsyncMock() writer.close = MagicMock() - # Simulate user typing "hello" then "quit" - reader.readline.side_effect = ["hello\r\n", "quit\r\n"] + readline = "mudlib.server.telnetlib3.readline_async" + with patch(readline, new_callable=AsyncMock) as mock_readline: + mock_readline.side_effect = ["hello", "quit"] + await server.shell(reader, writer) - await server.shell(reader, writer) - - # Check that welcome message was written calls = [str(call) for call in writer.write.call_args_list] assert any("Welcome" in call for call in calls) - - # Check that input was echoed assert any("hello" in call for call in calls) - - # Check that goodbye was written assert any("Goodbye" in call for call in calls) - - # Check that close was called writer.close.assert_called_once() @pytest.mark.asyncio -async def test_shell_handles_empty_input(): - """Test that shell handles EOF gracefully.""" +async def test_shell_handles_eof(): reader = AsyncMock() writer = MagicMock() writer.is_closing.return_value = False writer.drain = AsyncMock() writer.close = MagicMock() - # Simulate EOF - reader.readline.return_value = "" + readline = "mudlib.server.telnetlib3.readline_async" + with patch(readline, new_callable=AsyncMock) as mock_readline: + mock_readline.return_value = None + await server.shell(reader, writer) - await server.shell(reader, writer) - - # Should close cleanly writer.close.assert_called_once() @pytest.mark.asyncio async def test_shell_handles_quit(): - """Test that shell exits on quit command.""" reader = AsyncMock() writer = MagicMock() writer.is_closing.return_value = False writer.drain = AsyncMock() writer.close = MagicMock() - reader.readline.return_value = "quit\r\n" + readline = "mudlib.server.telnetlib3.readline_async" + with patch(readline, new_callable=AsyncMock) as mock_readline: + mock_readline.return_value = "quit" + await server.shell(reader, writer) - await server.shell(reader, writer) - - # Check that goodbye was written calls = [str(call) for call in writer.write.call_args_list] assert any("Goodbye" in call for call in calls) - writer.close.assert_called_once() diff --git a/uv.lock b/uv.lock index fa05154..9dce413 100644 --- a/uv.lock +++ b/uv.lock @@ -37,7 +37,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "telnetlib3" }] +requires-dist = [{ name = "telnetlib3", directory = "../../src/telnetlib3" }] [package.metadata.requires-dev] dev = [ @@ -153,11 +153,17 @@ wheels = [ [[package]] name = "telnetlib3" version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/2a/a9a7a4cb24626493806d95df0b19740d40a5583836ee070970489ca063b1/telnetlib3-2.2.0.tar.gz", hash = "sha256:85312604c9f52914938fe3697e5bbd219adab446af0df3045f21b07ba5417f73", size = 211769, upload-time = "2026-02-06T20:21:48.288Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/62/51a035c18305402ac5c5eefa88fdaf7e14eba6f09e3a17a218d72fe2c18b/telnetlib3-2.2.0-py3-none-any.whl", hash = "sha256:028648619e66d1a746791a3ef3e2a1cc7ffe4b78cf283c8028bef9e493f30554", size = 219273, upload-time = "2026-02-06T20:21:46.956Z" }, +source = { directory = "../../src/telnetlib3" } + +[package.metadata] +requires-dist = [ + { name = "prettytable", marker = "extra == 'extras'" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">3" }, + { name = "sphinx-autodoc-typehints", marker = "extra == 'docs'" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'" }, + { name = "ucs-detect", marker = "extra == 'extras'", specifier = ">=2" }, ] +provides-extras = ["docs", "extras"] [[package]] name = "typing-extensions"