Skip to content

Commit 1bfd2b0

Browse files
authored
3.0.0: Hawk TUI release (jquast#125)
3.0.0 * change: :attr:`~telnetlib3.client_base.BaseClient.connect_minwait` default now 0 (was 1.0 seconds in library API). * change: ``force_binary`` auto-enabled when CHARSET is negotiated (:rfc:`2066`) or ``LANG``/``CHARSET`` received via NEW_ENVIRON (:rfc:`1572`). SyncTERM font detection also enables it unconditionally. * change: ``--connect-timeout`` default changed from no limit to 10 seconds. * change: ``--reverse-video`` CLI option from 2.4.0 was removed. * change: CGA, EGA, and Amiga palettes removed from ``--colormatch``; only ``vga`` is available at this time. ``ice_colors`` are now True by default. * bugfix: ``read_some()`` in synchronous API (``TelnetConnection`` and ``ServerConnection``) blocked until EOF instead of returning available data. Now returns as soon as any data is available. * new: ``TelnetSessionContext`` base class and ``writer.ctx`` attribute for per-connection session state. Subclass to add application-specific attributes (e.g. MUD client state). * new: ``--ice-colors`` (default on) treats SGR 5 (blink) as bright background for proper 16-color BBS/ANSI art display. * new: ``--typescript FILE`` records session output to a file, similar to the Unix ``script(1)`` command. * new: shared ``TelnetProtocolBase`` mixin extracted from duplicated server and client protocol code. * new: ``_atomic_json_write()`` and ``_BytesSafeEncoder`` helpers in ``_paths`` module for fingerprinting subsystem. * enhancement: Microsoft Telnet (``telnet.exe``) compatibility refined — server now sends ``DO NEW_ENVIRON`` but excludes ``USER`` variable instead of skipping the option entirely, :ghissue:`24`. * enhancement: comprehensive pylint and mypy cleanup across the codebase.
1 parent 5a1687b commit 1bfd2b0

84 files changed

Lines changed: 6474 additions & 5208 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ jobs:
5050
run: python -Im tox -e ${{ matrix.toxenv }}
5151

5252
tests:
53-
name: Python ${{ matrix.python-version }} (${{ matrix.os }})${{ matrix.asyncio-debug && ' [asyncio-debug]' || '' }}
53+
name: Python ${{ matrix.python-version }} (${{ matrix.os }})${{ matrix.asyncio-debug && ' [asyncio-debug]' || '' }}${{ matrix.optional && ' [OPTIONAL]' || '' }}
54+
continue-on-error: ${{ matrix.optional || false }}
5455
strategy:
5556
fail-fast: false
5657
matrix:
@@ -65,15 +66,22 @@ jobs:
6566
- "3.13"
6667

6768
include:
68-
# Python 3.14 pre-release
69+
# Python 3.14 pre-release (Linux + Windows)
6970
- os: ubuntu-latest
7071
python-version: "3.14"
72+
- os: windows-latest
73+
python-version: "3.14"
7174

7275
# Python 3.14 with asyncio debug mode
7376
- os: ubuntu-latest
7477
python-version: "3.14"
7578
asyncio-debug: true
7679

80+
# Python 3.15 pre-release (optional)
81+
- os: ubuntu-latest
82+
python-version: "3.15"
83+
optional: true
84+
7785
runs-on: ${{ matrix.os }}
7886

7987
steps:
@@ -145,9 +153,8 @@ jobs:
145153
python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
146154
147155
- name: Upload coverage to Codecov
148-
uses: codecov/codecov-action@v4
156+
uses: codecov/codecov-action@v5
149157
with:
150-
token: ${{ secrets.CODECOV_TOKEN }}
151158
fail_ci_if_error: false
152159
verbose: true
153160

README.rst

Lines changed: 88 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -38,111 +38,109 @@ The python telnetlib.py_ module removed by Python 3.13 is also re-distributed as
3838

3939
See the `Guidebook`_ for examples and the `API documentation`_.
4040

41-
Asyncio Protocol
42-
----------------
43-
44-
The core protocol and CLI utilities are written using an `Asyncio Interface`_.
45-
46-
Blocking API
47-
------------
48-
49-
A Synchronous interface, modeled after telnetlib.py_ (client) and miniboa_ (server), with various
50-
enhancements in protocol negotiation is also provided. See `sync API documentation`_ for more.
51-
5241
Command-line Utilities
5342
----------------------
5443

55-
Two CLI tools are included: ``telnetlib3-client`` for connecting to servers
56-
and ``telnetlib3-server`` for hosting a server.
44+
The CLI utility ``telnetlib3-client`` is provided for connecting to servers and
45+
``telnetlib3-server`` for hosting a server.
5746

58-
Both tools argument ``--shell=my_module.fn_shell`` describing a python
59-
module path to a function of signature ``async def shell(reader, writer)``.
60-
The server also provides ``--pty-exec`` argument to host a stand-alone
61-
program.
47+
Both tools accept the argument ``--shell=my_module.fn_shell`` describing a python module path to a
48+
function of signature ``async def shell(reader, writer)``. The server also provides ``--pty-exec``
49+
argument to host stand-alone programs.
6250

6351
::
6452

65-
# utf8 roguelike server
53+
# telnet to utf8 roguelike server
6654
telnetlib3-client nethack.alt.org
67-
# utf8 bbs
55+
56+
# or bbs,
6857
telnetlib3-client xibalba.l33t.codes 44510
69-
# automatic communication with telnet server
58+
59+
# automatic script communicates with a server
7060
telnetlib3-client --shell bin.client_wargame.shell 1984.ws 666
71-
# run a server with default shell
61+
62+
# run a server bound with the default shell bound to 127.0.0.1 6023
7263
telnetlib3-server
73-
# or custom port and ip and shell
64+
65+
# or custom ip, port and shell
7466
telnetlib3-server 0.0.0.0 1984 --shell=bin.server_wargame.shell
75-
# run an external program with a pseudo-terminal (raw mode is default)
76-
telnetlib3-server --pty-exec /bin/bash -- --login
77-
# or a linemode program, bc (calculator)
78-
telnetlib3-server --pty-exec /bin/bc --line-mode
7967

68+
# host an external program with a pseudo-terminal (raw mode is default)
69+
telnetlib3-server --pty-exec /bin/bash -- --login
8070

81-
There are also fingerprinting CLIs, ``telnetlib3-fingerprint`` and
82-
``telnetlib3-fingerprint-server``
71+
# or host a program in linemode,
72+
telnetlib3-server --pty-exec /bin/bc --line-mode
8373

84-
::
74+
There are also two fingerprinting CLIs, ``telnetlib3-fingerprint`` and
75+
``telnetlib3-fingerprint-server``::
8576

8677
# host a server, wait for clients to connect and fingerprint them,
8778
telnetlib3-fingerprint-server
8879

89-
# report fingerprint of telnet server on 1984.ws
80+
# report fingerprint of the telnet server on 1984.ws
9081
telnetlib3-fingerprint 1984.ws
9182

83+
Encoding
84+
~~~~~~~~
9285

93-
Legacy telnetlib
94-
----------------
86+
The default encoding is the system locale, usually UTF-8, and, without negotiation of BINARY
87+
transmission, all Telnet protocol text *should* be limited to ASCII text, by strict compliance of
88+
Telnet. Further, the encoding used *should* be negotiated by CHARSET.
9589

96-
This library contains an *unadulterated copy* of Python 3.12's telnetlib.py_,
97-
from the standard library before it was removed in Python 3.13.
90+
When these conditions are true, telnetlib3-server and telnetlib3-client allow connections of any
91+
encoding supporting by the python language, and additionally specially ``ATASCII`` and ``PETSCII``
92+
encodings. Any server capable of negotiating CHARSET or LANG through NEW_ENVIRON is also presumed
93+
to support BINARY.
9894

99-
To migrate code, change import statements:
95+
From a February 2026 `census of MUDs <https://muds.modem.xyz>`_ and `BBSs servers
96+
<https://bbs.modem.xyz>`_:
10097

101-
.. code-block:: python
98+
- 2.8% of MUDs support bi-directional CHARSET
99+
- 0.5% of BBSs support bi-directional CHARSET.
100+
- 18.4% of BBSs support BINARY.
101+
- 3.2% of MUDs support BINARY.
102102

103-
# OLD imports:
104-
import telnetlib
103+
For this reason, it is often required to specify the encoding, eg.!
105104

106-
# NEW imports:
107-
import telnetlib3
105+
telnetlib3-client --encoding=cp437 20forbeers.com 1337
108106

109-
``telnetlib3`` did not provide server support, while this library also provides
110-
both client and server support through a similar Blocking API interface.
107+
Raw Mode
108+
~~~~~~~~
111109

112-
See `sync API documentation`_ for details.
110+
Some telnet servers, especially BBS systems or those designed for serial transmission but are
111+
connected to a TCP socket without any telnet negotiation may require "raw" mode argument::
113112

114-
Encoding
115-
--------
113+
telnetlib3-client --raw-mode area52.tk 5200 --encoding=atascii
116114

117-
Often required, ``--encoding`` and ``--force-binary``::
115+
Asyncio Protocol
116+
----------------
118117

119-
telnetlib3-client --encoding=cp437 --force-binary 20forbeers.com 1337
118+
The core protocol and CLI utilities are written using an `Asyncio Interface`_.
120119

121-
The default encoding is the system locale, usually UTF-8, but all Telnet
122-
protocol text *should* be limited to ASCII until BINARY mode is agreed by
123-
compliance of their respective RFCs.
120+
Blocking API
121+
------------
124122

125-
However, many clients and servers that are capable of non-ascii encodings like
126-
UTF-8 or CP437 may not be capable of negotiating about BINARY, NEW_ENVIRON,
127-
or CHARSET to negotiate about it.
123+
A Synchronous interface, modeled after telnetlib.py_ (client) and miniboa_ (server), with various
124+
enhancements in protocol negotiation is also provided. See `sync API documentation`_ for more.
128125

129-
In this case, use ``--force-binary`` and ``--encoding`` when the encoding of
130-
the remote end is known.
126+
Legacy telnetlib
127+
----------------
131128

132-
Go-Ahead (GA)
133-
--------------
129+
This library contains an *unadulterated copy* of Python 3.12's telnetlib.py_,
130+
from the standard library before it was removed in Python 3.13.
134131

135-
When a client does not negotiate Suppress Go-Ahead (SGA), the server sends
136-
``IAC GA`` after output to signal that the client may transmit. This is
137-
correct behavior for MUD clients like Mudlet that expect prompt detection
138-
via GA.
132+
To migrate code, change import statements:
139133

140-
If GA causes unwanted output for your use case, disable it::
134+
.. code-block:: python
141135
142-
telnetlib3-server --never-send-ga
136+
# OLD imports:
137+
import telnetlib
143138
144-
For PTY shells, GA is sent after 500ms of output idle time to avoid
145-
injecting GA in the middle of streaming output.
139+
# NEW imports:
140+
import telnetlib3
141+
142+
This library *also* provides an additional client (and server) API through a similar interface but
143+
offering more advanced negotiation features and options. See `sync API documentation`_ for more.
146144

147145
Quick Example
148146
=============
@@ -156,7 +154,7 @@ A simple telnet server:
156154
157155
async def shell(reader, writer):
158156
writer.write('\r\nWould you like to play a game? ')
159-
inp = await reader.read(1)
157+
inp = await reader.readline()
160158
if inp:
161159
writer.echo(inp)
162160
writer.write('\r\nThey say the only way to win '
@@ -170,6 +168,29 @@ A simple telnet server:
170168
171169
asyncio.run(main())
172170
171+
A client that connects and plays the game:
172+
173+
.. code-block:: python
174+
175+
import asyncio
176+
import telnetlib3
177+
178+
async def shell(reader, writer):
179+
while True:
180+
output = await reader.read(1024)
181+
if not output:
182+
break
183+
if '?' in output:
184+
writer.write('y\r\n')
185+
print(output, end='', flush=True)
186+
print()
187+
188+
async def main():
189+
reader, writer = await telnetlib3.open_connection('localhost', 6023)
190+
await shell(reader, writer)
191+
192+
asyncio.run(main())
193+
173194
More examples are available in the `Guidebook`_ and the `bin/`_ directory of the repository.
174195

175196
Features
@@ -185,7 +206,7 @@ The following RFC specifications are implemented:
185206
* `rfc-857`_, "Telnet Echo Option", May 1983.
186207
* `rfc-858`_, "Telnet Suppress Go Ahead Option", May 1983.
187208
* `rfc-859`_, "Telnet Status Option", May 1983.
188-
* `rfc-860`_, "Telnet Timing mark Option", May 1983.
209+
* `rfc-860`_, "Telnet Timing Mark Option", May 1983.
189210
* `rfc-885`_, "Telnet End of Record Option", Dec 1983.
190211
* `rfc-930`_, "Telnet Terminal Type Option", Jan 1984.
191212
* `rfc-1073`_, "Telnet Window Size Option", Oct 1988.
@@ -231,18 +252,6 @@ The following RFC specifications are implemented:
231252
.. _sync API documentation: https://telnetlib3.readthedocs.io/en/latest/api/sync.html
232253
.. _miniboa: https://github.com/shmup/miniboa
233254
.. _asyncio: https://docs.python.org/3/library/asyncio.html
234-
.. _wait_for(): https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.TelnetConnection.wait_for
235-
.. _get_extra_info(): https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.TelnetConnection.get_extra_info
236-
.. _readline(): https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.TelnetConnection.readline
237-
.. _read_until(): https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.TelnetConnection.read_until
238-
.. _active: https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.active
239-
.. _address: https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.address
240-
.. _terminal_type: https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.terminal_type
241-
.. _columns: https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.columns
242-
.. _rows: https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.rows
243-
.. _idle(): https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.idle
244-
.. _duration(): https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.duration
245-
.. _deactivate(): https://telnetlib3.readthedocs.io/en/latest/api/sync.html#telnetlib3.sync.ServerConnection.deactivate
246255

247256
Further Reading
248257
---------------

bin/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# bin/ package shell callbacks for telnetlib3 examples.
1+
# bin/ package -- shell callbacks for telnetlib3 examples.

bin/server_mud.py

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
44
Usage::
55
6-
$ python bin/server_mud.py
7-
$ telnet localhost 6023
6+
$ telnetlib3-server --line-mode --shell bin.server_mud.shell
7+
$ telnetlib3-client localhost 6023
8+
9+
The default Telnet server negotiates WILL SGA + WILL ECHO, which puts telnetlib3-client into kludge
10+
(raw) mode. MUD clients would prefer that you start the server with ``--line-mode``, to keep
11+
clients in their (default) NVT line mode.
812
"""
913

1014
from __future__ import annotations
@@ -15,12 +19,10 @@
1519
import random
1620
import asyncio
1721
import logging
18-
import argparse
1922
import unicodedata
2023
from typing import Any
2124

2225
# local
23-
import telnetlib3
2426
from telnetlib3.telopt import GMCP, MSDP, MSSP, WILL
2527
from telnetlib3.server_shell import readline2
2628

@@ -101,7 +103,7 @@
101103
}
102104

103105

104-
class Weapon: # pylint: disable=too-few-public-methods
106+
class Weapon:
105107
"""A weapon that can be held or placed in a room."""
106108

107109
def __init__(self, name: str, damage: tuple[int, int], start_room: str) -> None:
@@ -117,7 +119,7 @@ def damage_display(self) -> str:
117119
return f"{self.damage[0]}-{self.damage[1]}"
118120

119121

120-
class Player: # pylint: disable=too-few-public-methods
122+
class Player:
121123
"""A connected player."""
122124

123125
def __init__(self, name: str = "Adventurer") -> None:
@@ -272,9 +274,7 @@ def on_gmcp(writer: Any, package: str, data: Any) -> None:
272274
writer.write(f"[DEBUG GMCP] {package}: {json.dumps(data)}\r\n")
273275

274276

275-
def get_msdp_var( # pylint: disable=too-many-return-statements
276-
player: Player, var: str
277-
) -> dict[str, Any] | None:
277+
def get_msdp_var(player: Player, var: str) -> dict[str, Any] | None:
278278
"""Return MSDP value dict for *var*, or ``None`` if unknown."""
279279
if var == "CHARACTER_NAME":
280280
return {"CHARACTER_NAME": player.name}
@@ -834,35 +834,3 @@ async def shell(reader: Any, writer: Any) -> None:
834834
broadcast_room(None, player.room, f"{player.name} has left.")
835835
update_room_all(player.room)
836836
writer.close()
837-
838-
839-
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
840-
"""Parse command-line arguments."""
841-
ap = argparse.ArgumentParser(description="Mini-MUD demo server with telnetlib3")
842-
ap.add_argument("--host", default="127.0.0.1", help="bind address (default: 127.0.0.1)")
843-
ap.add_argument("--port", type=int, default=6023, help="bind port (default: 6023)")
844-
ap.add_argument("--log-level", default="INFO", help="log level (default: INFO)")
845-
return ap.parse_args(argv)
846-
847-
848-
async def main(argv: list[str] | None = None) -> None:
849-
"""Start the MUD server."""
850-
args = parse_args(argv)
851-
logging.basicConfig(
852-
level=getattr(logging, args.log_level.upper()),
853-
format="%(asctime)s %(message)s",
854-
datefmt="%H:%M:%S",
855-
)
856-
server = await telnetlib3.create_server(host=args.host, port=args.port, shell=shell)
857-
log.info("%s running on %s:%d", SERVER_NAME, args.host, args.port)
858-
print(
859-
f"{SERVER_NAME} running on"
860-
f" {args.host}:{args.port}\n"
861-
f"Connect with: telnet {args.host} {args.port}\n"
862-
"Press Ctrl+C to stop"
863-
)
864-
await server.wait_closed()
865-
866-
867-
if __name__ == "__main__":
868-
asyncio.run(main())

docs/api/session_context.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
session_context
2+
---------------
3+
4+
.. automodule:: telnetlib3._session_context
5+
:members:

0 commit comments

Comments
 (0)