From fc8ee4a89d0223914fb9ccf6c2e784986aa25269 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 16 Feb 2026 12:43:33 -0500 Subject: [PATCH] bugfix/improve linemode/raw switching live (#123) - bugfix that we accidentally "enabled" MUD protocols in telnetlib3-client in last release which causes SGML and probably other stuff we don't want or can't use -- these are now only enabled for fingerprinting client. - mainly, for supporting telnetlib3-client to connect to muds, and temporarily enabling and disabling echo for password prompts, this was not previously supported, we just "always did raw mode", but now we honor the server's wishes. guidebook entry about it. - pexpect is re-enabled an used again to great effect of coverage, - bugfix just one small item was missing to correctly track coverage w/pty - all tests changed to exclude common pylint disables globally and remove them individually --- bin/moderate_fingerprints.py | 1 - docs/guidebook.rst | 25 +- docs/history.rst | 10 + telnetlib3/accessories.py | 6 +- telnetlib3/client.py | 93 +- telnetlib3/client_base.py | 12 +- telnetlib3/client_shell.py | 525 ++++++++---- telnetlib3/color_filter.py | 126 ++- telnetlib3/encodings/__init__.py | 34 +- telnetlib3/encodings/atarist.py | 535 ++++++------ telnetlib3/encodings/atascii.py | 546 ++++++------ telnetlib3/encodings/petscii.py | 542 ++++++------ telnetlib3/fingerprinting.py | 5 - telnetlib3/fingerprinting_display.py | 5 - telnetlib3/server.py | 21 +- telnetlib3/server_fingerprinting.py | 92 +- telnetlib3/server_pty_shell.py | 6 - telnetlib3/stream_writer.py | 34 +- telnetlib3/sync.py | 1 - telnetlib3/telnetlib.py | 1 - telnetlib3/tests/accessories.py | 4 - telnetlib3/tests/conftest.py | 1 - telnetlib3/tests/pty_helper.py | 1 - telnetlib3/tests/test_accessories_extra.py | 2 +- telnetlib3/tests/test_atascii_codec.py | 254 +++--- telnetlib3/tests/test_charset.py | 2 +- telnetlib3/tests/test_client_shell.py | 800 ++++++++++++++---- telnetlib3/tests/test_client_unit.py | 6 +- telnetlib3/tests/test_color_filter.py | 64 +- telnetlib3/tests/test_core.py | 4 +- telnetlib3/tests/test_encoding.py | 2 +- telnetlib3/tests/test_environ.py | 2 +- telnetlib3/tests/test_fingerprinting.py | 5 +- telnetlib3/tests/test_guard_integration.py | 14 +- telnetlib3/tests/test_linemode.py | 2 +- telnetlib3/tests/test_mud_negotiation.py | 65 +- telnetlib3/tests/test_naws.py | 2 +- telnetlib3/tests/test_petscii_codec.py | 75 +- telnetlib3/tests/test_pty_shell.py | 7 +- telnetlib3/tests/test_reader.py | 2 +- telnetlib3/tests/test_server_api.py | 1 - .../tests/test_server_fingerprinting.py | 276 ++---- telnetlib3/tests/test_server_shell_unit.py | 7 +- telnetlib3/tests/test_shell.py | 2 +- telnetlib3/tests/test_status_logger.py | 1 - telnetlib3/tests/test_stream_writer_full.py | 1 - telnetlib3/tests/test_sync.py | 2 - telnetlib3/tests/test_telnetlib.py | 4 - telnetlib3/tests/test_timeout.py | 2 +- telnetlib3/tests/test_tspeed.py | 2 +- telnetlib3/tests/test_ttype.py | 2 +- telnetlib3/tests/test_uvloop_integration.py | 3 +- telnetlib3/tests/test_writer.py | 2 +- telnetlib3/tests/test_xdisploc.py | 2 +- tox.ini | 4 + 55 files changed, 2420 insertions(+), 1825 deletions(-) diff --git a/bin/moderate_fingerprints.py b/bin/moderate_fingerprints.py index 5abe01b2..0af97bf8 100755 --- a/bin/moderate_fingerprints.py +++ b/bin/moderate_fingerprints.py @@ -15,7 +15,6 @@ from pathlib import Path try: - # 3rd party from wcwidth import iter_sequences, strip_sequences _HAS_WCWIDTH = True diff --git a/docs/guidebook.rst b/docs/guidebook.rst index 720172e9..61094afe 100644 --- a/docs/guidebook.rst +++ b/docs/guidebook.rst @@ -277,25 +277,24 @@ telnet implementations, always use ``\r\n`` with ``write()``. Raw Mode and Line Mode ~~~~~~~~~~~~~~~~~~~~~~ -``telnetlib3-client`` defaults to **raw terminal mode** -- the local -terminal is set to raw (no line buffering, no local echo, no signal -processing), and each keystroke is sent to the server immediately. This -is the correct mode for most BBS and MUD servers that handle their own -echo and line editing. +By default ``telnetlib3-client`` matches the terminal's mode by the +server's stated telnet negotiation. It starts in line mode (local echo, +line buffering) and switches dynamically depending on server: -Use ``--line-mode`` to switch to line-buffered input with local echo, -which is appropriate for simple command-line services that expect the -client to perform local line editing:: +- Nothing: line mode with local echo +- ``WILL ECHO`` + ``WILL SGA``: kludge mode (raw, no local echo) +- ``WILL ECHO``: raw mode, server echoes +- ``WILL SGA``: character-at-a-time with local echo - # Default: raw mode (correct for most servers) - telnetlib3-client bbs.example.com +Use ``--raw-mode`` to force raw mode (no line buffering, no local echo), +which is needed for some legacy BBS systems that don't negotiate ``WILL +ECHO``. This is set true when ``--encoding=petscii`` or ``atascii``. - # Line mode: local echo and line buffering - telnetlib3-client --line-mode simple-service.example.com +Conversely, Use ``--line-mode`` to force line-buffered input with local echo. Similarly, ``telnetlib3-server --pty-exec`` defaults to raw PTY mode (disabling PTY echo), which is correct for programs that handle their own -terminal I/O (curses, blessed, etc.). Use ``--line-mode`` for programs +terminal I/O (bash, curses, etc.). Use ``--line-mode`` for programs that expect cooked/canonical PTY mode:: # Default: raw PTY (correct for curses programs) diff --git a/docs/history.rst b/docs/history.rst index 13507a76..14914621 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,5 +1,15 @@ History ======= +2.6.0 + * change: ``telnetlib3-client`` now sets terminal mode to the server's + preference via ``WILL ECHO`` and ``WILL SGA`` negotiation. Use + ``--raw-mode`` to restore legacy raw mode for servers that don't negotiate. + The Python API (``open_connection``, ``create_server``) is unchanged. + * change: ``telnetlib3-client`` declines MUD protocol options (GMCP, MSDP, + MSSP, MSP, MXP, ZMP, AARDWOLF, ATCP) by default. Use ``--always-do`` or + ``--always-will`` to opt in. + * bugfix: log output "staircase text" in raw terminal mode. + 2.5.0 * change: ``telnetlib3-client`` now defaults to raw terminal mode (no line buffering, no local echo), which is correct for most servers. Use ``--line-mode`` to restore line-buffered diff --git a/telnetlib3/accessories.py b/telnetlib3/accessories.py index 3818112e..dd29cde1 100644 --- a/telnetlib3/accessories.py +++ b/telnetlib3/accessories.py @@ -14,7 +14,6 @@ logging.addLevelName(TRACE, "TRACE") if TYPE_CHECKING: # pragma: no cover - # local from .stream_reader import TelnetReader, TelnetReaderUnicode __all__ = ( @@ -142,6 +141,11 @@ def make_logger( if logfile: _cfg["filename"] = logfile logging.basicConfig(**_cfg) + for handler in logging.getLogger().handlers: + if isinstance(handler, logging.StreamHandler) and not isinstance( + handler, logging.FileHandler + ): + handler.terminator = "\r\n" logging.getLogger().setLevel(lvl) logging.getLogger(name).setLevel(lvl) return logging.getLogger(name) diff --git a/telnetlib3/client.py b/telnetlib3/client.py index aa4e0703..8cd94634 100755 --- a/telnetlib3/client.py +++ b/telnetlib3/client.py @@ -98,7 +98,6 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None: and character set negotiation. """ # pylint: disable=import-outside-toplevel - # local from telnetlib3.telopt import NAWS, TTYPE, TSPEED, CHARSET, XDISPLOC, NEW_ENVIRON super().connection_made(transport) @@ -198,17 +197,17 @@ def _normalize_charset_name(name: str) -> str: :param name: Raw charset name from the server. :returns: Normalized name suitable for :func:`codecs.lookup`. """ - # std imports import re # pylint: disable=import-outside-toplevel - base = name.strip().replace(' ', '-') + + base = name.strip().replace(" ", "-") # Strip leading zeros from numeric segments: iso-8859-02 → iso-8859-2 - no_leading_zeros = re.sub(r'-0+(\d)', r'-\1', base) + no_leading_zeros = re.sub(r"-0+(\d)", r"-\1", base) # All hyphens removed: cp-1250 → cp1250 - no_hyphens = base.replace('-', '') + no_hyphens = base.replace("-", "") # Keep first hyphen-segment, collapse the rest: iso-8859-2 stays - parts = no_leading_zeros.split('-') + parts = no_leading_zeros.split("-") if len(parts) > 2: - partial = parts[0] + '-' + ''.join(parts[1:]) + partial = parts[0] + "-" + "".join(parts[1:]) else: partial = no_leading_zeros for candidate in (base, no_leading_zeros, no_hyphens, partial): @@ -256,9 +255,7 @@ def send_charset(self, offered: List[str]) -> str: for offer in offered: try: - canon = codecs.lookup( - self._normalize_charset_name(offer) - ).name + canon = codecs.lookup(self._normalize_charset_name(offer)).name # Record first viable encoding if first_viable is None: @@ -382,7 +379,6 @@ def send_env(self, keys: Sequence[str]) -> Dict[str, Any]: @staticmethod def _winsize() -> Tuple[int, int]: try: - # std imports import fcntl # pylint: disable=import-outside-toplevel import termios # pylint: disable=import-outside-toplevel @@ -575,7 +571,6 @@ def _patched_connection_made(transport: asyncio.BaseTransport) -> None: colormatch: str = args["colormatch"] shell_callback = args["shell"] if colormatch.lower() != "none": - # local from .color_filter import ( # pylint: disable=import-outside-toplevel PALETTES, ColorConfig, @@ -625,9 +620,8 @@ async def _color_shell( shell_callback = _color_shell # Wrap shell to inject raw_mode flag and input translation for retro encodings - raw_mode: bool = args.get("raw_mode", False) - if raw_mode: - # local + raw_mode_val: Optional[bool] = args.get("raw_mode", False) + if raw_mode_val is not False: from .client_shell import ( # pylint: disable=import-outside-toplevel _INPUT_XLAT, _INPUT_SEQ_XLAT, @@ -635,11 +629,16 @@ async def _color_shell( ) enc_key = (args.get("encoding", "") or "").lower() - byte_xlat = _INPUT_XLAT.get(enc_key, {}) - seq_xlat = _INPUT_SEQ_XLAT.get(enc_key, {}) + byte_xlat = dict(_INPUT_XLAT.get(enc_key, {})) + if args.get("ascii_eol"): + # --ascii-eol: don't translate CR/LF to encoding-native EOL + byte_xlat.pop(0x0D, None) + byte_xlat.pop(0x0A, None) + seq_xlat = {} if args.get("ansi_keys") else _INPUT_SEQ_XLAT.get(enc_key, {}) input_filter: Optional[InputFilter] = ( InputFilter(seq_xlat, byte_xlat) if (seq_xlat or byte_xlat) else None ) + ascii_eol: bool = args.get("ascii_eol", False) _inner_shell = shell_callback async def _raw_shell( @@ -647,7 +646,10 @@ async def _raw_shell( writer_arg: Union[TelnetWriter, TelnetWriterUnicode], ) -> None: # pylint: disable-next=protected-access - writer_arg._raw_mode = True # type: ignore[union-attr] + writer_arg._raw_mode = raw_mode_val # type: ignore[union-attr] + if ascii_eol: + # pylint: disable-next=protected-access + writer_arg._ascii_eol = True # type: ignore[union-attr] if input_filter is not None: # pylint: disable-next=protected-access writer_arg._input_filter = input_filter # type: ignore[union-attr] @@ -703,13 +705,21 @@ def _get_argument_parser() -> argparse.ArgumentParser: ) parser.add_argument("--force-binary", action="store_true", help="force encoding", default=True) - parser.add_argument( + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument( + "--raw-mode", + action="store_true", + default=False, + help="force raw terminal mode (no line buffering, no local echo). " + "Correct for BBS and retro systems. Default: auto-detect from " + "server negotiation.", + ) + mode_group.add_argument( "--line-mode", action="store_true", default=False, - help="use line-buffered input with local echo instead of raw terminal " - "mode. By default the client uses raw mode (no line buffering, no " - "local echo) which is correct for most BBS and MUD servers.", + help="force line-buffered input with local echo. Appropriate for " + "simple command-line services.", ) parser.add_argument( "--connect-minwait", default=0, type=float, help="shell delay for negotiation" @@ -779,6 +789,22 @@ def _get_argument_parser() -> argparse.ArgumentParser: default=False, help="swap foreground/background for light-background terminals", ) + parser.add_argument( + "--ascii-eol", + action="store_true", + default=False, + help="use ASCII CR/LF for line endings instead of encoding-native " + "EOL (e.g. ATASCII 0x9B). Use for BBSes that display retro " + "graphics but use standard CR/LF for line breaks.", + ) + parser.add_argument( + "--ansi-keys", + action="store_true", + default=False, + help="transmit raw ANSI escape sequences for arrow and function " + "keys instead of encoding-specific control codes. Use for " + "BBSes that expect ANSI cursor sequences.", + ) return parser @@ -790,7 +816,6 @@ def _parse_option_arg(value: str) -> bytes: :returns: Single-byte option value. :raises ValueError: When *value* is not a known name or valid integer. """ - # local from .telopt import option_from_name # pylint: disable=import-outside-toplevel try: @@ -815,12 +840,17 @@ def _parse_background_color(value: str) -> Tuple[int, int, int]: def _transform_args(args: argparse.Namespace) -> Dict[str, Any]: # Auto-enable force_binary for retro BBS encodings that use high-bit bytes. - # local from .encodings import FORCE_BINARY_ENCODINGS # pylint: disable=import-outside-toplevel force_binary = args.force_binary - raw_mode = not args.line_mode - if args.encoding.lower().replace('-', '_') in FORCE_BINARY_ENCODINGS: + # Three-state: True (forced raw), False (forced line), None (auto-detect) + if args.raw_mode: + raw_mode: Optional[bool] = True + elif args.line_mode: + raw_mode = False + else: + raw_mode = None + if args.encoding.lower().replace("-", "_") in FORCE_BINARY_ENCODINGS: force_binary = True raw_mode = True @@ -847,6 +877,8 @@ def _transform_args(args: argparse.Namespace) -> Dict[str, Any]: "background_color": _parse_background_color(args.background_color), "reverse_video": args.reverse_video, "raw_mode": raw_mode, + "ascii_eol": args.ascii_eol, + "ansi_keys": args.ansi_keys, } @@ -957,7 +989,6 @@ async def run_fingerprint_client() -> None: :func:`~telnetlib3.server_fingerprinting.fingerprinting_client_shell` via :func:`functools.partial`, and runs the connection. """ - # local from . import fingerprinting # pylint: disable=import-outside-toplevel from . import server_fingerprinting # pylint: disable=import-outside-toplevel @@ -1021,8 +1052,12 @@ def patched_connection_made(transport: asyncio.BaseTransport) -> None: client.writer.environ_encoding = environ_encoding # pylint: disable-next=protected-access client.writer._encoding_explicit = environ_encoding != "ascii" - client.writer.always_will = fp_always_will - client.writer.always_do = fp_always_do + # pylint: disable-next=import-outside-toplevel + from .fingerprinting import EXTENDED_OPTIONS + + mud_opts = {opt for opt, _, _ in EXTENDED_OPTIONS} + client.writer.always_will = fp_always_will | mud_opts + client.writer.always_do = fp_always_do | mud_opts def patched_send_env(keys: Sequence[str]) -> Dict[str, Any]: result = orig_send_env(keys) diff --git a/telnetlib3/client_base.py b/telnetlib3/client_base.py index f70a02d4..1cae6bd5 100644 --- a/telnetlib3/client_base.py +++ b/telnetlib3/client_base.py @@ -200,8 +200,7 @@ def begin_shell(self, future: asyncio.Future[None]) -> None: fut.add_done_callback( lambda fut_obj: ( self.waiter_closed.set_result(weakref.proxy(self)) - if self.waiter_closed is not None - and not self.waiter_closed.done() + if self.waiter_closed is not None and not self.waiter_closed.done() else None ) ) @@ -249,18 +248,18 @@ def _detect_syncterm_font(self, data: bytes) -> None: """ if self.writer is None: return - # local from .server_fingerprinting import ( # pylint: disable=import-outside-toplevel _SYNCTERM_BINARY_ENCODINGS, detect_syncterm_font, ) + encoding = detect_syncterm_font(data) if encoding is not None: self.log.debug("SyncTERM font switch: %s", encoding) - if getattr(self.writer, '_encoding_explicit', False): + if getattr(self.writer, "_encoding_explicit", False): self.log.debug( - "ignoring font switch, explicit encoding: %s", - self.writer.environ_encoding) + "ignoring font switch, explicit encoding: %s", self.writer.environ_encoding + ) else: self.writer.environ_encoding = encoding if encoding in _SYNCTERM_BINARY_ENCODINGS: @@ -345,7 +344,6 @@ def check_negotiation(self, final: bool = False) -> bool: combined when derived. """ # pylint: disable=import-outside-toplevel - # local from .telopt import TTYPE, CHARSET, NEW_ENVIRON # First check if there are any pending options diff --git a/telnetlib3/client_shell.py b/telnetlib3/client_shell.py index 2ee795a6..6527d2af 100644 --- a/telnetlib3/client_shell.py +++ b/telnetlib3/client_shell.py @@ -10,11 +10,18 @@ # local from . import accessories +from .accessories import TRACE from .stream_reader import TelnetReader, TelnetReaderUnicode from .stream_writer import TelnetWriter, TelnetWriterUnicode __all__ = ("InputFilter", "telnet_client_shell") +# ATASCII graphics characters that map to byte 0x0D and 0x0A respectively. +# When --ascii-eol is active, these are replaced with \r and \n before +# terminal display so that BBSes using ASCII CR/LF render correctly. +_ATASCII_CR_CHAR = "\U0001fb82" # UPPER ONE QUARTER BLOCK (from byte 0x0D) +_ATASCII_LF_CHAR = "\u25e3" # BLACK LOWER LEFT TRIANGLE (from byte 0x0A) + # Input byte translation tables for retro encodings in raw mode. # Maps terminal keyboard bytes to the raw bytes the BBS expects. # Applied BEFORE decoding/encoding, bypassing the codec entirely for @@ -39,34 +46,34 @@ # DEFAULT_SEQUENCE_MIXIN but kept minimal for the sequences that matter. _INPUT_SEQ_XLAT: Dict[str, Dict[bytes, bytes]] = { "atascii": { - b"\x1b[A": b"\x1c", # cursor up (CSI) - b"\x1b[B": b"\x1d", # cursor down - b"\x1b[C": b"\x1f", # cursor right - b"\x1b[D": b"\x1e", # cursor left - b"\x1bOA": b"\x1c", # cursor up (SS3 / application mode) - b"\x1bOB": b"\x1d", # cursor down - b"\x1bOC": b"\x1f", # cursor right - b"\x1bOD": b"\x1e", # cursor left - b"\x1b[3~": b"\x7e", # delete → ATASCII backspace - b"\t": b"\x7f", # tab → ATASCII tab + b"\x1b[A": b"\x1c", # cursor up (CSI) + b"\x1b[B": b"\x1d", # cursor down + b"\x1b[C": b"\x1f", # cursor right + b"\x1b[D": b"\x1e", # cursor left + b"\x1bOA": b"\x1c", # cursor up (SS3 / application mode) + b"\x1bOB": b"\x1d", # cursor down + b"\x1bOC": b"\x1f", # cursor right + b"\x1bOD": b"\x1e", # cursor left + b"\x1b[3~": b"\x7e", # delete → ATASCII backspace + b"\t": b"\x7f", # tab → ATASCII tab }, "petscii": { - b"\x1b[A": b"\x91", # cursor up (CSI) - b"\x1b[B": b"\x11", # cursor down - b"\x1b[C": b"\x1d", # cursor right - b"\x1b[D": b"\x9d", # cursor left - b"\x1bOA": b"\x91", # cursor up (SS3 / application mode) - b"\x1bOB": b"\x11", # cursor down - b"\x1bOC": b"\x1d", # cursor right - b"\x1bOD": b"\x9d", # cursor left - b"\x1b[3~": b"\x14", # delete → PETSCII DEL - b"\x1b[H": b"\x13", # home → PETSCII HOME - b"\x1b[2~": b"\x94", # insert → PETSCII INSERT + b"\x1b[A": b"\x91", # cursor up (CSI) + b"\x1b[B": b"\x11", # cursor down + b"\x1b[C": b"\x1d", # cursor right + b"\x1b[D": b"\x9d", # cursor left + b"\x1bOA": b"\x91", # cursor up (SS3 / application mode) + b"\x1bOB": b"\x11", # cursor down + b"\x1bOC": b"\x1d", # cursor right + b"\x1bOD": b"\x9d", # cursor left + b"\x1b[3~": b"\x14", # delete → PETSCII DEL + b"\x1b[H": b"\x13", # home → PETSCII HOME + b"\x1b[2~": b"\x94", # insert → PETSCII INSERT }, } -class InputFilter: # pylint: disable=too-few-public-methods +class InputFilter: """ Translate terminal escape sequences and single bytes to retro encoding bytes. @@ -75,15 +82,22 @@ class InputFilter: # pylint: disable=too-few-public-methods buffering inspired by blessed's ``get_leading_prefixes`` to handle sequences split across reads. + When a partial match is buffered (e.g. a bare ESC), :attr:`has_pending` + becomes ``True``. The caller should start an ``esc_delay`` timer and + call :meth:`flush` if no further input arrives before the timer fires. + :param seq_xlat: Multi-byte escape sequence → replacement bytes. :param byte_xlat: Single input byte → replacement byte. + :param esc_delay: Seconds to wait before flushing a buffered prefix + (default 0.35, matching blessed's ``DEFAULT_ESCDELAY``). """ def __init__( - self, seq_xlat: Dict[bytes, bytes], byte_xlat: Dict[int, int] + self, seq_xlat: Dict[bytes, bytes], byte_xlat: Dict[int, int], esc_delay: float = 0.35 ) -> None: """Initialize input filter with sequence and byte translation tables.""" self._byte_xlat = byte_xlat + self.esc_delay = esc_delay # Sort sequences longest-first so \x1b[3~ matches before \x1b[3 self._seq_sorted: Tuple[Tuple[bytes, bytes], ...] = tuple( sorted(seq_xlat.items(), key=lambda kv: len(kv[0]), reverse=True) @@ -94,6 +108,27 @@ def __init__( ) self._buf = b"" + @property + def has_pending(self) -> bool: + """Return ``True`` when the internal buffer holds a partial sequence.""" + return bool(self._buf) + + def flush(self) -> bytes: + """ + Flush buffered bytes, applying single-byte translation. + + Called when the ``esc_delay`` timer fires without new input, + meaning the buffered prefix is not a real escape sequence. + + :returns: Translated bytes from the buffer (may be empty). + """ + result = bytearray() + while self._buf: + b = self._buf[0] + self._buf = self._buf[1:] + result.append(self._byte_xlat.get(b, b)) + return bytes(result) + def feed(self, data: bytes) -> bytes: """ Process input bytes, returning raw bytes to send to the remote host. @@ -111,9 +146,9 @@ def feed(self, data: bytes) -> bytes: # Try multi-byte sequence match at current position matched = False for seq, repl in self._seq_sorted: - if self._buf[:len(seq)] == seq: + if self._buf[: len(seq)] == seq: result.extend(repl) - self._buf = self._buf[len(seq):] + self._buf = self._buf[len(seq) :] matched = True break if matched: @@ -138,7 +173,6 @@ async def telnet_client_shell( raise NotImplementedError("win32 not yet supported as telnet client. Please contribute!") else: - # std imports import os import signal import termios @@ -160,6 +194,54 @@ def __init__(self, telnet_writer: Union[TelnetWriter, TelnetWriterUnicode]) -> N self._fileno = sys.stdin.fileno() self._istty = os.path.sameopenfile(0, 1) self._save_mode: Optional[Terminal.ModeDef] = None + self.software_echo = False + self._remove_winch = False + self._winch_handle: Optional[asyncio.TimerHandle] = None + + def setup_winch(self) -> None: + """Register SIGWINCH handler to send NAWS on terminal resize.""" + if not self._istty or not hasattr(signal, "SIGWINCH"): + return + try: + loop = asyncio.get_event_loop() + writer = self.telnet_writer + + def _send_naws() -> None: + from .telopt import NAWS # pylint: disable=import-outside-toplevel + + try: + if writer.local_option.enabled(NAWS) and not writer.is_closing(): + writer._send_naws() # pylint: disable=protected-access + except Exception: # pylint: disable=broad-exception-caught + pass + + def _on_winch() -> None: + if self._winch_handle is not None and not self._winch_handle.cancelled(): + try: + self._winch_handle.cancel() + except Exception: # pylint: disable=broad-exception-caught + pass + self._winch_handle = loop.call_later(0.05, _send_naws) + + loop.add_signal_handler(signal.SIGWINCH, _on_winch) + self._remove_winch = True + except Exception: # pylint: disable=broad-exception-caught + self._remove_winch = False + + def cleanup_winch(self) -> None: + """Remove SIGWINCH handler and cancel pending timer.""" + if self._istty and self._remove_winch: + try: + asyncio.get_event_loop().remove_signal_handler(signal.SIGWINCH) + except Exception: # pylint: disable=broad-exception-caught + pass + self._remove_winch = False + if self._winch_handle is not None: + try: + self._winch_handle.cancel() + except Exception: # pylint: disable=broad-exception-caught + pass + self._winch_handle = None def __enter__(self) -> "Terminal": self._save_mode = self.get_mode() @@ -169,6 +251,7 @@ def __enter__(self) -> "Terminal": return self def __exit__(self, *_: Any) -> None: + self.cleanup_winch() if self._istty: assert self._save_mode is not None termios.tcsetattr(self._fileno, termios.TCSAFLUSH, list(self._save_mode)) @@ -183,53 +266,28 @@ def set_mode(self, mode: "Terminal.ModeDef") -> None: """Set terminal mode attributes.""" termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, list(mode)) - def determine_mode(self, mode: "Terminal.ModeDef") -> "Terminal.ModeDef": - """Return copy of 'mode' with changes suggested for telnet connection.""" - raw_mode = getattr(self.telnet_writer, '_raw_mode', False) - if not self.telnet_writer.will_echo and not raw_mode: - self.telnet_writer.log.debug("local echo, linemode") - return mode - if raw_mode and not self.telnet_writer.will_echo: - self.telnet_writer.log.debug("raw mode forced, no server echo") - else: - self.telnet_writer.log.debug("server echo, kludge mode") + def _make_raw( + self, mode: "Terminal.ModeDef", suppress_echo: bool = True + ) -> "Terminal.ModeDef": + """ + Return copy of *mode* with raw terminal attributes set. - # "Raw mode", see tty.py function setraw. This allows sending - # of ^J, ^C, ^S, ^\, and others, which might otherwise - # interrupt with signals or map to another character. We also - # trust the remote server to manage CR/LF without mapping. - # + :param suppress_echo: When True, disable local ECHO (server echoes). When False, keep + local ECHO enabled (character-at-a-time with local echo, e.g. SGA without ECHO). + """ iflag = mode.iflag & ~( - termios.BRKINT - | termios.ICRNL # Do not send INTR signal on break - | termios.INPCK # Do not map CR to NL on input - | termios.ISTRIP # Disable input parity checking - | termios.IXON # Do not strip input characters to 7 bits - ) # Disable START/STOP output control - - # Disable parity generation and detection, - # Select eight bits per byte character size. + termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON + ) cflag = mode.cflag & ~(termios.CSIZE | termios.PARENB) cflag = cflag | termios.CS8 - - # Disable canonical input (^H and ^C processing), - # disable any other special control characters, - # disable checking for INTR, QUIT, and SUSP input. - lflag = mode.lflag & ~(termios.ICANON | termios.IEXTEN | termios.ISIG | termios.ECHO) - - # Disable post-output processing, - # such as mapping LF('\n') to CRLF('\r\n') in output. + lflag_mask = termios.ICANON | termios.IEXTEN | termios.ISIG + if suppress_echo: + lflag_mask |= termios.ECHO + lflag = mode.lflag & ~lflag_mask oflag = mode.oflag & ~(termios.OPOST | termios.ONLCR) - - # "A pending read is not satisfied until MIN bytes are received - # (i.e., the pending read until MIN bytes are received), or a - # signal is received. A program that uses this case to read - # record-based terminal I/O may block indefinitely in the read - # operation." cc = list(mode.cc) cc[termios.VMIN] = 1 cc[termios.VTIME] = 0 - return self.ModeDef( iflag=iflag, oflag=oflag, @@ -240,6 +298,87 @@ def determine_mode(self, mode: "Terminal.ModeDef") -> "Terminal.ModeDef": cc=cc, ) + def _server_will_sga(self) -> bool: + """Whether server has negotiated WILL SGA.""" + from .telopt import SGA # pylint: disable=import-outside-toplevel + + return bool(self.telnet_writer.client and self.telnet_writer.remote_option.enabled(SGA)) + + def check_auto_mode( + self, switched_to_raw: bool, last_will_echo: bool + ) -> "tuple[bool, bool, bool] | None": + """ + Check if auto-mode switching is needed. + + :param switched_to_raw: Whether terminal has already switched to raw mode. + :param last_will_echo: Previous value of server's WILL ECHO state. + :returns: ``(switched_to_raw, last_will_echo, local_echo)`` tuple + if mode changed, or ``None`` if no change needed. + """ + if not self._istty: + return None + _wecho = self.telnet_writer.will_echo + _wsga = self._server_will_sga() + _should_switch = not switched_to_raw and (_wecho or _wsga) + _echo_changed = switched_to_raw and _wecho != last_will_echo + if not (_should_switch or _echo_changed): + return None + assert self._save_mode is not None + self.set_mode(self._make_raw(self._save_mode, suppress_echo=True)) + self.telnet_writer.log.debug( + "auto: %s (server %s ECHO)", + ( + "switching to raw mode" + if _should_switch + else ("disabling" if _wecho else "enabling") + " software echo" + ), + "WILL" if _wecho else "WONT", + ) + return (True if _should_switch else switched_to_raw, _wecho, not _wecho) + + def determine_mode(self, mode: "Terminal.ModeDef") -> "Terminal.ModeDef": + """ + Return copy of 'mode' with changes suggested for telnet connection. + + Auto mode (``_raw_mode is None``): follows the server's negotiation. + + ================= ======== ========== ================================ + Server negotiates ICANON ECHO Behavior + ================= ======== ========== ================================ + Nothing on on Line mode, local echo + WILL SGA only **off** on Character-at-a-time, local echo + WILL ECHO only **off** **off** Raw mode, server echoes (rare) + WILL SGA + ECHO **off** **off** Full kludge mode (most common) + ================= ======== ========== ================================ + """ + raw_mode = getattr(self.telnet_writer, "_raw_mode", False) + will_echo = self.telnet_writer.will_echo + will_sga = self._server_will_sga() + # Auto mode (None): follow server negotiation + if raw_mode is None: + if will_echo and will_sga: + self.telnet_writer.log.debug("auto: server echo + SGA, kludge mode") + return self._make_raw(mode) + if will_echo: + self.telnet_writer.log.debug("auto: server echo, raw mode") + return self._make_raw(mode) + if will_sga: + self.telnet_writer.log.debug("auto: SGA without echo, character-at-a-time") + self.software_echo = True + return self._make_raw(mode, suppress_echo=True) + self.telnet_writer.log.debug("auto: no server echo yet, line mode") + return mode + # Explicit line mode (False) + if not raw_mode: + self.telnet_writer.log.debug("local echo, linemode") + return mode + # Explicit raw mode (True) + if not will_echo: + self.telnet_writer.log.debug("raw mode forced, no server echo") + else: + self.telnet_writer.log.debug("server echo, kludge mode") + return self._make_raw(mode) + async def make_stdio(self) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]: """Return (reader, writer) pair for sys.stdin, sys.stdout.""" reader = asyncio.StreamReader() @@ -268,7 +407,81 @@ async def make_stdio(self) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]: return reader, writer - # pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-nested-blocks + def _transform_output( + out: str, writer: Union[TelnetWriter, TelnetWriterUnicode], in_raw_mode: bool + ) -> str: + r""" + Apply color filter, ASCII EOL substitution, and CRLF normalization. + + :param out: Server output text to transform. + :param writer: Telnet writer (checked for ``_color_filter`` and ``_ascii_eol``). + :param in_raw_mode: When ``True``, normalize line endings to ``\r\n``. + :returns: Transformed output string. + """ + _cf = getattr(writer, "_color_filter", None) + if _cf is not None: + out = _cf.filter(out) + if getattr(writer, "_ascii_eol", False): + out = out.replace(_ATASCII_CR_CHAR, "\r").replace(_ATASCII_LF_CHAR, "\n") + if in_raw_mode: + out = out.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "\r\n") + else: + # Cooked mode: PTY ONLCR converts \n → \r\n, so strip \r before \n + # to avoid doubling (\r\n → \r\r\n). + out = out.replace("\r\n", "\n") + return out + + def _send_stdin( + inp: bytes, + telnet_writer: Union[TelnetWriter, TelnetWriterUnicode], + stdout: asyncio.StreamWriter, + local_echo: bool, + ) -> "tuple[Optional[asyncio.Task[None]], bool]": + """ + Send stdin input to server and optionally echo locally. + + :param inp: Raw bytes from terminal stdin. + :param telnet_writer: Telnet writer for sending to server. + :param stdout: Local stdout writer for software echo. + :param local_echo: When ``True``, echo input bytes to stdout. + :returns: ``(esc_timer_task_or_None, has_pending)`` tuple. + """ + _inf = getattr(telnet_writer, "_input_filter", None) + pending = False + new_timer: Optional[asyncio.Task[None]] = None + if _inf is not None: + translated = _inf.feed(inp) + if translated: + telnet_writer._write(translated) # pylint: disable=protected-access + if _inf.has_pending: + pending = True + new_timer = asyncio.ensure_future(asyncio.sleep(_inf.esc_delay)) + else: + telnet_writer._write(inp) # pylint: disable=protected-access + if local_echo: + _echo_buf = bytearray() + for _b in inp: + if _b in (0x7F, 0x08): + _echo_buf.extend(b"\b \b") + elif _b == 0x0D: + _echo_buf.extend(b"\r\n") + elif _b >= 0x20: + _echo_buf.append(_b) + if _echo_buf: + stdout.write(bytes(_echo_buf)) + return new_timer, pending + + def _flush_color_filter( + writer: Union[TelnetWriter, TelnetWriterUnicode], stdout: asyncio.StreamWriter + ) -> None: + """Flush any pending color filter output to stdout.""" + _cf = getattr(writer, "_color_filter", None) + if _cf is not None: + _flush = _cf.flush() + if _flush: + stdout.write(_flush.encode()) + + # pylint: disable=too-many-locals,too-many-branches,too-many-statements async def telnet_client_shell( telnet_reader: Union[TelnetReader, TelnetReaderUnicode], telnet_writer: Union[TelnetWriter, TelnetWriterUnicode], @@ -286,57 +499,32 @@ async def telnet_client_shell( with Terminal(telnet_writer=telnet_writer) as term: linesep = "\n" + switched_to_raw = False + last_will_echo = False + local_echo = term.software_echo if term._istty: # pylint: disable=protected-access - _raw = getattr(telnet_writer, '_raw_mode', False) - if telnet_writer.will_echo or _raw: + raw_mode = getattr(telnet_writer, "_raw_mode", False) + if telnet_writer.will_echo or raw_mode is True: linesep = "\r\n" stdin, stdout = await term.make_stdio() escape_name = accessories.name_unicode(keyboard_escape) stdout.write(f"Escape character is '{escape_name}'.{linesep}".encode()) + term.setup_winch() - # Setup SIGWINCH handler to send NAWS on terminal resize (POSIX only). - # We debounce to avoid flooding on continuous resizes. - loop = asyncio.get_event_loop() - winch_pending: dict[str, Optional[asyncio.TimerHandle]] = {"h": None} - remove_winch = False - if term._istty: # pylint: disable=protected-access - try: - - def _send_naws() -> None: - # local - from .telopt import NAWS # pylint: disable=import-outside-toplevel - - try: - if ( - telnet_writer.local_option.enabled(NAWS) - and not telnet_writer.is_closing() - ): - telnet_writer._send_naws() # pylint: disable=protected-access - except Exception: # pylint: disable=broad-exception-caught - pass - - def _on_winch() -> None: - h = winch_pending.get("h") - if h is not None and not h.cancelled(): - try: - h.cancel() - except Exception: # pylint: disable=broad-exception-caught - pass - winch_pending["h"] = loop.call_later(0.05, _send_naws) - - if hasattr(signal, "SIGWINCH"): - loop.add_signal_handler(signal.SIGWINCH, _on_winch) - remove_winch = True - except Exception: # pylint: disable=broad-exception-caught - remove_winch = False + def _handle_close(msg: str) -> None: + _flush_color_filter(telnet_writer, stdout) + stdout.write(f"\033[m{linesep}{msg}{linesep}".encode()) + term.cleanup_winch() stdin_task = accessories.make_reader_task(stdin) telnet_task = accessories.make_reader_task(telnet_reader, size=2**24) + esc_timer_task: Optional[asyncio.Task[None]] = None wait_for = set([stdin_task, telnet_task]) + # -- event loop: multiplex stdin, server output, and ESC_DELAY timer -- while wait_for: done, _ = await asyncio.wait(wait_for, return_when=asyncio.FIRST_COMPLETED) - # Prefer handling stdin events first to avoid starvation under heavy output + # Prefer handling stdin events first to avoid starvation if stdin_task in done: task = stdin_task done.discard(task) @@ -344,94 +532,65 @@ def _on_winch() -> None: task = done.pop() wait_for.discard(task) - telnet_writer.log.debug("task=%s, wait_for=%s", task, wait_for) + telnet_writer.log.log(TRACE, "task=%s, wait_for=%s", task, wait_for) + + # ESC_DELAY timer fired — flush buffered partial sequence + if task is esc_timer_task: + esc_timer_task = None + _inf = getattr(telnet_writer, "_input_filter", None) + if _inf is not None and _inf.has_pending: + flushed = _inf.flush() + if flushed: + telnet_writer._write(flushed) # pylint: disable=protected-access + continue # client input if task == stdin_task: + # Cancel ESC_DELAY timer — new input resolves buffering + if esc_timer_task is not None and esc_timer_task in wait_for: + esc_timer_task.cancel() + wait_for.discard(esc_timer_task) + esc_timer_task = None inp = task.result() - if inp: - if keyboard_escape in inp.decode(): - # on ^], close connection to remote host - try: - telnet_writer.close() - except Exception: # pylint: disable=broad-exception-caught - pass - if telnet_task in wait_for: - telnet_task.cancel() - wait_for.remove(telnet_task) - _cf = getattr(telnet_writer, "_color_filter", None) - if _cf is not None: - _flush = _cf.flush() - if _flush: - stdout.write(_flush.encode()) - stdout.write(f"\033[m{linesep}Connection closed.{linesep}".encode()) - # Cleanup resize handler on local escape close - if term._istty and remove_winch: # pylint: disable=protected-access - try: - loop.remove_signal_handler(signal.SIGWINCH) - except Exception: # pylint: disable=broad-exception-caught - pass - h = winch_pending.get("h") - if h is not None: - try: - h.cancel() - except Exception: # pylint: disable=broad-exception-caught - pass - break - _inf = getattr(telnet_writer, '_input_filter', None) - if _inf is not None: - translated = _inf.feed(inp) - if translated: - telnet_writer._write(translated) # pylint: disable=protected-access - else: - telnet_writer.write(inp.decode()) - stdin_task = accessories.make_reader_task(stdin) - wait_for.add(stdin_task) - else: + if not inp: telnet_writer.log.debug("EOF from client stdin") + continue + if keyboard_escape in inp.decode(): + try: + telnet_writer.close() + except Exception: # pylint: disable=broad-exception-caught + pass + if telnet_task in wait_for: + telnet_task.cancel() + wait_for.remove(telnet_task) + _handle_close("Connection closed.") + break + new_timer, has_pending = _send_stdin(inp, telnet_writer, stdout, local_echo) + if has_pending and esc_timer_task not in wait_for: + esc_timer_task = new_timer + if esc_timer_task is not None: + wait_for.add(esc_timer_task) + stdin_task = accessories.make_reader_task(stdin) + wait_for.add(stdin_task) # server output - if task == telnet_task: + elif task == telnet_task: out = task.result() - - # TODO: We should not require to check for '_eof' value, - # but for some systems, htc.zapto.org, it is required, - # where b'' is received even though connection is on?. if not out and telnet_reader._eof: # pylint: disable=protected-access if stdin_task in wait_for: stdin_task.cancel() wait_for.remove(stdin_task) - _cf = getattr(telnet_writer, "_color_filter", None) - if _cf is not None: - _flush = _cf.flush() - if _flush: - stdout.write(_flush.encode()) - stdout.write( - f"\033[m{linesep}Connection closed by foreign host.{linesep}".encode() - ) - # Cleanup resize handler on remote close - if term._istty and remove_winch: # pylint: disable=protected-access - try: - loop.remove_signal_handler(signal.SIGWINCH) - except Exception: # pylint: disable=broad-exception-caught - pass - h = winch_pending.get("h") - if h is not None: - try: - h.cancel() - except Exception: # pylint: disable=broad-exception-caught - pass - else: - _cf = getattr(telnet_writer, "_color_filter", None) - if _cf is not None: - out = _cf.filter(out) - if getattr(telnet_writer, '_raw_mode', False): - # Normalize all line endings to LF, then to CRLF - # for the raw terminal (OPOST disabled). PETSCII - # BBSes send bare CR (0x0D) as line terminator. - out = (out.replace('\r\n', '\n') - .replace('\r', '\n') - .replace('\n', '\r\n')) - stdout.write(out.encode() or b":?!?:") - telnet_task = accessories.make_reader_task(telnet_reader, size=2**24) - wait_for.add(telnet_task) + _handle_close("Connection closed by foreign host.") + continue + raw_mode = getattr(telnet_writer, "_raw_mode", False) + in_raw = raw_mode is True or (raw_mode is None and switched_to_raw) + out = _transform_output(out, telnet_writer, in_raw) + if raw_mode is None: + mode_result = term.check_auto_mode(switched_to_raw, last_will_echo) + if mode_result is not None: + if not switched_to_raw: + linesep = "\r\n" + switched_to_raw, last_will_echo, local_echo = mode_result + stdout.write(out.encode() or b":?!?:") + telnet_task = accessories.make_reader_task(telnet_reader, size=2**24) + wait_for.add(telnet_task) diff --git a/telnetlib3/color_filter.py b/telnetlib3/color_filter.py index 2a8fb76b..0415dcd9 100644 --- a/telnetlib3/color_filter.py +++ b/telnetlib3/color_filter.py @@ -41,13 +41,7 @@ # 3rd party from wcwidth.sgr_state import _SGR_PATTERN -__all__ = ( - "AtasciiControlFilter", - "ColorConfig", - "ColorFilter", - "PetsciiColorFilter", - "PALETTES", -) +__all__ = ("AtasciiControlFilter", "ColorConfig", "ColorFilter", "PetsciiColorFilter", "PALETTES") # Type alias for a 16-color palette: 16 (R, G, B) tuples indexed 0-15. # Index 0-7: normal colors (black, red, green, yellow, blue, magenta, cyan, white) @@ -174,22 +168,22 @@ # VIC-II C64 palette (Pepto's colodore reference). # Indexed by VIC-II color register 0-15, NOT ANSI SGR order. "c64": ( - (0, 0, 0), # 0 black - (255, 255, 255), # 1 white - (136, 0, 0), # 2 red - (170, 255, 238), # 3 cyan - (204, 68, 204), # 4 purple - (0, 204, 85), # 5 green - (0, 0, 170), # 6 blue - (238, 238, 119), # 7 yellow - (221, 136, 85), # 8 orange - (102, 68, 0), # 9 brown - (255, 119, 119), # 10 pink / light red - (51, 51, 51), # 11 dark grey - (119, 119, 119), # 12 grey - (170, 255, 102), # 13 light green - (0, 136, 255), # 14 light blue - (187, 187, 187), # 15 light grey + (0, 0, 0), # 0 black + (255, 255, 255), # 1 white + (136, 0, 0), # 2 red + (170, 255, 238), # 3 cyan + (204, 68, 204), # 4 purple + (0, 204, 85), # 5 green + (0, 0, 170), # 6 blue + (238, 238, 119), # 7 yellow + (221, 136, 85), # 8 orange + (102, 68, 0), # 9 brown + (255, 119, 119), # 10 pink / light red + (51, 51, 51), # 11 dark grey + (119, 119, 119), # 12 grey + (170, 255, 102), # 13 light green + (0, 136, 255), # 14 light blue + (187, 187, 187), # 15 light grey ), } @@ -459,47 +453,43 @@ def flush(self) -> str: # PETSCII decoded control character → VIC-II palette index (0-15). _PETSCII_COLOR_CODES: Dict[str, int] = { - '\x05': 1, # WHT (white) - '\x1c': 2, # RED - '\x1e': 5, # GRN (green) - '\x1f': 6, # BLU (blue) - '\x81': 8, # ORN (orange) - '\x90': 0, # BLK (black) - '\x95': 9, # BRN (brown) - '\x96': 10, # LRD (pink / light red) - '\x97': 11, # GR1 (dark grey) - '\x98': 12, # GR2 (grey) - '\x99': 13, # LGR (light green) - '\x9a': 14, # LBL (light blue) - '\x9b': 15, # GR3 (light grey) - '\x9c': 4, # PUR (purple) - '\x9e': 7, # YEL (yellow) - '\x9f': 3, # CYN (cyan) + "\x05": 1, # WHT (white) + "\x1c": 2, # RED + "\x1e": 5, # GRN (green) + "\x1f": 6, # BLU (blue) + "\x81": 8, # ORN (orange) + "\x90": 0, # BLK (black) + "\x95": 9, # BRN (brown) + "\x96": 10, # LRD (pink / light red) + "\x97": 11, # GR1 (dark grey) + "\x98": 12, # GR2 (grey) + "\x99": 13, # LGR (light green) + "\x9a": 14, # LBL (light blue) + "\x9b": 15, # GR3 (light grey) + "\x9c": 4, # PUR (purple) + "\x9e": 7, # YEL (yellow) + "\x9f": 3, # CYN (cyan) } # PETSCII cursor/screen control codes → ANSI escape sequences. _PETSCII_CURSOR_CODES: Dict[str, str] = { - '\x11': '\x1b[B', # cursor down - '\x91': '\x1b[A', # cursor up - '\x1d': '\x1b[C', # cursor right - '\x9d': '\x1b[D', # cursor left - '\x13': '\x1b[H', # HOME (cursor to top-left) - '\x93': '\x1b[2J', # CLR (clear screen) - '\x14': '\x08\x1b[P', # DEL (destructive backspace) + "\x11": "\x1b[B", # cursor down + "\x91": "\x1b[A", # cursor up + "\x1d": "\x1b[C", # cursor right + "\x9d": "\x1b[D", # cursor left + "\x13": "\x1b[H", # HOME (cursor to top-left) + "\x93": "\x1b[2J", # CLR (clear screen) + "\x14": "\x08\x1b[P", # DEL (destructive backspace) } # All PETSCII control chars handled by the filter. _PETSCII_FILTER_CHARS = ( - frozenset(_PETSCII_COLOR_CODES) - | frozenset(_PETSCII_CURSOR_CODES) - | {'\x12', '\x92'} + frozenset(_PETSCII_COLOR_CODES) | frozenset(_PETSCII_CURSOR_CODES) | {"\x12", "\x92"} ) # Precompiled pattern matching any single PETSCII control character that # the filter should consume (color codes, cursor codes, RVS ON/OFF). -_PETSCII_CTRL_RE = re.compile( - '[' + re.escape(''.join(sorted(_PETSCII_FILTER_CHARS))) + ']' -) +_PETSCII_CTRL_RE = re.compile("[" + re.escape("".join(sorted(_PETSCII_FILTER_CHARS))) + "]") class PetsciiColorFilter: @@ -536,7 +526,7 @@ def __init__(self, config: Optional[ColorConfig] = None) -> None: def _sgr_for_index(self, idx: int) -> str: """Return a 24-bit foreground SGR sequence for palette *idx*.""" r, g, b = self._adjusted[idx] - return f'\x1b[38;2;{r};{g};{b}m' + return f"\x1b[38;2;{r};{g};{b}m" def filter(self, text: str) -> str: """ @@ -561,11 +551,11 @@ def _replace(self, match: Match[str]) -> str: cursor = _PETSCII_CURSOR_CODES.get(ch) if cursor is not None: return cursor - if ch == '\x12': - return '\x1b[7m' - if ch == '\x92': - return '\x1b[27m' - return '' + if ch == "\x12": + return "\x1b[7m" + if ch == "\x92": + return "\x1b[27m" + return "" def flush(self) -> str: """ @@ -582,18 +572,16 @@ def flush(self) -> str: # The atascii codec decodes control bytes to Unicode glyphs; this map # translates those glyphs to the terminal actions they represent. _ATASCII_CONTROL_CODES: Dict[str, str] = { - '\u25c0': '\x08\x1b[P', # ◀ backspace/delete (0x7E / 0xFE) - '\u25b6': '\t', # ▶ tab (0x7F / 0xFF) - '\u21b0': '\x1b[2J\x1b[H', # ↰ clear screen (0x7D / 0xFD) - '\u2191': '\x1b[A', # ↑ cursor up (0x1C / 0x9C) - '\u2193': '\x1b[B', # ↓ cursor down (0x1D / 0x9D) - '\u2190': '\x1b[D', # ← cursor left (0x1E / 0x9E) - '\u2192': '\x1b[C', # → cursor right (0x1F / 0x9F) + "\u25c0": "\x08\x1b[P", # ◀ backspace/delete (0x7E / 0xFE) + "\u25b6": "\t", # ▶ tab (0x7F / 0xFF) + "\u21b0": "\x1b[2J\x1b[H", # ↰ clear screen (0x7D / 0xFD) + "\u2191": "\x1b[A", # ↑ cursor up (0x1C / 0x9C) + "\u2193": "\x1b[B", # ↓ cursor down (0x1D / 0x9D) + "\u2190": "\x1b[D", # ← cursor left (0x1E / 0x9E) + "\u2192": "\x1b[C", # → cursor right (0x1F / 0x9F) } -_ATASCII_CTRL_RE = re.compile( - '[' + re.escape(''.join(sorted(_ATASCII_CONTROL_CODES))) + ']' -) +_ATASCII_CTRL_RE = re.compile("[" + re.escape("".join(sorted(_ATASCII_CONTROL_CODES))) + "]") class AtasciiControlFilter: @@ -624,7 +612,7 @@ def filter(self, text: str) -> str: @staticmethod def _replace(match: Match[str]) -> str: """Regex callback for a single ATASCII control glyph.""" - return _ATASCII_CONTROL_CODES.get(match.group(), '') + return _ATASCII_CONTROL_CODES.get(match.group(), "") @staticmethod def flush() -> str: diff --git a/telnetlib3/encodings/__init__.py b/telnetlib3/encodings/__init__.py index c9f3be05..30fb7bfe 100644 --- a/telnetlib3/encodings/__init__.py +++ b/telnetlib3/encodings/__init__.py @@ -10,14 +10,15 @@ # std imports import codecs import importlib +from typing import Optional -_cache = {} -_aliases = {} +_cache: dict[str, Optional[codecs.CodecInfo]] = {} +_aliases: dict[str, codecs.CodecInfo] = {} -def _search_function(encoding): +def _search_function(encoding: str) -> Optional[codecs.CodecInfo]: """Codec search function registered with codecs.register().""" - normalized = encoding.lower().replace('-', '_') + normalized = encoding.lower().replace("-", "_") if normalized in _aliases: return _aliases[normalized] @@ -26,20 +27,20 @@ def _search_function(encoding): return _cache[normalized] try: - mod = importlib.import_module(f'.{normalized}', package=__name__) + mod = importlib.import_module(f".{normalized}", package=__name__) except ImportError: _cache[normalized] = None return None try: - info = mod.getregentry() + info: codecs.CodecInfo = mod.getregentry() except AttributeError: _cache[normalized] = None return None _cache[normalized] = info - if hasattr(mod, 'getaliases'): + if hasattr(mod, "getaliases"): for alias in mod.getaliases(): _aliases[alias] = info @@ -48,10 +49,19 @@ def _search_function(encoding): #: Encoding names (and aliases) that require BINARY mode for high-bit bytes. #: Used by CLI entry points to auto-enable ``--force-binary``. -FORCE_BINARY_ENCODINGS = frozenset({ - 'atascii', 'atari8bit', 'atari_8bit', - 'petscii', 'cbm', 'commodore', 'c64', 'c128', - 'atarist', 'atari', -}) +FORCE_BINARY_ENCODINGS = frozenset( + { + "atascii", + "atari8bit", + "atari_8bit", + "petscii", + "cbm", + "commodore", + "c64", + "c128", + "atarist", + "atari", + } +) codecs.register(_search_function) diff --git a/telnetlib3/encodings/atarist.py b/telnetlib3/encodings/atarist.py index c454d19c..cf1dd3c4 100644 --- a/telnetlib3/encodings/atarist.py +++ b/telnetlib3/encodings/atarist.py @@ -3,6 +3,7 @@ Generated from ftp://ftp.unicode.org/Public/MAPPINGS/VENDORS/MISC/ATARIST.TXT """ + # pylint: disable=redefined-builtin # std imports @@ -12,19 +13,19 @@ class Codec(codecs.Codec): """Atari ST character map codec.""" - def encode(self, input, errors='strict'): + def encode(self, input: str, errors: str = "strict") -> tuple[bytes, int]: """Encode input string using Atari ST character map.""" return codecs.charmap_encode(input, errors, ENCODING_TABLE) - def decode(self, input, errors='strict'): + def decode(self, input: bytes, errors: str = "strict") -> tuple[str, int]: """Decode input bytes using Atari ST character map.""" - return codecs.charmap_decode(input, errors, DECODING_TABLE) + return codecs.charmap_decode(input, errors, DECODING_TABLE) # type: ignore[arg-type] class IncrementalEncoder(codecs.IncrementalEncoder): """Atari ST incremental encoder.""" - def encode(self, input, final=False): + def encode(self, input: str, final: bool = False) -> bytes: """Encode input string incrementally.""" return codecs.charmap_encode(input, self.errors, ENCODING_TABLE)[0] @@ -32,9 +33,13 @@ def encode(self, input, final=False): class IncrementalDecoder(codecs.IncrementalDecoder): """Atari ST incremental decoder.""" - def decode(self, input, final=False): + def decode( # type: ignore[override] + self, input: bytes, final: bool = False + ) -> str: """Decode input bytes incrementally.""" - return codecs.charmap_decode(input, self.errors, DECODING_TABLE)[0] + return codecs.charmap_decode(input, self.errors, DECODING_TABLE)[ # type: ignore[arg-type] + 0 + ] class StreamWriter(Codec, codecs.StreamWriter): @@ -45,17 +50,17 @@ class StreamReader(Codec, codecs.StreamReader): """Atari ST stream reader.""" -def getaliases(): +def getaliases() -> tuple[str, ...]: """Return codec aliases.""" - return ('atari',) + return ("atari",) -def getregentry(): +def getregentry() -> codecs.CodecInfo: """Return the codec registry entry.""" return codecs.CodecInfo( - name='atarist', + name="atarist", encode=Codec().encode, - decode=Codec().decode, + decode=Codec().decode, # type: ignore[arg-type] incrementalencoder=IncrementalEncoder, incrementaldecoder=IncrementalDecoder, streamreader=StreamReader, @@ -66,262 +71,262 @@ def getregentry(): # Decoding Table DECODING_TABLE = ( - '\x00' # 0x00 -> NULL - '\x01' # 0x01 -> START OF HEADING - '\x02' # 0x02 -> START OF TEXT - '\x03' # 0x03 -> END OF TEXT - '\x04' # 0x04 -> END OF TRANSMISSION - '\x05' # 0x05 -> ENQUIRY - '\x06' # 0x06 -> ACKNOWLEDGE - '\x07' # 0x07 -> BELL - '\x08' # 0x08 -> BACKSPACE - '\t' # 0x09 -> HORIZONTAL TABULATION - '\n' # 0x0A -> LINE FEED - '\x0b' # 0x0B -> VERTICAL TABULATION - '\x0c' # 0x0C -> FORM FEED - '\r' # 0x0D -> CARRIAGE RETURN - '\x0e' # 0x0E -> SHIFT OUT - '\x0f' # 0x0F -> SHIFT IN - '\x10' # 0x10 -> DATA LINK ESCAPE - '\x11' # 0x11 -> DEVICE CONTROL ONE - '\x12' # 0x12 -> DEVICE CONTROL TWO - '\x13' # 0x13 -> DEVICE CONTROL THREE - '\x14' # 0x14 -> DEVICE CONTROL FOUR - '\x15' # 0x15 -> NEGATIVE ACKNOWLEDGE - '\x16' # 0x16 -> SYNCHRONOUS IDLE - '\x17' # 0x17 -> END OF TRANSMISSION BLOCK - '\x18' # 0x18 -> CANCEL - '\x19' # 0x19 -> END OF MEDIUM - '\x1a' # 0x1A -> SUBSTITUTE - '\x1b' # 0x1B -> ESCAPE - '\x1c' # 0x1C -> FILE SEPARATOR - '\x1d' # 0x1D -> GROUP SEPARATOR - '\x1e' # 0x1E -> RECORD SEPARATOR - '\x1f' # 0x1F -> UNIT SEPARATOR - ' ' # 0x20 -> SPACE - '!' # 0x21 -> EXCLAMATION MARK + "\x00" # 0x00 -> NULL + "\x01" # 0x01 -> START OF HEADING + "\x02" # 0x02 -> START OF TEXT + "\x03" # 0x03 -> END OF TEXT + "\x04" # 0x04 -> END OF TRANSMISSION + "\x05" # 0x05 -> ENQUIRY + "\x06" # 0x06 -> ACKNOWLEDGE + "\x07" # 0x07 -> BELL + "\x08" # 0x08 -> BACKSPACE + "\t" # 0x09 -> HORIZONTAL TABULATION + "\n" # 0x0A -> LINE FEED + "\x0b" # 0x0B -> VERTICAL TABULATION + "\x0c" # 0x0C -> FORM FEED + "\r" # 0x0D -> CARRIAGE RETURN + "\x0e" # 0x0E -> SHIFT OUT + "\x0f" # 0x0F -> SHIFT IN + "\x10" # 0x10 -> DATA LINK ESCAPE + "\x11" # 0x11 -> DEVICE CONTROL ONE + "\x12" # 0x12 -> DEVICE CONTROL TWO + "\x13" # 0x13 -> DEVICE CONTROL THREE + "\x14" # 0x14 -> DEVICE CONTROL FOUR + "\x15" # 0x15 -> NEGATIVE ACKNOWLEDGE + "\x16" # 0x16 -> SYNCHRONOUS IDLE + "\x17" # 0x17 -> END OF TRANSMISSION BLOCK + "\x18" # 0x18 -> CANCEL + "\x19" # 0x19 -> END OF MEDIUM + "\x1a" # 0x1A -> SUBSTITUTE + "\x1b" # 0x1B -> ESCAPE + "\x1c" # 0x1C -> FILE SEPARATOR + "\x1d" # 0x1D -> GROUP SEPARATOR + "\x1e" # 0x1E -> RECORD SEPARATOR + "\x1f" # 0x1F -> UNIT SEPARATOR + " " # 0x20 -> SPACE + "!" # 0x21 -> EXCLAMATION MARK '"' # 0x22 -> QUOTATION MARK - '#' # 0x23 -> NUMBER SIGN - '$' # 0x24 -> DOLLAR SIGN - '%' # 0x25 -> PERCENT SIGN - '&' # 0x26 -> AMPERSAND + "#" # 0x23 -> NUMBER SIGN + "$" # 0x24 -> DOLLAR SIGN + "%" # 0x25 -> PERCENT SIGN + "&" # 0x26 -> AMPERSAND "'" # 0x27 -> APOSTROPHE - '(' # 0x28 -> LEFT PARENTHESIS - ')' # 0x29 -> RIGHT PARENTHESIS - '*' # 0x2A -> ASTERISK - '+' # 0x2B -> PLUS SIGN - ',' # 0x2C -> COMMA - '-' # 0x2D -> HYPHEN-MINUS - '.' # 0x2E -> FULL STOP - '/' # 0x2F -> SOLIDUS - '0' # 0x30 -> DIGIT ZERO - '1' # 0x31 -> DIGIT ONE - '2' # 0x32 -> DIGIT TWO - '3' # 0x33 -> DIGIT THREE - '4' # 0x34 -> DIGIT FOUR - '5' # 0x35 -> DIGIT FIVE - '6' # 0x36 -> DIGIT SIX - '7' # 0x37 -> DIGIT SEVEN - '8' # 0x38 -> DIGIT EIGHT - '9' # 0x39 -> DIGIT NINE - ':' # 0x3A -> COLON - ';' # 0x3B -> SEMICOLON - '<' # 0x3C -> LESS-THAN SIGN - '=' # 0x3D -> EQUALS SIGN - '>' # 0x3E -> GREATER-THAN SIGN - '?' # 0x3F -> QUESTION MARK - '@' # 0x40 -> COMMERCIAL AT - 'A' # 0x41 -> LATIN CAPITAL LETTER A - 'B' # 0x42 -> LATIN CAPITAL LETTER B - 'C' # 0x43 -> LATIN CAPITAL LETTER C - 'D' # 0x44 -> LATIN CAPITAL LETTER D - 'E' # 0x45 -> LATIN CAPITAL LETTER E - 'F' # 0x46 -> LATIN CAPITAL LETTER F - 'G' # 0x47 -> LATIN CAPITAL LETTER G - 'H' # 0x48 -> LATIN CAPITAL LETTER H - 'I' # 0x49 -> LATIN CAPITAL LETTER I - 'J' # 0x4A -> LATIN CAPITAL LETTER J - 'K' # 0x4B -> LATIN CAPITAL LETTER K - 'L' # 0x4C -> LATIN CAPITAL LETTER L - 'M' # 0x4D -> LATIN CAPITAL LETTER M - 'N' # 0x4E -> LATIN CAPITAL LETTER N - 'O' # 0x4F -> LATIN CAPITAL LETTER O - 'P' # 0x50 -> LATIN CAPITAL LETTER P - 'Q' # 0x51 -> LATIN CAPITAL LETTER Q - 'R' # 0x52 -> LATIN CAPITAL LETTER R - 'S' # 0x53 -> LATIN CAPITAL LETTER S - 'T' # 0x54 -> LATIN CAPITAL LETTER T - 'U' # 0x55 -> LATIN CAPITAL LETTER U - 'V' # 0x56 -> LATIN CAPITAL LETTER V - 'W' # 0x57 -> LATIN CAPITAL LETTER W - 'X' # 0x58 -> LATIN CAPITAL LETTER X - 'Y' # 0x59 -> LATIN CAPITAL LETTER Y - 'Z' # 0x5A -> LATIN CAPITAL LETTER Z - '[' # 0x5B -> LEFT SQUARE BRACKET - '\\' # 0x5C -> REVERSE SOLIDUS - ']' # 0x5D -> RIGHT SQUARE BRACKET - '^' # 0x5E -> CIRCUMFLEX ACCENT - '_' # 0x5F -> LOW LINE - '`' # 0x60 -> GRAVE ACCENT - 'a' # 0x61 -> LATIN SMALL LETTER A - 'b' # 0x62 -> LATIN SMALL LETTER B - 'c' # 0x63 -> LATIN SMALL LETTER C - 'd' # 0x64 -> LATIN SMALL LETTER D - 'e' # 0x65 -> LATIN SMALL LETTER E - 'f' # 0x66 -> LATIN SMALL LETTER F - 'g' # 0x67 -> LATIN SMALL LETTER G - 'h' # 0x68 -> LATIN SMALL LETTER H - 'i' # 0x69 -> LATIN SMALL LETTER I - 'j' # 0x6A -> LATIN SMALL LETTER J - 'k' # 0x6B -> LATIN SMALL LETTER K - 'l' # 0x6C -> LATIN SMALL LETTER L - 'm' # 0x6D -> LATIN SMALL LETTER M - 'n' # 0x6E -> LATIN SMALL LETTER N - 'o' # 0x6F -> LATIN SMALL LETTER O - 'p' # 0x70 -> LATIN SMALL LETTER P - 'q' # 0x71 -> LATIN SMALL LETTER Q - 'r' # 0x72 -> LATIN SMALL LETTER R - 's' # 0x73 -> LATIN SMALL LETTER S - 't' # 0x74 -> LATIN SMALL LETTER T - 'u' # 0x75 -> LATIN SMALL LETTER U - 'v' # 0x76 -> LATIN SMALL LETTER V - 'w' # 0x77 -> LATIN SMALL LETTER W - 'x' # 0x78 -> LATIN SMALL LETTER X - 'y' # 0x79 -> LATIN SMALL LETTER Y - 'z' # 0x7A -> LATIN SMALL LETTER Z - '{' # 0x7B -> LEFT CURLY BRACKET - '|' # 0x7C -> VERTICAL LINE - '}' # 0x7D -> RIGHT CURLY BRACKET - '~' # 0x7E -> TILDE - '\x7f' # 0x7F -> DELETE - '\xc7' # 0x80 -> LATIN CAPITAL LETTER C WITH CEDILLA - '\xfc' # 0x81 -> LATIN SMALL LETTER U WITH DIAERESIS - '\xe9' # 0x82 -> LATIN SMALL LETTER E WITH ACUTE - '\xe2' # 0x83 -> LATIN SMALL LETTER A WITH CIRCUMFLEX - '\xe4' # 0x84 -> LATIN SMALL LETTER A WITH DIAERESIS - '\xe0' # 0x85 -> LATIN SMALL LETTER A WITH GRAVE - '\xe5' # 0x86 -> LATIN SMALL LETTER A WITH RING ABOVE - '\xe7' # 0x87 -> LATIN SMALL LETTER C WITH CEDILLA - '\xea' # 0x88 -> LATIN SMALL LETTER E WITH CIRCUMFLEX - '\xeb' # 0x89 -> LATIN SMALL LETTER E WITH DIAERESIS - '\xe8' # 0x8A -> LATIN SMALL LETTER E WITH GRAVE - '\xef' # 0x8B -> LATIN SMALL LETTER I WITH DIAERESIS - '\xee' # 0x8C -> LATIN SMALL LETTER I WITH CIRCUMFLEX - '\xec' # 0x8D -> LATIN SMALL LETTER I WITH GRAVE - '\xc4' # 0x8E -> LATIN CAPITAL LETTER A WITH DIAERESIS - '\xc5' # 0x8F -> LATIN CAPITAL LETTER A WITH RING ABOVE - '\xc9' # 0x90 -> LATIN CAPITAL LETTER E WITH ACUTE - '\xe6' # 0x91 -> LATIN SMALL LETTER AE - '\xc6' # 0x92 -> LATIN CAPITAL LETTER AE - '\xf4' # 0x93 -> LATIN SMALL LETTER O WITH CIRCUMFLEX - '\xf6' # 0x94 -> LATIN SMALL LETTER O WITH DIAERESIS - '\xf2' # 0x95 -> LATIN SMALL LETTER O WITH GRAVE - '\xfb' # 0x96 -> LATIN SMALL LETTER U WITH CIRCUMFLEX - '\xf9' # 0x97 -> LATIN SMALL LETTER U WITH GRAVE - '\xff' # 0x98 -> LATIN SMALL LETTER Y WITH DIAERESIS - '\xd6' # 0x99 -> LATIN CAPITAL LETTER O WITH DIAERESIS - '\xdc' # 0x9A -> LATIN CAPITAL LETTER U WITH DIAERESIS - '\xa2' # 0x9B -> CENT SIGN - '\xa3' # 0x9C -> POUND SIGN - '\xa5' # 0x9D -> YEN SIGN - '\xdf' # 0x9E -> LATIN SMALL LETTER SHARP S - '\u0192' # 0x9F -> LATIN SMALL LETTER F WITH HOOK - '\xe1' # 0xA0 -> LATIN SMALL LETTER A WITH ACUTE - '\xed' # 0xA1 -> LATIN SMALL LETTER I WITH ACUTE - '\xf3' # 0xA2 -> LATIN SMALL LETTER O WITH ACUTE - '\xfa' # 0xA3 -> LATIN SMALL LETTER U WITH ACUTE - '\xf1' # 0xA4 -> LATIN SMALL LETTER N WITH TILDE - '\xd1' # 0xA5 -> LATIN CAPITAL LETTER N WITH TILDE - '\xaa' # 0xA6 -> FEMININE ORDINAL INDICATOR - '\xba' # 0xA7 -> MASCULINE ORDINAL INDICATOR - '\xbf' # 0xA8 -> INVERTED QUESTION MARK - '\u2310' # 0xA9 -> REVERSED NOT SIGN - '\xac' # 0xAA -> NOT SIGN - '\xbd' # 0xAB -> VULGAR FRACTION ONE HALF - '\xbc' # 0xAC -> VULGAR FRACTION ONE QUARTER - '\xa1' # 0xAD -> INVERTED EXCLAMATION MARK - '\xab' # 0xAE -> LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - '\xbb' # 0xAF -> RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - '\xe3' # 0xB0 -> LATIN SMALL LETTER A WITH TILDE - '\xf5' # 0xB1 -> LATIN SMALL LETTER O WITH TILDE - '\xd8' # 0xB2 -> LATIN CAPITAL LETTER O WITH STROKE - '\xf8' # 0xB3 -> LATIN SMALL LETTER O WITH STROKE - '\u0153' # 0xB4 -> LATIN SMALL LIGATURE OE - '\u0152' # 0xB5 -> LATIN CAPITAL LIGATURE OE - '\xc0' # 0xB6 -> LATIN CAPITAL LETTER A WITH GRAVE - '\xc3' # 0xB7 -> LATIN CAPITAL LETTER A WITH TILDE - '\xd5' # 0xB8 -> LATIN CAPITAL LETTER O WITH TILDE - '\xa8' # 0xB9 -> DIAERESIS - '\xb4' # 0xBA -> ACUTE ACCENT - '\u2020' # 0xBB -> DAGGER - '\xb6' # 0xBC -> PILCROW SIGN - '\xa9' # 0xBD -> COPYRIGHT SIGN - '\xae' # 0xBE -> REGISTERED SIGN - '\u2122' # 0xBF -> TRADE MARK SIGN - '\u0133' # 0xC0 -> LATIN SMALL LIGATURE IJ - '\u0132' # 0xC1 -> LATIN CAPITAL LIGATURE IJ - '\u05d0' # 0xC2 -> HEBREW LETTER ALEF - '\u05d1' # 0xC3 -> HEBREW LETTER BET - '\u05d2' # 0xC4 -> HEBREW LETTER GIMEL - '\u05d3' # 0xC5 -> HEBREW LETTER DALET - '\u05d4' # 0xC6 -> HEBREW LETTER HE - '\u05d5' # 0xC7 -> HEBREW LETTER VAV - '\u05d6' # 0xC8 -> HEBREW LETTER ZAYIN - '\u05d7' # 0xC9 -> HEBREW LETTER HET - '\u05d8' # 0xCA -> HEBREW LETTER TET - '\u05d9' # 0xCB -> HEBREW LETTER YOD - '\u05db' # 0xCC -> HEBREW LETTER KAF - '\u05dc' # 0xCD -> HEBREW LETTER LAMED - '\u05de' # 0xCE -> HEBREW LETTER MEM - '\u05e0' # 0xCF -> HEBREW LETTER NUN - '\u05e1' # 0xD0 -> HEBREW LETTER SAMEKH - '\u05e2' # 0xD1 -> HEBREW LETTER AYIN - '\u05e4' # 0xD2 -> HEBREW LETTER PE - '\u05e6' # 0xD3 -> HEBREW LETTER TSADI - '\u05e7' # 0xD4 -> HEBREW LETTER QOF - '\u05e8' # 0xD5 -> HEBREW LETTER RESH - '\u05e9' # 0xD6 -> HEBREW LETTER SHIN - '\u05ea' # 0xD7 -> HEBREW LETTER TAV - '\u05df' # 0xD8 -> HEBREW LETTER FINAL NUN - '\u05da' # 0xD9 -> HEBREW LETTER FINAL KAF - '\u05dd' # 0xDA -> HEBREW LETTER FINAL MEM - '\u05e3' # 0xDB -> HEBREW LETTER FINAL PE - '\u05e5' # 0xDC -> HEBREW LETTER FINAL TSADI - '\xa7' # 0xDD -> SECTION SIGN - '\u2227' # 0xDE -> LOGICAL AND - '\u221e' # 0xDF -> INFINITY - '\u03b1' # 0xE0 -> GREEK SMALL LETTER ALPHA - '\u03b2' # 0xE1 -> GREEK SMALL LETTER BETA - '\u0393' # 0xE2 -> GREEK CAPITAL LETTER GAMMA - '\u03c0' # 0xE3 -> GREEK SMALL LETTER PI - '\u03a3' # 0xE4 -> GREEK CAPITAL LETTER SIGMA - '\u03c3' # 0xE5 -> GREEK SMALL LETTER SIGMA - '\xb5' # 0xE6 -> MICRO SIGN - '\u03c4' # 0xE7 -> GREEK SMALL LETTER TAU - '\u03a6' # 0xE8 -> GREEK CAPITAL LETTER PHI - '\u0398' # 0xE9 -> GREEK CAPITAL LETTER THETA - '\u03a9' # 0xEA -> GREEK CAPITAL LETTER OMEGA - '\u03b4' # 0xEB -> GREEK SMALL LETTER DELTA - '\u222e' # 0xEC -> CONTOUR INTEGRAL - '\u03c6' # 0xED -> GREEK SMALL LETTER PHI - '\u2208' # 0xEE -> ELEMENT OF SIGN - '\u2229' # 0xEF -> INTERSECTION - '\u2261' # 0xF0 -> IDENTICAL TO - '\xb1' # 0xF1 -> PLUS-MINUS SIGN - '\u2265' # 0xF2 -> GREATER-THAN OR EQUAL TO - '\u2264' # 0xF3 -> LESS-THAN OR EQUAL TO - '\u2320' # 0xF4 -> TOP HALF INTEGRAL - '\u2321' # 0xF5 -> BOTTOM HALF INTEGRAL - '\xf7' # 0xF6 -> DIVISION SIGN - '\u2248' # 0xF7 -> ALMOST EQUAL TO - '\xb0' # 0xF8 -> DEGREE SIGN - '\u2219' # 0xF9 -> BULLET OPERATOR - '\xb7' # 0xFA -> MIDDLE DOT - '\u221a' # 0xFB -> SQUARE ROOT - '\u207f' # 0xFC -> SUPERSCRIPT LATIN SMALL LETTER N - '\xb2' # 0xFD -> SUPERSCRIPT TWO - '\xb3' # 0xFE -> SUPERSCRIPT THREE - '\xaf' # 0xFF -> MACRON + "(" # 0x28 -> LEFT PARENTHESIS + ")" # 0x29 -> RIGHT PARENTHESIS + "*" # 0x2A -> ASTERISK + "+" # 0x2B -> PLUS SIGN + "," # 0x2C -> COMMA + "-" # 0x2D -> HYPHEN-MINUS + "." # 0x2E -> FULL STOP + "/" # 0x2F -> SOLIDUS + "0" # 0x30 -> DIGIT ZERO + "1" # 0x31 -> DIGIT ONE + "2" # 0x32 -> DIGIT TWO + "3" # 0x33 -> DIGIT THREE + "4" # 0x34 -> DIGIT FOUR + "5" # 0x35 -> DIGIT FIVE + "6" # 0x36 -> DIGIT SIX + "7" # 0x37 -> DIGIT SEVEN + "8" # 0x38 -> DIGIT EIGHT + "9" # 0x39 -> DIGIT NINE + ":" # 0x3A -> COLON + ";" # 0x3B -> SEMICOLON + "<" # 0x3C -> LESS-THAN SIGN + "=" # 0x3D -> EQUALS SIGN + ">" # 0x3E -> GREATER-THAN SIGN + "?" # 0x3F -> QUESTION MARK + "@" # 0x40 -> COMMERCIAL AT + "A" # 0x41 -> LATIN CAPITAL LETTER A + "B" # 0x42 -> LATIN CAPITAL LETTER B + "C" # 0x43 -> LATIN CAPITAL LETTER C + "D" # 0x44 -> LATIN CAPITAL LETTER D + "E" # 0x45 -> LATIN CAPITAL LETTER E + "F" # 0x46 -> LATIN CAPITAL LETTER F + "G" # 0x47 -> LATIN CAPITAL LETTER G + "H" # 0x48 -> LATIN CAPITAL LETTER H + "I" # 0x49 -> LATIN CAPITAL LETTER I + "J" # 0x4A -> LATIN CAPITAL LETTER J + "K" # 0x4B -> LATIN CAPITAL LETTER K + "L" # 0x4C -> LATIN CAPITAL LETTER L + "M" # 0x4D -> LATIN CAPITAL LETTER M + "N" # 0x4E -> LATIN CAPITAL LETTER N + "O" # 0x4F -> LATIN CAPITAL LETTER O + "P" # 0x50 -> LATIN CAPITAL LETTER P + "Q" # 0x51 -> LATIN CAPITAL LETTER Q + "R" # 0x52 -> LATIN CAPITAL LETTER R + "S" # 0x53 -> LATIN CAPITAL LETTER S + "T" # 0x54 -> LATIN CAPITAL LETTER T + "U" # 0x55 -> LATIN CAPITAL LETTER U + "V" # 0x56 -> LATIN CAPITAL LETTER V + "W" # 0x57 -> LATIN CAPITAL LETTER W + "X" # 0x58 -> LATIN CAPITAL LETTER X + "Y" # 0x59 -> LATIN CAPITAL LETTER Y + "Z" # 0x5A -> LATIN CAPITAL LETTER Z + "[" # 0x5B -> LEFT SQUARE BRACKET + "\\" # 0x5C -> REVERSE SOLIDUS + "]" # 0x5D -> RIGHT SQUARE BRACKET + "^" # 0x5E -> CIRCUMFLEX ACCENT + "_" # 0x5F -> LOW LINE + "`" # 0x60 -> GRAVE ACCENT + "a" # 0x61 -> LATIN SMALL LETTER A + "b" # 0x62 -> LATIN SMALL LETTER B + "c" # 0x63 -> LATIN SMALL LETTER C + "d" # 0x64 -> LATIN SMALL LETTER D + "e" # 0x65 -> LATIN SMALL LETTER E + "f" # 0x66 -> LATIN SMALL LETTER F + "g" # 0x67 -> LATIN SMALL LETTER G + "h" # 0x68 -> LATIN SMALL LETTER H + "i" # 0x69 -> LATIN SMALL LETTER I + "j" # 0x6A -> LATIN SMALL LETTER J + "k" # 0x6B -> LATIN SMALL LETTER K + "l" # 0x6C -> LATIN SMALL LETTER L + "m" # 0x6D -> LATIN SMALL LETTER M + "n" # 0x6E -> LATIN SMALL LETTER N + "o" # 0x6F -> LATIN SMALL LETTER O + "p" # 0x70 -> LATIN SMALL LETTER P + "q" # 0x71 -> LATIN SMALL LETTER Q + "r" # 0x72 -> LATIN SMALL LETTER R + "s" # 0x73 -> LATIN SMALL LETTER S + "t" # 0x74 -> LATIN SMALL LETTER T + "u" # 0x75 -> LATIN SMALL LETTER U + "v" # 0x76 -> LATIN SMALL LETTER V + "w" # 0x77 -> LATIN SMALL LETTER W + "x" # 0x78 -> LATIN SMALL LETTER X + "y" # 0x79 -> LATIN SMALL LETTER Y + "z" # 0x7A -> LATIN SMALL LETTER Z + "{" # 0x7B -> LEFT CURLY BRACKET + "|" # 0x7C -> VERTICAL LINE + "}" # 0x7D -> RIGHT CURLY BRACKET + "~" # 0x7E -> TILDE + "\x7f" # 0x7F -> DELETE + "\xc7" # 0x80 -> LATIN CAPITAL LETTER C WITH CEDILLA + "\xfc" # 0x81 -> LATIN SMALL LETTER U WITH DIAERESIS + "\xe9" # 0x82 -> LATIN SMALL LETTER E WITH ACUTE + "\xe2" # 0x83 -> LATIN SMALL LETTER A WITH CIRCUMFLEX + "\xe4" # 0x84 -> LATIN SMALL LETTER A WITH DIAERESIS + "\xe0" # 0x85 -> LATIN SMALL LETTER A WITH GRAVE + "\xe5" # 0x86 -> LATIN SMALL LETTER A WITH RING ABOVE + "\xe7" # 0x87 -> LATIN SMALL LETTER C WITH CEDILLA + "\xea" # 0x88 -> LATIN SMALL LETTER E WITH CIRCUMFLEX + "\xeb" # 0x89 -> LATIN SMALL LETTER E WITH DIAERESIS + "\xe8" # 0x8A -> LATIN SMALL LETTER E WITH GRAVE + "\xef" # 0x8B -> LATIN SMALL LETTER I WITH DIAERESIS + "\xee" # 0x8C -> LATIN SMALL LETTER I WITH CIRCUMFLEX + "\xec" # 0x8D -> LATIN SMALL LETTER I WITH GRAVE + "\xc4" # 0x8E -> LATIN CAPITAL LETTER A WITH DIAERESIS + "\xc5" # 0x8F -> LATIN CAPITAL LETTER A WITH RING ABOVE + "\xc9" # 0x90 -> LATIN CAPITAL LETTER E WITH ACUTE + "\xe6" # 0x91 -> LATIN SMALL LETTER AE + "\xc6" # 0x92 -> LATIN CAPITAL LETTER AE + "\xf4" # 0x93 -> LATIN SMALL LETTER O WITH CIRCUMFLEX + "\xf6" # 0x94 -> LATIN SMALL LETTER O WITH DIAERESIS + "\xf2" # 0x95 -> LATIN SMALL LETTER O WITH GRAVE + "\xfb" # 0x96 -> LATIN SMALL LETTER U WITH CIRCUMFLEX + "\xf9" # 0x97 -> LATIN SMALL LETTER U WITH GRAVE + "\xff" # 0x98 -> LATIN SMALL LETTER Y WITH DIAERESIS + "\xd6" # 0x99 -> LATIN CAPITAL LETTER O WITH DIAERESIS + "\xdc" # 0x9A -> LATIN CAPITAL LETTER U WITH DIAERESIS + "\xa2" # 0x9B -> CENT SIGN + "\xa3" # 0x9C -> POUND SIGN + "\xa5" # 0x9D -> YEN SIGN + "\xdf" # 0x9E -> LATIN SMALL LETTER SHARP S + "\u0192" # 0x9F -> LATIN SMALL LETTER F WITH HOOK + "\xe1" # 0xA0 -> LATIN SMALL LETTER A WITH ACUTE + "\xed" # 0xA1 -> LATIN SMALL LETTER I WITH ACUTE + "\xf3" # 0xA2 -> LATIN SMALL LETTER O WITH ACUTE + "\xfa" # 0xA3 -> LATIN SMALL LETTER U WITH ACUTE + "\xf1" # 0xA4 -> LATIN SMALL LETTER N WITH TILDE + "\xd1" # 0xA5 -> LATIN CAPITAL LETTER N WITH TILDE + "\xaa" # 0xA6 -> FEMININE ORDINAL INDICATOR + "\xba" # 0xA7 -> MASCULINE ORDINAL INDICATOR + "\xbf" # 0xA8 -> INVERTED QUESTION MARK + "\u2310" # 0xA9 -> REVERSED NOT SIGN + "\xac" # 0xAA -> NOT SIGN + "\xbd" # 0xAB -> VULGAR FRACTION ONE HALF + "\xbc" # 0xAC -> VULGAR FRACTION ONE QUARTER + "\xa1" # 0xAD -> INVERTED EXCLAMATION MARK + "\xab" # 0xAE -> LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + "\xbb" # 0xAF -> RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + "\xe3" # 0xB0 -> LATIN SMALL LETTER A WITH TILDE + "\xf5" # 0xB1 -> LATIN SMALL LETTER O WITH TILDE + "\xd8" # 0xB2 -> LATIN CAPITAL LETTER O WITH STROKE + "\xf8" # 0xB3 -> LATIN SMALL LETTER O WITH STROKE + "\u0153" # 0xB4 -> LATIN SMALL LIGATURE OE + "\u0152" # 0xB5 -> LATIN CAPITAL LIGATURE OE + "\xc0" # 0xB6 -> LATIN CAPITAL LETTER A WITH GRAVE + "\xc3" # 0xB7 -> LATIN CAPITAL LETTER A WITH TILDE + "\xd5" # 0xB8 -> LATIN CAPITAL LETTER O WITH TILDE + "\xa8" # 0xB9 -> DIAERESIS + "\xb4" # 0xBA -> ACUTE ACCENT + "\u2020" # 0xBB -> DAGGER + "\xb6" # 0xBC -> PILCROW SIGN + "\xa9" # 0xBD -> COPYRIGHT SIGN + "\xae" # 0xBE -> REGISTERED SIGN + "\u2122" # 0xBF -> TRADE MARK SIGN + "\u0133" # 0xC0 -> LATIN SMALL LIGATURE IJ + "\u0132" # 0xC1 -> LATIN CAPITAL LIGATURE IJ + "\u05d0" # 0xC2 -> HEBREW LETTER ALEF + "\u05d1" # 0xC3 -> HEBREW LETTER BET + "\u05d2" # 0xC4 -> HEBREW LETTER GIMEL + "\u05d3" # 0xC5 -> HEBREW LETTER DALET + "\u05d4" # 0xC6 -> HEBREW LETTER HE + "\u05d5" # 0xC7 -> HEBREW LETTER VAV + "\u05d6" # 0xC8 -> HEBREW LETTER ZAYIN + "\u05d7" # 0xC9 -> HEBREW LETTER HET + "\u05d8" # 0xCA -> HEBREW LETTER TET + "\u05d9" # 0xCB -> HEBREW LETTER YOD + "\u05db" # 0xCC -> HEBREW LETTER KAF + "\u05dc" # 0xCD -> HEBREW LETTER LAMED + "\u05de" # 0xCE -> HEBREW LETTER MEM + "\u05e0" # 0xCF -> HEBREW LETTER NUN + "\u05e1" # 0xD0 -> HEBREW LETTER SAMEKH + "\u05e2" # 0xD1 -> HEBREW LETTER AYIN + "\u05e4" # 0xD2 -> HEBREW LETTER PE + "\u05e6" # 0xD3 -> HEBREW LETTER TSADI + "\u05e7" # 0xD4 -> HEBREW LETTER QOF + "\u05e8" # 0xD5 -> HEBREW LETTER RESH + "\u05e9" # 0xD6 -> HEBREW LETTER SHIN + "\u05ea" # 0xD7 -> HEBREW LETTER TAV + "\u05df" # 0xD8 -> HEBREW LETTER FINAL NUN + "\u05da" # 0xD9 -> HEBREW LETTER FINAL KAF + "\u05dd" # 0xDA -> HEBREW LETTER FINAL MEM + "\u05e3" # 0xDB -> HEBREW LETTER FINAL PE + "\u05e5" # 0xDC -> HEBREW LETTER FINAL TSADI + "\xa7" # 0xDD -> SECTION SIGN + "\u2227" # 0xDE -> LOGICAL AND + "\u221e" # 0xDF -> INFINITY + "\u03b1" # 0xE0 -> GREEK SMALL LETTER ALPHA + "\u03b2" # 0xE1 -> GREEK SMALL LETTER BETA + "\u0393" # 0xE2 -> GREEK CAPITAL LETTER GAMMA + "\u03c0" # 0xE3 -> GREEK SMALL LETTER PI + "\u03a3" # 0xE4 -> GREEK CAPITAL LETTER SIGMA + "\u03c3" # 0xE5 -> GREEK SMALL LETTER SIGMA + "\xb5" # 0xE6 -> MICRO SIGN + "\u03c4" # 0xE7 -> GREEK SMALL LETTER TAU + "\u03a6" # 0xE8 -> GREEK CAPITAL LETTER PHI + "\u0398" # 0xE9 -> GREEK CAPITAL LETTER THETA + "\u03a9" # 0xEA -> GREEK CAPITAL LETTER OMEGA + "\u03b4" # 0xEB -> GREEK SMALL LETTER DELTA + "\u222e" # 0xEC -> CONTOUR INTEGRAL + "\u03c6" # 0xED -> GREEK SMALL LETTER PHI + "\u2208" # 0xEE -> ELEMENT OF SIGN + "\u2229" # 0xEF -> INTERSECTION + "\u2261" # 0xF0 -> IDENTICAL TO + "\xb1" # 0xF1 -> PLUS-MINUS SIGN + "\u2265" # 0xF2 -> GREATER-THAN OR EQUAL TO + "\u2264" # 0xF3 -> LESS-THAN OR EQUAL TO + "\u2320" # 0xF4 -> TOP HALF INTEGRAL + "\u2321" # 0xF5 -> BOTTOM HALF INTEGRAL + "\xf7" # 0xF6 -> DIVISION SIGN + "\u2248" # 0xF7 -> ALMOST EQUAL TO + "\xb0" # 0xF8 -> DEGREE SIGN + "\u2219" # 0xF9 -> BULLET OPERATOR + "\xb7" # 0xFA -> MIDDLE DOT + "\u221a" # 0xFB -> SQUARE ROOT + "\u207f" # 0xFC -> SUPERSCRIPT LATIN SMALL LETTER N + "\xb2" # 0xFD -> SUPERSCRIPT TWO + "\xb3" # 0xFE -> SUPERSCRIPT THREE + "\xaf" # 0xFF -> MACRON ) # Encoding table diff --git a/telnetlib3/encodings/atascii.py b/telnetlib3/encodings/atascii.py index 616d917d..ec024d17 100644 --- a/telnetlib3/encodings/atascii.py +++ b/telnetlib3/encodings/atascii.py @@ -33,267 +33,267 @@ DECODING_TABLE = ( # 0x00-0x1F: Graphics characters - '\u2665' # 0x00 BLACK HEART SUIT - '\u251c' # 0x01 BOX DRAWINGS LIGHT VERTICAL AND RIGHT - '\u23b9' # 0x02 RIGHT VERTICAL BOX LINE - '\u2518' # 0x03 BOX DRAWINGS LIGHT UP AND LEFT - '\u2524' # 0x04 BOX DRAWINGS LIGHT VERTICAL AND LEFT - '\u2510' # 0x05 BOX DRAWINGS LIGHT DOWN AND LEFT - '\u2571' # 0x06 BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT - '\u2572' # 0x07 BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT - '\u25e2' # 0x08 BLACK LOWER RIGHT TRIANGLE - '\u2597' # 0x09 QUADRANT LOWER RIGHT - '\u25e3' # 0x0A BLACK LOWER LEFT TRIANGLE - '\u259d' # 0x0B QUADRANT UPPER RIGHT - '\u2598' # 0x0C QUADRANT UPPER LEFT - '\U0001fb82' # 0x0D UPPER ONE QUARTER BLOCK - '\u2582' # 0x0E LOWER ONE QUARTER BLOCK - '\u2596' # 0x0F QUADRANT LOWER LEFT - '\u2663' # 0x10 BLACK CLUB SUIT - '\u250c' # 0x11 BOX DRAWINGS LIGHT DOWN AND RIGHT - '\u2500' # 0x12 BOX DRAWINGS LIGHT HORIZONTAL - '\u253c' # 0x13 BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL - '\u25cf' # 0x14 BLACK CIRCLE - '\u2584' # 0x15 LOWER HALF BLOCK - '\u258e' # 0x16 LEFT ONE QUARTER BLOCK - '\u252c' # 0x17 BOX DRAWINGS LIGHT DOWN AND HORIZONTAL - '\u2534' # 0x18 BOX DRAWINGS LIGHT UP AND HORIZONTAL - '\u258c' # 0x19 LEFT HALF BLOCK - '\u2514' # 0x1A BOX DRAWINGS LIGHT UP AND RIGHT - '\u241b' # 0x1B SYMBOL FOR ESCAPE - '\u2191' # 0x1C UPWARDS ARROW (cursor up) - '\u2193' # 0x1D DOWNWARDS ARROW (cursor down) - '\u2190' # 0x1E LEFTWARDS ARROW (cursor left) - '\u2192' # 0x1F RIGHTWARDS ARROW (cursor right) + "\u2665" # 0x00 BLACK HEART SUIT + "\u251c" # 0x01 BOX DRAWINGS LIGHT VERTICAL AND RIGHT + "\u23b9" # 0x02 RIGHT VERTICAL BOX LINE + "\u2518" # 0x03 BOX DRAWINGS LIGHT UP AND LEFT + "\u2524" # 0x04 BOX DRAWINGS LIGHT VERTICAL AND LEFT + "\u2510" # 0x05 BOX DRAWINGS LIGHT DOWN AND LEFT + "\u2571" # 0x06 BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT + "\u2572" # 0x07 BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT + "\u25e2" # 0x08 BLACK LOWER RIGHT TRIANGLE + "\u2597" # 0x09 QUADRANT LOWER RIGHT + "\u25e3" # 0x0A BLACK LOWER LEFT TRIANGLE + "\u259d" # 0x0B QUADRANT UPPER RIGHT + "\u2598" # 0x0C QUADRANT UPPER LEFT + "\U0001fb82" # 0x0D UPPER ONE QUARTER BLOCK + "\u2582" # 0x0E LOWER ONE QUARTER BLOCK + "\u2596" # 0x0F QUADRANT LOWER LEFT + "\u2663" # 0x10 BLACK CLUB SUIT + "\u250c" # 0x11 BOX DRAWINGS LIGHT DOWN AND RIGHT + "\u2500" # 0x12 BOX DRAWINGS LIGHT HORIZONTAL + "\u253c" # 0x13 BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL + "\u25cf" # 0x14 BLACK CIRCLE + "\u2584" # 0x15 LOWER HALF BLOCK + "\u258e" # 0x16 LEFT ONE QUARTER BLOCK + "\u252c" # 0x17 BOX DRAWINGS LIGHT DOWN AND HORIZONTAL + "\u2534" # 0x18 BOX DRAWINGS LIGHT UP AND HORIZONTAL + "\u258c" # 0x19 LEFT HALF BLOCK + "\u2514" # 0x1A BOX DRAWINGS LIGHT UP AND RIGHT + "\u241b" # 0x1B SYMBOL FOR ESCAPE + "\u2191" # 0x1C UPWARDS ARROW (cursor up) + "\u2193" # 0x1D DOWNWARDS ARROW (cursor down) + "\u2190" # 0x1E LEFTWARDS ARROW (cursor left) + "\u2192" # 0x1F RIGHTWARDS ARROW (cursor right) # 0x20-0x5F: Standard ASCII - ' ' # 0x20 SPACE - '!' # 0x21 - '"' # 0x22 - '#' # 0x23 - '$' # 0x24 - '%' # 0x25 - '&' # 0x26 - "'" # 0x27 - '(' # 0x28 - ')' # 0x29 - '*' # 0x2A - '+' # 0x2B - ',' # 0x2C - '-' # 0x2D - '.' # 0x2E - '/' # 0x2F - '0' # 0x30 - '1' # 0x31 - '2' # 0x32 - '3' # 0x33 - '4' # 0x34 - '5' # 0x35 - '6' # 0x36 - '7' # 0x37 - '8' # 0x38 - '9' # 0x39 - ':' # 0x3A - ';' # 0x3B - '<' # 0x3C - '=' # 0x3D - '>' # 0x3E - '?' # 0x3F - '@' # 0x40 - 'A' # 0x41 - 'B' # 0x42 - 'C' # 0x43 - 'D' # 0x44 - 'E' # 0x45 - 'F' # 0x46 - 'G' # 0x47 - 'H' # 0x48 - 'I' # 0x49 - 'J' # 0x4A - 'K' # 0x4B - 'L' # 0x4C - 'M' # 0x4D - 'N' # 0x4E - 'O' # 0x4F - 'P' # 0x50 - 'Q' # 0x51 - 'R' # 0x52 - 'S' # 0x53 - 'T' # 0x54 - 'U' # 0x55 - 'V' # 0x56 - 'W' # 0x57 - 'X' # 0x58 - 'Y' # 0x59 - 'Z' # 0x5A - '[' # 0x5B - '\\' # 0x5C - ']' # 0x5D - '^' # 0x5E - '_' # 0x5F + " " # 0x20 SPACE + "!" # 0x21 + '"' # 0x22 + "#" # 0x23 + "$" # 0x24 + "%" # 0x25 + "&" # 0x26 + "'" # 0x27 + "(" # 0x28 + ")" # 0x29 + "*" # 0x2A + "+" # 0x2B + "," # 0x2C + "-" # 0x2D + "." # 0x2E + "/" # 0x2F + "0" # 0x30 + "1" # 0x31 + "2" # 0x32 + "3" # 0x33 + "4" # 0x34 + "5" # 0x35 + "6" # 0x36 + "7" # 0x37 + "8" # 0x38 + "9" # 0x39 + ":" # 0x3A + ";" # 0x3B + "<" # 0x3C + "=" # 0x3D + ">" # 0x3E + "?" # 0x3F + "@" # 0x40 + "A" # 0x41 + "B" # 0x42 + "C" # 0x43 + "D" # 0x44 + "E" # 0x45 + "F" # 0x46 + "G" # 0x47 + "H" # 0x48 + "I" # 0x49 + "J" # 0x4A + "K" # 0x4B + "L" # 0x4C + "M" # 0x4D + "N" # 0x4E + "O" # 0x4F + "P" # 0x50 + "Q" # 0x51 + "R" # 0x52 + "S" # 0x53 + "T" # 0x54 + "U" # 0x55 + "V" # 0x56 + "W" # 0x57 + "X" # 0x58 + "Y" # 0x59 + "Z" # 0x5A + "[" # 0x5B + "\\" # 0x5C + "]" # 0x5D + "^" # 0x5E + "_" # 0x5F # 0x60-0x7F: Lowercase + special glyphs - '\u2666' # 0x60 BLACK DIAMOND SUIT - 'a' # 0x61 - 'b' # 0x62 - 'c' # 0x63 - 'd' # 0x64 - 'e' # 0x65 - 'f' # 0x66 - 'g' # 0x67 - 'h' # 0x68 - 'i' # 0x69 - 'j' # 0x6A - 'k' # 0x6B - 'l' # 0x6C - 'm' # 0x6D - 'n' # 0x6E - 'o' # 0x6F - 'p' # 0x70 - 'q' # 0x71 - 'r' # 0x72 - 's' # 0x73 - 't' # 0x74 - 'u' # 0x75 - 'v' # 0x76 - 'w' # 0x77 - 'x' # 0x78 - 'y' # 0x79 - 'z' # 0x7A - '\u2660' # 0x7B BLACK SPADE SUIT - '|' # 0x7C VERTICAL LINE - '\u21b0' # 0x7D UPWARDS ARROW WITH TIP LEFTWARDS (clear screen) - '\u25c0' # 0x7E BLACK LEFT-POINTING TRIANGLE (backspace) - '\u25b6' # 0x7F BLACK RIGHT-POINTING TRIANGLE (tab) + "\u2666" # 0x60 BLACK DIAMOND SUIT + "a" # 0x61 + "b" # 0x62 + "c" # 0x63 + "d" # 0x64 + "e" # 0x65 + "f" # 0x66 + "g" # 0x67 + "h" # 0x68 + "i" # 0x69 + "j" # 0x6A + "k" # 0x6B + "l" # 0x6C + "m" # 0x6D + "n" # 0x6E + "o" # 0x6F + "p" # 0x70 + "q" # 0x71 + "r" # 0x72 + "s" # 0x73 + "t" # 0x74 + "u" # 0x75 + "v" # 0x76 + "w" # 0x77 + "x" # 0x78 + "y" # 0x79 + "z" # 0x7A + "\u2660" # 0x7B BLACK SPADE SUIT + "|" # 0x7C VERTICAL LINE + "\u21b0" # 0x7D UPWARDS ARROW WITH TIP LEFTWARDS (clear screen) + "\u25c0" # 0x7E BLACK LEFT-POINTING TRIANGLE (backspace) + "\u25b6" # 0x7F BLACK RIGHT-POINTING TRIANGLE (tab) # 0x80-0xFF: Inverse video range # Bytes with distinct glyphs get their own Unicode mapping; # the rest share the same character as (byte & 0x7F). - '\u2665' # 0x80 = inverse of 0x00 (heart) - '\u251c' # 0x81 = inverse of 0x01 - '\u258a' # 0x82 LEFT THREE QUARTERS BLOCK (distinct) - '\u2518' # 0x83 = inverse of 0x03 - '\u2524' # 0x84 = inverse of 0x04 - '\u2510' # 0x85 = inverse of 0x05 - '\u2571' # 0x86 = inverse of 0x06 - '\u2572' # 0x87 = inverse of 0x07 - '\u25e4' # 0x88 BLACK UPPER LEFT TRIANGLE (distinct) - '\u259b' # 0x89 QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT (distinct) - '\u25e5' # 0x8A BLACK UPPER RIGHT TRIANGLE (distinct) - '\u2599' # 0x8B QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT (distinct) - '\u259f' # 0x8C QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT (distinct) - '\u2586' # 0x8D LOWER THREE QUARTERS BLOCK (distinct) - '\U0001fb85' # 0x8E UPPER THREE QUARTERS BLOCK (distinct) - '\u259c' # 0x8F QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT (distinct) - '\u2663' # 0x90 = inverse of 0x10 (club) - '\u250c' # 0x91 = inverse of 0x11 - '\u2500' # 0x92 = inverse of 0x12 - '\u253c' # 0x93 = inverse of 0x13 - '\u25d8' # 0x94 INVERSE BULLET (distinct) - '\u2580' # 0x95 UPPER HALF BLOCK (distinct) - '\U0001fb8a' # 0x96 RIGHT THREE QUARTERS BLOCK (distinct) - '\u252c' # 0x97 = inverse of 0x17 - '\u2534' # 0x98 = inverse of 0x18 - '\u2590' # 0x99 RIGHT HALF BLOCK (distinct) - '\u2514' # 0x9A = inverse of 0x1A - '\n' # 0x9B ATASCII END OF LINE - '\u2191' # 0x9C = inverse of 0x1C (up arrow) - '\u2193' # 0x9D = inverse of 0x1D (down arrow) - '\u2190' # 0x9E = inverse of 0x1E (left arrow) - '\u2192' # 0x9F = inverse of 0x1F (right arrow) - '\u2588' # 0xA0 FULL BLOCK (distinct) - '!' # 0xA1 = inverse of 0x21 - '"' # 0xA2 = inverse of 0x22 - '#' # 0xA3 = inverse of 0x23 - '$' # 0xA4 = inverse of 0x24 - '%' # 0xA5 = inverse of 0x25 - '&' # 0xA6 = inverse of 0x26 - "'" # 0xA7 = inverse of 0x27 - '(' # 0xA8 = inverse of 0x28 - ')' # 0xA9 = inverse of 0x29 - '*' # 0xAA = inverse of 0x2A - '+' # 0xAB = inverse of 0x2B - ',' # 0xAC = inverse of 0x2C - '-' # 0xAD = inverse of 0x2D - '.' # 0xAE = inverse of 0x2E - '/' # 0xAF = inverse of 0x2F - '0' # 0xB0 = inverse of 0x30 - '1' # 0xB1 = inverse of 0x31 - '2' # 0xB2 = inverse of 0x32 - '3' # 0xB3 = inverse of 0x33 - '4' # 0xB4 = inverse of 0x34 - '5' # 0xB5 = inverse of 0x35 - '6' # 0xB6 = inverse of 0x36 - '7' # 0xB7 = inverse of 0x37 - '8' # 0xB8 = inverse of 0x38 - '9' # 0xB9 = inverse of 0x39 - ':' # 0xBA = inverse of 0x3A - ';' # 0xBB = inverse of 0x3B - '<' # 0xBC = inverse of 0x3C - '=' # 0xBD = inverse of 0x3D - '>' # 0xBE = inverse of 0x3E - '?' # 0xBF = inverse of 0x3F - '@' # 0xC0 = inverse of 0x40 - 'A' # 0xC1 = inverse of 0x41 - 'B' # 0xC2 = inverse of 0x42 - 'C' # 0xC3 = inverse of 0x43 - 'D' # 0xC4 = inverse of 0x44 - 'E' # 0xC5 = inverse of 0x45 - 'F' # 0xC6 = inverse of 0x46 - 'G' # 0xC7 = inverse of 0x47 - 'H' # 0xC8 = inverse of 0x48 - 'I' # 0xC9 = inverse of 0x49 - 'J' # 0xCA = inverse of 0x4A - 'K' # 0xCB = inverse of 0x4B - 'L' # 0xCC = inverse of 0x4C - 'M' # 0xCD = inverse of 0x4D - 'N' # 0xCE = inverse of 0x4E - 'O' # 0xCF = inverse of 0x4F - 'P' # 0xD0 = inverse of 0x50 - 'Q' # 0xD1 = inverse of 0x51 - 'R' # 0xD2 = inverse of 0x52 - 'S' # 0xD3 = inverse of 0x53 - 'T' # 0xD4 = inverse of 0x54 - 'U' # 0xD5 = inverse of 0x55 - 'V' # 0xD6 = inverse of 0x56 - 'W' # 0xD7 = inverse of 0x57 - 'X' # 0xD8 = inverse of 0x58 - 'Y' # 0xD9 = inverse of 0x59 - 'Z' # 0xDA = inverse of 0x5A - '[' # 0xDB = inverse of 0x5B - '\\' # 0xDC = inverse of 0x5C - ']' # 0xDD = inverse of 0x5D - '^' # 0xDE = inverse of 0x5E - '_' # 0xDF = inverse of 0x5F - '\u2666' # 0xE0 = inverse of 0x60 (diamond) - 'a' # 0xE1 = inverse of 0x61 - 'b' # 0xE2 = inverse of 0x62 - 'c' # 0xE3 = inverse of 0x63 - 'd' # 0xE4 = inverse of 0x64 - 'e' # 0xE5 = inverse of 0x65 - 'f' # 0xE6 = inverse of 0x66 - 'g' # 0xE7 = inverse of 0x67 - 'h' # 0xE8 = inverse of 0x68 - 'i' # 0xE9 = inverse of 0x69 - 'j' # 0xEA = inverse of 0x6A - 'k' # 0xEB = inverse of 0x6B - 'l' # 0xEC = inverse of 0x6C - 'm' # 0xED = inverse of 0x6D - 'n' # 0xEE = inverse of 0x6E - 'o' # 0xEF = inverse of 0x6F - 'p' # 0xF0 = inverse of 0x70 - 'q' # 0xF1 = inverse of 0x71 - 'r' # 0xF2 = inverse of 0x72 - 's' # 0xF3 = inverse of 0x73 - 't' # 0xF4 = inverse of 0x74 - 'u' # 0xF5 = inverse of 0x75 - 'v' # 0xF6 = inverse of 0x76 - 'w' # 0xF7 = inverse of 0x77 - 'x' # 0xF8 = inverse of 0x78 - 'y' # 0xF9 = inverse of 0x79 - 'z' # 0xFA = inverse of 0x7A - '\u2660' # 0xFB = inverse of 0x7B (spade) - '|' # 0xFC = inverse of 0x7C - '\u21b0' # 0xFD = inverse of 0x7D (clear screen) - '\u25c0' # 0xFE = inverse of 0x7E (backspace) - '\u25b6' # 0xFF = inverse of 0x7F (tab) + "\u2665" # 0x80 = inverse of 0x00 (heart) + "\u251c" # 0x81 = inverse of 0x01 + "\u258a" # 0x82 LEFT THREE QUARTERS BLOCK (distinct) + "\u2518" # 0x83 = inverse of 0x03 + "\u2524" # 0x84 = inverse of 0x04 + "\u2510" # 0x85 = inverse of 0x05 + "\u2571" # 0x86 = inverse of 0x06 + "\u2572" # 0x87 = inverse of 0x07 + "\u25e4" # 0x88 BLACK UPPER LEFT TRIANGLE (distinct) + "\u259b" # 0x89 QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT (distinct) + "\u25e5" # 0x8A BLACK UPPER RIGHT TRIANGLE (distinct) + "\u2599" # 0x8B QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT (distinct) + "\u259f" # 0x8C QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT (distinct) + "\u2586" # 0x8D LOWER THREE QUARTERS BLOCK (distinct) + "\U0001fb85" # 0x8E UPPER THREE QUARTERS BLOCK (distinct) + "\u259c" # 0x8F QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT (distinct) + "\u2663" # 0x90 = inverse of 0x10 (club) + "\u250c" # 0x91 = inverse of 0x11 + "\u2500" # 0x92 = inverse of 0x12 + "\u253c" # 0x93 = inverse of 0x13 + "\u25d8" # 0x94 INVERSE BULLET (distinct) + "\u2580" # 0x95 UPPER HALF BLOCK (distinct) + "\U0001fb8a" # 0x96 RIGHT THREE QUARTERS BLOCK (distinct) + "\u252c" # 0x97 = inverse of 0x17 + "\u2534" # 0x98 = inverse of 0x18 + "\u2590" # 0x99 RIGHT HALF BLOCK (distinct) + "\u2514" # 0x9A = inverse of 0x1A + "\n" # 0x9B ATASCII END OF LINE + "\u2191" # 0x9C = inverse of 0x1C (up arrow) + "\u2193" # 0x9D = inverse of 0x1D (down arrow) + "\u2190" # 0x9E = inverse of 0x1E (left arrow) + "\u2192" # 0x9F = inverse of 0x1F (right arrow) + "\u2588" # 0xA0 FULL BLOCK (distinct) + "!" # 0xA1 = inverse of 0x21 + '"' # 0xA2 = inverse of 0x22 + "#" # 0xA3 = inverse of 0x23 + "$" # 0xA4 = inverse of 0x24 + "%" # 0xA5 = inverse of 0x25 + "&" # 0xA6 = inverse of 0x26 + "'" # 0xA7 = inverse of 0x27 + "(" # 0xA8 = inverse of 0x28 + ")" # 0xA9 = inverse of 0x29 + "*" # 0xAA = inverse of 0x2A + "+" # 0xAB = inverse of 0x2B + "," # 0xAC = inverse of 0x2C + "-" # 0xAD = inverse of 0x2D + "." # 0xAE = inverse of 0x2E + "/" # 0xAF = inverse of 0x2F + "0" # 0xB0 = inverse of 0x30 + "1" # 0xB1 = inverse of 0x31 + "2" # 0xB2 = inverse of 0x32 + "3" # 0xB3 = inverse of 0x33 + "4" # 0xB4 = inverse of 0x34 + "5" # 0xB5 = inverse of 0x35 + "6" # 0xB6 = inverse of 0x36 + "7" # 0xB7 = inverse of 0x37 + "8" # 0xB8 = inverse of 0x38 + "9" # 0xB9 = inverse of 0x39 + ":" # 0xBA = inverse of 0x3A + ";" # 0xBB = inverse of 0x3B + "<" # 0xBC = inverse of 0x3C + "=" # 0xBD = inverse of 0x3D + ">" # 0xBE = inverse of 0x3E + "?" # 0xBF = inverse of 0x3F + "@" # 0xC0 = inverse of 0x40 + "A" # 0xC1 = inverse of 0x41 + "B" # 0xC2 = inverse of 0x42 + "C" # 0xC3 = inverse of 0x43 + "D" # 0xC4 = inverse of 0x44 + "E" # 0xC5 = inverse of 0x45 + "F" # 0xC6 = inverse of 0x46 + "G" # 0xC7 = inverse of 0x47 + "H" # 0xC8 = inverse of 0x48 + "I" # 0xC9 = inverse of 0x49 + "J" # 0xCA = inverse of 0x4A + "K" # 0xCB = inverse of 0x4B + "L" # 0xCC = inverse of 0x4C + "M" # 0xCD = inverse of 0x4D + "N" # 0xCE = inverse of 0x4E + "O" # 0xCF = inverse of 0x4F + "P" # 0xD0 = inverse of 0x50 + "Q" # 0xD1 = inverse of 0x51 + "R" # 0xD2 = inverse of 0x52 + "S" # 0xD3 = inverse of 0x53 + "T" # 0xD4 = inverse of 0x54 + "U" # 0xD5 = inverse of 0x55 + "V" # 0xD6 = inverse of 0x56 + "W" # 0xD7 = inverse of 0x57 + "X" # 0xD8 = inverse of 0x58 + "Y" # 0xD9 = inverse of 0x59 + "Z" # 0xDA = inverse of 0x5A + "[" # 0xDB = inverse of 0x5B + "\\" # 0xDC = inverse of 0x5C + "]" # 0xDD = inverse of 0x5D + "^" # 0xDE = inverse of 0x5E + "_" # 0xDF = inverse of 0x5F + "\u2666" # 0xE0 = inverse of 0x60 (diamond) + "a" # 0xE1 = inverse of 0x61 + "b" # 0xE2 = inverse of 0x62 + "c" # 0xE3 = inverse of 0x63 + "d" # 0xE4 = inverse of 0x64 + "e" # 0xE5 = inverse of 0x65 + "f" # 0xE6 = inverse of 0x66 + "g" # 0xE7 = inverse of 0x67 + "h" # 0xE8 = inverse of 0x68 + "i" # 0xE9 = inverse of 0x69 + "j" # 0xEA = inverse of 0x6A + "k" # 0xEB = inverse of 0x6B + "l" # 0xEC = inverse of 0x6C + "m" # 0xED = inverse of 0x6D + "n" # 0xEE = inverse of 0x6E + "o" # 0xEF = inverse of 0x6F + "p" # 0xF0 = inverse of 0x70 + "q" # 0xF1 = inverse of 0x71 + "r" # 0xF2 = inverse of 0x72 + "s" # 0xF3 = inverse of 0x73 + "t" # 0xF4 = inverse of 0x74 + "u" # 0xF5 = inverse of 0x75 + "v" # 0xF6 = inverse of 0x76 + "w" # 0xF7 = inverse of 0x77 + "x" # 0xF8 = inverse of 0x78 + "y" # 0xF9 = inverse of 0x79 + "z" # 0xFA = inverse of 0x7A + "\u2660" # 0xFB = inverse of 0x7B (spade) + "|" # 0xFC = inverse of 0x7C + "\u21b0" # 0xFD = inverse of 0x7D (clear screen) + "\u25c0" # 0xFE = inverse of 0x7E (backspace) + "\u25b6" # 0xFF = inverse of 0x7F (tab) ) assert len(DECODING_TABLE) == 256 @@ -307,44 +307,40 @@ def _normalize_eol(text: str) -> str: (U+000D) has no native ATASCII representation (byte 0x0D is a graphics character), so CR and CRLF are both folded to LF before charmap encoding. """ - return text.replace('\r\n', '\n').replace('\r', '\n') + return text.replace("\r\n", "\n").replace("\r", "\n") class Codec(codecs.Codec): """ATASCII character map codec.""" def encode( # pylint: disable=redefined-builtin - self, input: str, errors: str = 'strict' + self, input: str, errors: str = "strict" ) -> Tuple[bytes, int]: """Encode input string using ATASCII character map.""" input = _normalize_eol(input) return codecs.charmap_encode(input, errors, ENCODING_TABLE) def decode( # pylint: disable=redefined-builtin - self, input: bytes, errors: str = 'strict' + self, input: bytes, errors: str = "strict" ) -> Tuple[str, int]: """Decode input bytes using ATASCII character map.""" - return codecs.charmap_decode( - input, errors, DECODING_TABLE # type: ignore[arg-type] - ) + return codecs.charmap_decode(input, errors, DECODING_TABLE) # type: ignore[arg-type] class IncrementalEncoder(codecs.IncrementalEncoder): """ATASCII incremental encoder with CR/CRLF → LF normalization.""" - def __init__(self, errors: str = 'strict') -> None: + def __init__(self, errors: str = "strict") -> None: """Initialize encoder with pending CR state.""" super().__init__(errors) self._pending_cr = False - def encode( # pylint: disable=redefined-builtin - self, input: str, final: bool = False - ) -> bytes: + def encode(self, input: str, final: bool = False) -> bytes: # pylint: disable=redefined-builtin """Encode input string incrementally.""" if self._pending_cr: - input = '\r' + input + input = "\r" + input self._pending_cr = False - if not final and input.endswith('\r'): + if not final and input.endswith("\r"): input = input[:-1] self._pending_cr = True input = _normalize_eol(input) @@ -370,9 +366,9 @@ def decode( # type: ignore[override] # pylint: disable=redefined-builtin self, input: bytes, final: bool = False ) -> str: """Decode input bytes incrementally.""" - return codecs.charmap_decode( - input, self.errors, DECODING_TABLE # type: ignore[arg-type] - )[0] + return codecs.charmap_decode(input, self.errors, DECODING_TABLE)[ # type: ignore[arg-type] + 0 + ] class StreamWriter(Codec, codecs.StreamWriter): @@ -386,7 +382,7 @@ class StreamReader(Codec, codecs.StreamReader): def getregentry() -> codecs.CodecInfo: """Return the codec registry entry.""" return codecs.CodecInfo( - name='atascii', + name="atascii", encode=Codec().encode, decode=Codec().decode, # type: ignore[arg-type] incrementalencoder=IncrementalEncoder, @@ -398,7 +394,7 @@ def getregentry() -> codecs.CodecInfo: def getaliases() -> Tuple[str, ...]: """Return codec aliases.""" - return ('atari8bit', 'atari_8bit') + return ("atari8bit", "atari_8bit") # Build encoding table preferring normal bytes (0x00-0x7F) over inverse @@ -407,11 +403,11 @@ def getaliases() -> Tuple[str, ...]: # overwriting with normal-range entries so they take priority. # Exception: '\n' must still encode to 0x9B (ATASCII EOL), not 0x0A. def _build_encoding_table() -> Dict[int, int]: - table: Dict[int, int] = codecs.charmap_build(DECODING_TABLE) + table: Dict[int, int] = codecs.charmap_build(DECODING_TABLE) # type: ignore[assignment] for byte_val in range(0x80): table[ord(DECODING_TABLE[byte_val])] = byte_val # '\n' at 0x0A must encode to 0x9B (ATASCII EOL), not 0x0A - table[ord('\n')] = 0x9B + table[ord("\n")] = 0x9B return table diff --git a/telnetlib3/encodings/petscii.py b/telnetlib3/encodings/petscii.py index 68f11edf..f7c4dcf5 100644 --- a/telnetlib3/encodings/petscii.py +++ b/telnetlib3/encodings/petscii.py @@ -34,269 +34,269 @@ DECODING_TABLE = ( # 0x00-0x1F: Control codes - '\x00' # 0x00 NUL - '\x01' # 0x01 (unused) - '\x02' # 0x02 (unused) - '\x03' # 0x03 RUN/STOP - '\x04' # 0x04 (unused) - '\x05' # 0x05 WHT (white) - '\x06' # 0x06 (unused) - '\x07' # 0x07 BEL - '\x08' # 0x08 shift-disable - '\x09' # 0x09 shift-enable - '\n' # 0x0A LF - '\x0b' # 0x0B (unused) - '\x0c' # 0x0C (unused) - '\r' # 0x0D RETURN - '\x0e' # 0x0E lowercase charset - '\x0f' # 0x0F (unused) - '\x10' # 0x10 (unused) - '\x11' # 0x11 cursor down - '\x12' # 0x12 RVS ON - '\x13' # 0x13 HOME - '\x14' # 0x14 DEL - '\x15' # 0x15 (unused) - '\x16' # 0x16 (unused) - '\x17' # 0x17 (unused) - '\x18' # 0x18 (unused) - '\x19' # 0x19 (unused) - '\x1a' # 0x1A (unused) - '\x1b' # 0x1B ESC - '\x1c' # 0x1C RED - '\x1d' # 0x1D cursor right - '\x1e' # 0x1E GRN - '\x1f' # 0x1F BLU + "\x00" # 0x00 NUL + "\x01" # 0x01 (unused) + "\x02" # 0x02 (unused) + "\x03" # 0x03 RUN/STOP + "\x04" # 0x04 (unused) + "\x05" # 0x05 WHT (white) + "\x06" # 0x06 (unused) + "\x07" # 0x07 BEL + "\x08" # 0x08 shift-disable + "\x09" # 0x09 shift-enable + "\n" # 0x0A LF + "\x0b" # 0x0B (unused) + "\x0c" # 0x0C (unused) + "\r" # 0x0D RETURN + "\x0e" # 0x0E lowercase charset + "\x0f" # 0x0F (unused) + "\x10" # 0x10 (unused) + "\x11" # 0x11 cursor down + "\x12" # 0x12 RVS ON + "\x13" # 0x13 HOME + "\x14" # 0x14 DEL + "\x15" # 0x15 (unused) + "\x16" # 0x16 (unused) + "\x17" # 0x17 (unused) + "\x18" # 0x18 (unused) + "\x19" # 0x19 (unused) + "\x1a" # 0x1A (unused) + "\x1b" # 0x1B ESC + "\x1c" # 0x1C RED + "\x1d" # 0x1D cursor right + "\x1e" # 0x1E GRN + "\x1f" # 0x1F BLU # 0x20-0x3F: ASCII compatible - ' ' # 0x20 SPACE - '!' # 0x21 - '"' # 0x22 - '#' # 0x23 - '$' # 0x24 - '%' # 0x25 - '&' # 0x26 - "'" # 0x27 - '(' # 0x28 - ')' # 0x29 - '*' # 0x2A - '+' # 0x2B - ',' # 0x2C - '-' # 0x2D - '.' # 0x2E - '/' # 0x2F - '0' # 0x30 - '1' # 0x31 - '2' # 0x32 - '3' # 0x33 - '4' # 0x34 - '5' # 0x35 - '6' # 0x36 - '7' # 0x37 - '8' # 0x38 - '9' # 0x39 - ':' # 0x3A - ';' # 0x3B - '<' # 0x3C - '=' # 0x3D - '>' # 0x3E - '?' # 0x3F + " " # 0x20 SPACE + "!" # 0x21 + '"' # 0x22 + "#" # 0x23 + "$" # 0x24 + "%" # 0x25 + "&" # 0x26 + "'" # 0x27 + "(" # 0x28 + ")" # 0x29 + "*" # 0x2A + "+" # 0x2B + "," # 0x2C + "-" # 0x2D + "." # 0x2E + "/" # 0x2F + "0" # 0x30 + "1" # 0x31 + "2" # 0x32 + "3" # 0x33 + "4" # 0x34 + "5" # 0x35 + "6" # 0x36 + "7" # 0x37 + "8" # 0x38 + "9" # 0x39 + ":" # 0x3A + ";" # 0x3B + "<" # 0x3C + "=" # 0x3D + ">" # 0x3E + "?" # 0x3F # 0x40-0x5F: Letters and symbols - '@' # 0x40 - 'a' # 0x41 lowercase (PETSCII shifted mode) - 'b' # 0x42 - 'c' # 0x43 - 'd' # 0x44 - 'e' # 0x45 - 'f' # 0x46 - 'g' # 0x47 - 'h' # 0x48 - 'i' # 0x49 - 'j' # 0x4A - 'k' # 0x4B - 'l' # 0x4C - 'm' # 0x4D - 'n' # 0x4E - 'o' # 0x4F - 'p' # 0x50 - 'q' # 0x51 - 'r' # 0x52 - 's' # 0x53 - 't' # 0x54 - 'u' # 0x55 - 'v' # 0x56 - 'w' # 0x57 - 'x' # 0x58 - 'y' # 0x59 - 'z' # 0x5A - '[' # 0x5B - '\u00a3' # 0x5C POUND SIGN - ']' # 0x5D - '\u2191' # 0x5E UP ARROW - '\u2190' # 0x5F LEFT ARROW + "@" # 0x40 + "a" # 0x41 lowercase (PETSCII shifted mode) + "b" # 0x42 + "c" # 0x43 + "d" # 0x44 + "e" # 0x45 + "f" # 0x46 + "g" # 0x47 + "h" # 0x48 + "i" # 0x49 + "j" # 0x4A + "k" # 0x4B + "l" # 0x4C + "m" # 0x4D + "n" # 0x4E + "o" # 0x4F + "p" # 0x50 + "q" # 0x51 + "r" # 0x52 + "s" # 0x53 + "t" # 0x54 + "u" # 0x55 + "v" # 0x56 + "w" # 0x57 + "x" # 0x58 + "y" # 0x59 + "z" # 0x5A + "[" # 0x5B + "\u00a3" # 0x5C POUND SIGN + "]" # 0x5D + "\u2191" # 0x5E UP ARROW + "\u2190" # 0x5F LEFT ARROW # 0x60-0x7F: Graphics characters (shifted mode) - '\u2500' # 0x60 HORIZONTAL LINE - '\u2660' # 0x61 BLACK SPADE SUIT - '\u2502' # 0x62 VERTICAL LINE - '\u2500' # 0x63 HORIZONTAL LINE - '\u2597' # 0x64 QUADRANT LOWER RIGHT - '\u2596' # 0x65 QUADRANT LOWER LEFT - '\u2598' # 0x66 QUADRANT UPPER LEFT - '\u259d' # 0x67 QUADRANT UPPER RIGHT - '\u2599' # 0x68 QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT - '\u259f' # 0x69 QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT - '\u259e' # 0x6A QUADRANT UPPER RIGHT AND LOWER LEFT - '\u2595' # 0x6B RIGHT ONE EIGHTH BLOCK - '\u258f' # 0x6C LEFT ONE EIGHTH BLOCK - '\u2584' # 0x6D LOWER HALF BLOCK - '\u2580' # 0x6E UPPER HALF BLOCK - '\u2588' # 0x6F FULL BLOCK - '\u2584' # 0x70 LOWER HALF BLOCK (variant) - '\u259b' # 0x71 QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT - '\u2583' # 0x72 LOWER THREE EIGHTHS BLOCK - '\u2665' # 0x73 BLACK HEART SUIT - '\u259c' # 0x74 QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT - '\u256d' # 0x75 BOX DRAWINGS LIGHT ARC DOWN AND RIGHT - '\u2573' # 0x76 BOX DRAWINGS LIGHT DIAGONAL CROSS - '\u25cb' # 0x77 WHITE CIRCLE - '\u2663' # 0x78 BLACK CLUB SUIT - '\u259a' # 0x79 QUADRANT UPPER LEFT AND LOWER RIGHT - '\u2666' # 0x7A BLACK DIAMOND SUIT - '\u253c' # 0x7B BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL - '\u2502' # 0x7C VERTICAL LINE (with serif, approx) - '\u2571' # 0x7D BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT - '\u03c0' # 0x7E GREEK SMALL LETTER PI - '\u25e5' # 0x7F BLACK UPPER RIGHT TRIANGLE + "\u2500" # 0x60 HORIZONTAL LINE + "\u2660" # 0x61 BLACK SPADE SUIT + "\u2502" # 0x62 VERTICAL LINE + "\u2500" # 0x63 HORIZONTAL LINE + "\u2597" # 0x64 QUADRANT LOWER RIGHT + "\u2596" # 0x65 QUADRANT LOWER LEFT + "\u2598" # 0x66 QUADRANT UPPER LEFT + "\u259d" # 0x67 QUADRANT UPPER RIGHT + "\u2599" # 0x68 QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT + "\u259f" # 0x69 QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT + "\u259e" # 0x6A QUADRANT UPPER RIGHT AND LOWER LEFT + "\u2595" # 0x6B RIGHT ONE EIGHTH BLOCK + "\u258f" # 0x6C LEFT ONE EIGHTH BLOCK + "\u2584" # 0x6D LOWER HALF BLOCK + "\u2580" # 0x6E UPPER HALF BLOCK + "\u2588" # 0x6F FULL BLOCK + "\u2584" # 0x70 LOWER HALF BLOCK (variant) + "\u259b" # 0x71 QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT + "\u2583" # 0x72 LOWER THREE EIGHTHS BLOCK + "\u2665" # 0x73 BLACK HEART SUIT + "\u259c" # 0x74 QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT + "\u256d" # 0x75 BOX DRAWINGS LIGHT ARC DOWN AND RIGHT + "\u2573" # 0x76 BOX DRAWINGS LIGHT DIAGONAL CROSS + "\u25cb" # 0x77 WHITE CIRCLE + "\u2663" # 0x78 BLACK CLUB SUIT + "\u259a" # 0x79 QUADRANT UPPER LEFT AND LOWER RIGHT + "\u2666" # 0x7A BLACK DIAMOND SUIT + "\u253c" # 0x7B BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL + "\u2502" # 0x7C VERTICAL LINE (with serif, approx) + "\u2571" # 0x7D BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT + "\u03c0" # 0x7E GREEK SMALL LETTER PI + "\u25e5" # 0x7F BLACK UPPER RIGHT TRIANGLE # 0x80-0x9F: Control codes (colors, function keys, cursor) - '\x80' # 0x80 (unused) - '\x81' # 0x81 ORN (orange) - '\x82' # 0x82 (unused) - '\x83' # 0x83 (unused) - '\x84' # 0x84 (unused) - '\x85' # 0x85 F1 - '\x86' # 0x86 F3 - '\x87' # 0x87 F5 - '\x88' # 0x88 F7 - '\x89' # 0x89 F2 - '\x8a' # 0x8A F4 - '\x8b' # 0x8B F6 - '\x8c' # 0x8C F8 - '\r' # 0x8D SHIFT-RETURN - '\x8e' # 0x8E uppercase charset - '\x8f' # 0x8F (unused) - '\x90' # 0x90 BLK (black) - '\x91' # 0x91 cursor up - '\x92' # 0x92 RVS OFF - '\x93' # 0x93 CLR (clear screen) - '\x94' # 0x94 INS (insert) - '\x95' # 0x95 BRN (brown) - '\x96' # 0x96 LRD (light red) - '\x97' # 0x97 GR1 (dark grey) - '\x98' # 0x98 GR2 (medium grey) - '\x99' # 0x99 LGR (light green) - '\x9a' # 0x9A LBL (light blue) - '\x9b' # 0x9B GR3 (light grey) - '\x9c' # 0x9C PUR (purple) - '\x9d' # 0x9D cursor left - '\x9e' # 0x9E YEL (yellow) - '\x9f' # 0x9F CYN (cyan) + "\x80" # 0x80 (unused) + "\x81" # 0x81 ORN (orange) + "\x82" # 0x82 (unused) + "\x83" # 0x83 (unused) + "\x84" # 0x84 (unused) + "\x85" # 0x85 F1 + "\x86" # 0x86 F3 + "\x87" # 0x87 F5 + "\x88" # 0x88 F7 + "\x89" # 0x89 F2 + "\x8a" # 0x8A F4 + "\x8b" # 0x8B F6 + "\x8c" # 0x8C F8 + "\r" # 0x8D SHIFT-RETURN + "\x8e" # 0x8E uppercase charset + "\x8f" # 0x8F (unused) + "\x90" # 0x90 BLK (black) + "\x91" # 0x91 cursor up + "\x92" # 0x92 RVS OFF + "\x93" # 0x93 CLR (clear screen) + "\x94" # 0x94 INS (insert) + "\x95" # 0x95 BRN (brown) + "\x96" # 0x96 LRD (light red) + "\x97" # 0x97 GR1 (dark grey) + "\x98" # 0x98 GR2 (medium grey) + "\x99" # 0x99 LGR (light green) + "\x9a" # 0x9A LBL (light blue) + "\x9b" # 0x9B GR3 (light grey) + "\x9c" # 0x9C PUR (purple) + "\x9d" # 0x9D cursor left + "\x9e" # 0x9E YEL (yellow) + "\x9f" # 0x9F CYN (cyan) # 0xA0-0xBF: Shifted graphics - '\xa0' # 0xA0 SHIFTED SPACE (non-breaking) - '\u2584' # 0xA1 LOWER HALF BLOCK - '\u2580' # 0xA2 UPPER HALF BLOCK - '\u2500' # 0xA3 HORIZONTAL LINE - '\u2500' # 0xA4 HORIZONTAL LINE (lower) - '\u2500' # 0xA5 HORIZONTAL LINE (upper) - '\u2502' # 0xA6 VERTICAL LINE (right shifted) - '\u2502' # 0xA7 VERTICAL LINE (left shifted) - '\u2502' # 0xA8 VERTICAL LINE - '\u256e' # 0xA9 BOX DRAWINGS LIGHT ARC DOWN AND LEFT - '\u2570' # 0xAA BOX DRAWINGS LIGHT ARC UP AND RIGHT - '\u256f' # 0xAB BOX DRAWINGS LIGHT ARC UP AND LEFT - '\u2572' # 0xAC BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT - '\u2571' # 0xAD BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT - '\u2573' # 0xAE BOX DRAWINGS LIGHT DIAGONAL CROSS (small) - '\u2022' # 0xAF BULLET - '\u25e4' # 0xB0 BLACK UPPER LEFT TRIANGLE - '\u258c' # 0xB1 LEFT HALF BLOCK - '\u2597' # 0xB2 QUADRANT LOWER RIGHT - '\u2514' # 0xB3 BOX DRAWINGS LIGHT UP AND RIGHT - '\u2510' # 0xB4 BOX DRAWINGS LIGHT DOWN AND LEFT - '\u2582' # 0xB5 LOWER ONE QUARTER BLOCK - '\u250c' # 0xB6 BOX DRAWINGS LIGHT DOWN AND RIGHT - '\u2534' # 0xB7 BOX DRAWINGS LIGHT UP AND HORIZONTAL - '\u252c' # 0xB8 BOX DRAWINGS LIGHT DOWN AND HORIZONTAL - '\u2524' # 0xB9 BOX DRAWINGS LIGHT VERTICAL AND LEFT - '\u251c' # 0xBA BOX DRAWINGS LIGHT VERTICAL AND RIGHT - '\u2586' # 0xBB LOWER THREE QUARTERS BLOCK - '\u2585' # 0xBC LOWER FIVE EIGHTHS BLOCK - '\u2590' # 0xBD RIGHT HALF BLOCK - '\u2588' # 0xBE FULL BLOCK (variant) - '\u2572' # 0xBF DIAGONAL (variant) + "\xa0" # 0xA0 SHIFTED SPACE (non-breaking) + "\u2584" # 0xA1 LOWER HALF BLOCK + "\u2580" # 0xA2 UPPER HALF BLOCK + "\u2500" # 0xA3 HORIZONTAL LINE + "\u2500" # 0xA4 HORIZONTAL LINE (lower) + "\u2500" # 0xA5 HORIZONTAL LINE (upper) + "\u2502" # 0xA6 VERTICAL LINE (right shifted) + "\u2502" # 0xA7 VERTICAL LINE (left shifted) + "\u2502" # 0xA8 VERTICAL LINE + "\u256e" # 0xA9 BOX DRAWINGS LIGHT ARC DOWN AND LEFT + "\u2570" # 0xAA BOX DRAWINGS LIGHT ARC UP AND RIGHT + "\u256f" # 0xAB BOX DRAWINGS LIGHT ARC UP AND LEFT + "\u2572" # 0xAC BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT + "\u2571" # 0xAD BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT + "\u2573" # 0xAE BOX DRAWINGS LIGHT DIAGONAL CROSS (small) + "\u2022" # 0xAF BULLET + "\u25e4" # 0xB0 BLACK UPPER LEFT TRIANGLE + "\u258c" # 0xB1 LEFT HALF BLOCK + "\u2597" # 0xB2 QUADRANT LOWER RIGHT + "\u2514" # 0xB3 BOX DRAWINGS LIGHT UP AND RIGHT + "\u2510" # 0xB4 BOX DRAWINGS LIGHT DOWN AND LEFT + "\u2582" # 0xB5 LOWER ONE QUARTER BLOCK + "\u250c" # 0xB6 BOX DRAWINGS LIGHT DOWN AND RIGHT + "\u2534" # 0xB7 BOX DRAWINGS LIGHT UP AND HORIZONTAL + "\u252c" # 0xB8 BOX DRAWINGS LIGHT DOWN AND HORIZONTAL + "\u2524" # 0xB9 BOX DRAWINGS LIGHT VERTICAL AND LEFT + "\u251c" # 0xBA BOX DRAWINGS LIGHT VERTICAL AND RIGHT + "\u2586" # 0xBB LOWER THREE QUARTERS BLOCK + "\u2585" # 0xBC LOWER FIVE EIGHTHS BLOCK + "\u2590" # 0xBD RIGHT HALF BLOCK + "\u2588" # 0xBE FULL BLOCK (variant) + "\u2572" # 0xBF DIAGONAL (variant) # 0xC0-0xDF: Horizontal line + uppercase A-Z + graphics - '\u2500' # 0xC0 HORIZONTAL LINE (same as 0x60) - 'A' # 0xC1 LATIN CAPITAL LETTER A - 'B' # 0xC2 LATIN CAPITAL LETTER B - 'C' # 0xC3 LATIN CAPITAL LETTER C - 'D' # 0xC4 LATIN CAPITAL LETTER D - 'E' # 0xC5 LATIN CAPITAL LETTER E - 'F' # 0xC6 LATIN CAPITAL LETTER F - 'G' # 0xC7 LATIN CAPITAL LETTER G - 'H' # 0xC8 LATIN CAPITAL LETTER H - 'I' # 0xC9 LATIN CAPITAL LETTER I - 'J' # 0xCA LATIN CAPITAL LETTER J - 'K' # 0xCB LATIN CAPITAL LETTER K - 'L' # 0xCC LATIN CAPITAL LETTER L - 'M' # 0xCD LATIN CAPITAL LETTER M - 'N' # 0xCE LATIN CAPITAL LETTER N - 'O' # 0xCF LATIN CAPITAL LETTER O - 'P' # 0xD0 LATIN CAPITAL LETTER P - 'Q' # 0xD1 LATIN CAPITAL LETTER Q - 'R' # 0xD2 LATIN CAPITAL LETTER R - 'S' # 0xD3 LATIN CAPITAL LETTER S - 'T' # 0xD4 LATIN CAPITAL LETTER T - 'U' # 0xD5 LATIN CAPITAL LETTER U - 'V' # 0xD6 LATIN CAPITAL LETTER V - 'W' # 0xD7 LATIN CAPITAL LETTER W - 'X' # 0xD8 LATIN CAPITAL LETTER X - 'Y' # 0xD9 LATIN CAPITAL LETTER Y - 'Z' # 0xDA LATIN CAPITAL LETTER Z - '\u253c' # 0xDB BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL - '\u2502' # 0xDC VERTICAL LINE (with tick) - '\u2571' # 0xDD DIAGONAL - '\u03c0' # 0xDE GREEK SMALL LETTER PI - '\u25e5' # 0xDF BLACK UPPER RIGHT TRIANGLE + "\u2500" # 0xC0 HORIZONTAL LINE (same as 0x60) + "A" # 0xC1 LATIN CAPITAL LETTER A + "B" # 0xC2 LATIN CAPITAL LETTER B + "C" # 0xC3 LATIN CAPITAL LETTER C + "D" # 0xC4 LATIN CAPITAL LETTER D + "E" # 0xC5 LATIN CAPITAL LETTER E + "F" # 0xC6 LATIN CAPITAL LETTER F + "G" # 0xC7 LATIN CAPITAL LETTER G + "H" # 0xC8 LATIN CAPITAL LETTER H + "I" # 0xC9 LATIN CAPITAL LETTER I + "J" # 0xCA LATIN CAPITAL LETTER J + "K" # 0xCB LATIN CAPITAL LETTER K + "L" # 0xCC LATIN CAPITAL LETTER L + "M" # 0xCD LATIN CAPITAL LETTER M + "N" # 0xCE LATIN CAPITAL LETTER N + "O" # 0xCF LATIN CAPITAL LETTER O + "P" # 0xD0 LATIN CAPITAL LETTER P + "Q" # 0xD1 LATIN CAPITAL LETTER Q + "R" # 0xD2 LATIN CAPITAL LETTER R + "S" # 0xD3 LATIN CAPITAL LETTER S + "T" # 0xD4 LATIN CAPITAL LETTER T + "U" # 0xD5 LATIN CAPITAL LETTER U + "V" # 0xD6 LATIN CAPITAL LETTER V + "W" # 0xD7 LATIN CAPITAL LETTER W + "X" # 0xD8 LATIN CAPITAL LETTER X + "Y" # 0xD9 LATIN CAPITAL LETTER Y + "Z" # 0xDA LATIN CAPITAL LETTER Z + "\u253c" # 0xDB BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL + "\u2502" # 0xDC VERTICAL LINE (with tick) + "\u2571" # 0xDD DIAGONAL + "\u03c0" # 0xDE GREEK SMALL LETTER PI + "\u25e5" # 0xDF BLACK UPPER RIGHT TRIANGLE # 0xE0-0xFE: Graphics (same as 0xA0-0xBE) - '\xa0' # 0xE0 SHIFTED SPACE - '\u2584' # 0xE1 LOWER HALF BLOCK - '\u2580' # 0xE2 UPPER HALF BLOCK - '\u2500' # 0xE3 HORIZONTAL LINE - '\u2500' # 0xE4 HORIZONTAL LINE (lower) - '\u2500' # 0xE5 HORIZONTAL LINE (upper) - '\u2502' # 0xE6 VERTICAL LINE (right shifted) - '\u2502' # 0xE7 VERTICAL LINE (left shifted) - '\u2502' # 0xE8 VERTICAL LINE - '\u256e' # 0xE9 BOX DRAWINGS LIGHT ARC DOWN AND LEFT - '\u2570' # 0xEA BOX DRAWINGS LIGHT ARC UP AND RIGHT - '\u256f' # 0xEB BOX DRAWINGS LIGHT ARC UP AND LEFT - '\u2572' # 0xEC DIAGONAL - '\u2571' # 0xED DIAGONAL - '\u2573' # 0xEE BOX DRAWINGS LIGHT DIAGONAL CROSS - '\u2022' # 0xEF BULLET - '\u25e4' # 0xF0 BLACK UPPER LEFT TRIANGLE - '\u258c' # 0xF1 LEFT HALF BLOCK - '\u2597' # 0xF2 QUADRANT LOWER RIGHT - '\u2514' # 0xF3 BOX DRAWINGS LIGHT UP AND RIGHT - '\u2510' # 0xF4 BOX DRAWINGS LIGHT DOWN AND LEFT - '\u2582' # 0xF5 LOWER ONE QUARTER BLOCK - '\u250c' # 0xF6 BOX DRAWINGS LIGHT DOWN AND RIGHT - '\u2534' # 0xF7 BOX DRAWINGS LIGHT UP AND HORIZONTAL - '\u252c' # 0xF8 BOX DRAWINGS LIGHT DOWN AND HORIZONTAL - '\u2524' # 0xF9 BOX DRAWINGS LIGHT VERTICAL AND LEFT - '\u251c' # 0xFA BOX DRAWINGS LIGHT VERTICAL AND RIGHT - '\u2586' # 0xFB LOWER THREE QUARTERS BLOCK - '\u2585' # 0xFC LOWER FIVE EIGHTHS BLOCK - '\u2590' # 0xFD RIGHT HALF BLOCK - '\u2588' # 0xFE FULL BLOCK - '\u03c0' # 0xFF PI + "\xa0" # 0xE0 SHIFTED SPACE + "\u2584" # 0xE1 LOWER HALF BLOCK + "\u2580" # 0xE2 UPPER HALF BLOCK + "\u2500" # 0xE3 HORIZONTAL LINE + "\u2500" # 0xE4 HORIZONTAL LINE (lower) + "\u2500" # 0xE5 HORIZONTAL LINE (upper) + "\u2502" # 0xE6 VERTICAL LINE (right shifted) + "\u2502" # 0xE7 VERTICAL LINE (left shifted) + "\u2502" # 0xE8 VERTICAL LINE + "\u256e" # 0xE9 BOX DRAWINGS LIGHT ARC DOWN AND LEFT + "\u2570" # 0xEA BOX DRAWINGS LIGHT ARC UP AND RIGHT + "\u256f" # 0xEB BOX DRAWINGS LIGHT ARC UP AND LEFT + "\u2572" # 0xEC DIAGONAL + "\u2571" # 0xED DIAGONAL + "\u2573" # 0xEE BOX DRAWINGS LIGHT DIAGONAL CROSS + "\u2022" # 0xEF BULLET + "\u25e4" # 0xF0 BLACK UPPER LEFT TRIANGLE + "\u258c" # 0xF1 LEFT HALF BLOCK + "\u2597" # 0xF2 QUADRANT LOWER RIGHT + "\u2514" # 0xF3 BOX DRAWINGS LIGHT UP AND RIGHT + "\u2510" # 0xF4 BOX DRAWINGS LIGHT DOWN AND LEFT + "\u2582" # 0xF5 LOWER ONE QUARTER BLOCK + "\u250c" # 0xF6 BOX DRAWINGS LIGHT DOWN AND RIGHT + "\u2534" # 0xF7 BOX DRAWINGS LIGHT UP AND HORIZONTAL + "\u252c" # 0xF8 BOX DRAWINGS LIGHT DOWN AND HORIZONTAL + "\u2524" # 0xF9 BOX DRAWINGS LIGHT VERTICAL AND LEFT + "\u251c" # 0xFA BOX DRAWINGS LIGHT VERTICAL AND RIGHT + "\u2586" # 0xFB LOWER THREE QUARTERS BLOCK + "\u2585" # 0xFC LOWER FIVE EIGHTHS BLOCK + "\u2590" # 0xFD RIGHT HALF BLOCK + "\u2588" # 0xFE FULL BLOCK + "\u03c0" # 0xFF PI ) assert len(DECODING_TABLE) == 256 @@ -305,19 +305,23 @@ class Codec(codecs.Codec): """PETSCII character map codec.""" - def encode(self, input, errors='strict'): # pylint: disable=redefined-builtin + def encode( # pylint: disable=redefined-builtin + self, input: str, errors: str = "strict" + ) -> tuple[bytes, int]: """Encode input string using PETSCII character map.""" return codecs.charmap_encode(input, errors, ENCODING_TABLE) - def decode(self, input, errors='strict'): # pylint: disable=redefined-builtin + def decode( # pylint: disable=redefined-builtin + self, input: bytes, errors: str = "strict" + ) -> tuple[str, int]: """Decode input bytes using PETSCII character map.""" - return codecs.charmap_decode(input, errors, DECODING_TABLE) + return codecs.charmap_decode(input, errors, DECODING_TABLE) # type: ignore[arg-type] class IncrementalEncoder(codecs.IncrementalEncoder): """PETSCII incremental encoder.""" - def encode(self, input, final=False): # pylint: disable=redefined-builtin + def encode(self, input: str, final: bool = False) -> bytes: # pylint: disable=redefined-builtin """Encode input string incrementally.""" return codecs.charmap_encode(input, self.errors, ENCODING_TABLE)[0] @@ -325,9 +329,13 @@ def encode(self, input, final=False): # pylint: disable=redefined-builtin class IncrementalDecoder(codecs.IncrementalDecoder): """PETSCII incremental decoder.""" - def decode(self, input, final=False): # pylint: disable=redefined-builtin + def decode( # type: ignore[override] # pylint: disable=redefined-builtin + self, input: bytes, final: bool = False + ) -> str: """Decode input bytes incrementally.""" - return codecs.charmap_decode(input, self.errors, DECODING_TABLE)[0] + return codecs.charmap_decode(input, self.errors, DECODING_TABLE)[ # type: ignore[arg-type] + 0 + ] class StreamWriter(Codec, codecs.StreamWriter): @@ -338,12 +346,12 @@ class StreamReader(Codec, codecs.StreamReader): """PETSCII stream reader.""" -def getregentry(): +def getregentry() -> codecs.CodecInfo: """Return the codec registry entry.""" return codecs.CodecInfo( - name='petscii', + name="petscii", encode=Codec().encode, - decode=Codec().decode, + decode=Codec().decode, # type: ignore[arg-type] incrementalencoder=IncrementalEncoder, incrementaldecoder=IncrementalDecoder, streamreader=StreamReader, @@ -351,9 +359,9 @@ def getregentry(): ) -def getaliases(): +def getaliases() -> tuple[str, ...]: """Return codec aliases.""" - return ('cbm', 'commodore', 'c64', 'c128') + return ("cbm", "commodore", "c64", "c128") ENCODING_TABLE = codecs.charmap_build(DECODING_TABLE) diff --git a/telnetlib3/fingerprinting.py b/telnetlib3/fingerprinting.py index a2268819..566b6572 100644 --- a/telnetlib3/fingerprinting.py +++ b/telnetlib3/fingerprinting.py @@ -200,7 +200,6 @@ def on_request_environ(self) -> list[Union[str, bytes]]: # pylint: disable-next=no-member base: list[Union[str, bytes]] = super().on_request_environ() # type: ignore[misc] # Insert extended keys before the trailing VAR/USERVAR sentinels - # local from .telopt import VAR, USERVAR # pylint: disable=import-outside-toplevel extra = [k for k in ENVIRON_EXTENDED if k not in base] @@ -849,7 +848,6 @@ def _validate_suggestion(text: str) -> Optional[str]: def _cooked_input(prompt: str) -> str: """Call :func:`input` with echo and canonical mode temporarily enabled.""" - # std imports import termios # pylint: disable=import-outside-toplevel fd = sys.stdin.fileno() @@ -1044,7 +1042,6 @@ async def fingerprinting_server_shell( :param writer: TelnetWriter instance. """ # pylint: disable=import-outside-toplevel - # local from .server_pty_shell import pty_shell writer = cast(TelnetWriterUnicode, writer) @@ -1096,7 +1093,6 @@ def fingerprinting_post_script(filepath: str) -> None: :param filepath: Path to the saved fingerprint JSON file. """ - # local # pylint: disable-next=import-outside-toplevel,cyclic-import from .fingerprinting_display import fingerprinting_post_script as _fps @@ -1117,7 +1113,6 @@ def fingerprint_server_main() -> None: """ # pylint: disable=import-outside-toplevel,global-statement # local import is required to prevent circular imports - # local from .server import _config, run_server, parse_server_args # noqa: PLC0415 global DATA_DIR diff --git a/telnetlib3/fingerprinting_display.py b/telnetlib3/fingerprinting_display.py index 11d3051f..4444b8db 100644 --- a/telnetlib3/fingerprinting_display.py +++ b/telnetlib3/fingerprinting_display.py @@ -415,7 +415,6 @@ def _build_telnet_rows( # pylint: disable=too-many-locals,unused-argument def _make_terminal(**kwargs: Any) -> Any: """Create a blessed Terminal, falling back to ``ansi`` on setupterm failure.""" - # 3rd party from blessed import Terminal # pylint: disable=import-outside-toplevel,import-error with warnings.catch_warnings(record=True) as caught: @@ -525,7 +524,6 @@ def _display_compact_summary( # pylint: disable=too-complex,too-many-branches ) -> bool: """Display compact fingerprint summary using prettytable.""" try: - # 3rd party from ucs_detect import ( # pylint: disable=import-outside-toplevel _collect_side_by_side_lines, ) @@ -884,7 +882,6 @@ def _strip_empty_features(d: Dict[str, Any]) -> None: def _normalize_color_hex(hex_color: str) -> str: """Normalize X11 color hex to standard 6-digit format.""" - # 3rd party from blessed.colorspace import ( # pylint: disable=import-outside-toplevel,import-error hex_to_rgb, rgb_to_hex, @@ -1072,7 +1069,6 @@ def _show_database( ) -> None: """Display scrollable database of all known fingerprints.""" try: - # 3rd party from prettytable import PrettyTable # pylint: disable=import-outside-toplevel except ImportError: echo("prettytable not installed.\n") @@ -1279,7 +1275,6 @@ def _process_client_fingerprint(filepath: str, data: Dict[str, Any]) -> None: _setup_term_environ(data) try: - # 3rd party import blessed # noqa: F401 # pylint: disable=import-outside-toplevel,unused-import except ImportError: print(json.dumps(data, indent=2, sort_keys=True)) diff --git a/telnetlib3/server.py b/telnetlib3/server.py index ad61941c..6eddea1c 100755 --- a/telnetlib3/server.py +++ b/telnetlib3/server.py @@ -32,7 +32,6 @@ # Check if PTY support is available (Unix-only modules: pty, termios, fcntl) try: - # std imports import pty # noqa: F401 pylint:disable=unused-import import fcntl # noqa: F401 pylint:disable=unused-import import termios # noqa: F401 pylint:disable=unused-import @@ -134,7 +133,6 @@ def __init__( # pylint: disable=too-many-positional-arguments def connection_made(self, transport: asyncio.BaseTransport) -> None: """Handle new connection and wire up telnet option callbacks.""" - # local from .telopt import ( # pylint: disable=import-outside-toplevel NAWS, TTYPE, @@ -177,7 +175,6 @@ def data_received(self, data: bytes) -> None: def begin_negotiation(self) -> None: """Begin telnet negotiation by requesting terminal type.""" - # local from .telopt import DO, TTYPE # pylint: disable=import-outside-toplevel super().begin_negotiation() @@ -196,7 +193,6 @@ def begin_advanced_negotiation(self) -> None: MUD clients (Mudlet, TinTin++, etc.) interpret ``WILL ECHO`` as "password mode" and mask input. See ``_negotiate_echo()``. """ - # local from .telopt import ( # pylint: disable=import-outside-toplevel DO, SGA, @@ -218,7 +214,6 @@ def begin_advanced_negotiation(self) -> None: def check_negotiation(self, final: bool = False) -> bool: """Check if negotiation is complete including encoding.""" - # local from .telopt import ( # pylint: disable=import-outside-toplevel DO, SB, @@ -431,7 +426,6 @@ def on_request_environ(self) -> List[Union[str, bytes]]: :data:`~.fingerprinting.ENVIRON_EXTENDED` for a larger set used during client fingerprinting. """ - # local from .telopt import VAR, USERVAR # pylint: disable=import-outside-toplevel return [ @@ -589,7 +583,6 @@ def _negotiate_environ(self) -> None: return self._environ_requested = True - # local from .telopt import DO, NEW_ENVIRON # pylint: disable=import-outside-toplevel ttype1 = self.get_extra_info("ttype1") or "" @@ -619,7 +612,6 @@ def _negotiate_echo(self) -> None: return self._echo_negotiated = True - # local from .telopt import ECHO, WILL # pylint: disable=import-outside-toplevel from .fingerprinting import _is_maybe_mud # pylint: disable=import-outside-toplevel @@ -631,7 +623,6 @@ def _negotiate_echo(self) -> None: def _check_encoding(self) -> bool: # Periodically check for completion of ``waiter_encoding``. - # local from .telopt import DO, SB, BINARY, CHARSET # pylint: disable=import-outside-toplevel assert self.writer is not None @@ -999,12 +990,7 @@ def parse_server_args() -> Dict[str, Any]: ) # Hidden backwards-compat: --pty-raw was the default since 2.5, # keep it as a silent no-op so existing scripts don't break. - parser.add_argument( - "--pty-raw", - action="store_true", - default=False, - help=argparse.SUPPRESS, - ) + parser.add_argument("--pty-raw", action="store_true", default=False, help=argparse.SUPPRESS) parser.add_argument( "--robot-check", action="store_true", @@ -1041,10 +1027,9 @@ def parse_server_args() -> Dict[str, Any]: result["pty_raw"] = False # Auto-enable force_binary for retro BBS encodings that use high-bit bytes. - # local from .encodings import FORCE_BINARY_ENCODINGS # pylint: disable=import-outside-toplevel - if result["encoding"].lower().replace('-', '_') in FORCE_BINARY_ENCODINGS: + if result["encoding"].lower().replace("-", "_") in FORCE_BINARY_ENCODINGS: result["force_binary"] = True return result @@ -1083,14 +1068,12 @@ async def run_server( # pylint: disable=too-many-positional-arguments,too-many- if pty_exec: if not PTY_SUPPORT: raise NotImplementedError("PTY support is not available on this platform (Windows?)") - # local from .server_pty_shell import make_pty_shell # pylint: disable=import-outside-toplevel shell = make_pty_shell(pty_exec, pty_args, raw_mode=pty_raw) # Wrap shell with guards if enabled if robot_check or pty_fork_limit: - # local # pylint: disable=import-outside-toplevel from .guard_shells import robot_shell # pylint: disable=import-outside-toplevel from .guard_shells import ConnectionCounter, busy_shell diff --git a/telnetlib3/server_fingerprinting.py b/telnetlib3/server_fingerprinting.py index 475facb8..2f81fbc7 100644 --- a/telnetlib3/server_fingerprinting.py +++ b/telnetlib3/server_fingerprinting.py @@ -95,9 +95,7 @@ # Match numbered menu items offering ANSI, e.g. "(1) Ansi", "[2] ANSI", # "3. English/ANSI", or "1 ... English/ANSI". Brackets, dot, and # ellipsis delimiters are accepted. -_MENU_ANSI_RE = re.compile( - rb"[\[(]?(\d+)\s*(?:[\])]|\.{1,3})\s*(?:\S+\s*/\s*)?ANSI", re.IGNORECASE -) +_MENU_ANSI_RE = re.compile(rb"[\[(]?(\d+)\s*(?:[\])]|\.{1,3})\s*(?:\S+\s*/\s*)?ANSI", re.IGNORECASE) # Match "gb/big5" encoding selection prompts common on Chinese BBS systems. _GB_BIG5_RE = re.compile(rb"(?i)(?:^|[^a-zA-Z0-9])gb\s*/\s*big\s*5(?:[^a-zA-Z0-9]|$)") @@ -114,15 +112,11 @@ # Match "HIT RETURN", "PRESS RETURN", "PRESS ENTER", "HIT ENTER", etc. # Common on Worldgroup/MajorBBS and other vintage BBS systems. -_RETURN_PROMPT_RE = re.compile( - rb"(?i)(?:hit|press)\s+(?:return|enter)\s*[:\.]?" -) +_RETURN_PROMPT_RE = re.compile(rb"(?i)(?:hit|press)\s+(?:return|enter)\s*[:\.]?") # Match "Press the BACKSPACE key" prompts — standard telnet terminal # detection (e.g. TelnetBible.com). Respond with ASCII BS (0x08). -_BACKSPACE_KEY_RE = re.compile( - rb"(?i)press\s+the\s+backspace\s+key" -) +_BACKSPACE_KEY_RE = re.compile(rb"(?i)press\s+the\s+backspace\s+key") # Match "press del/backspace" and "hit your backspace/delete" prompts from # PETSCII BBS systems (e.g. Image BBS C/G detect). Respond with PETSCII @@ -135,9 +129,7 @@ # Match "More: (Y)es, (N)o, (C)ontinuous?" pagination prompts. # Answer "C" (Continuous) to disable pagination and collect the full banner. -_MORE_PROMPT_RE = re.compile( - rb"(?i)more[:\s]*\(?[yY]\)?.*\(?[cC]\)?\s*(?:ontinuous|ont)" -) +_MORE_PROMPT_RE = re.compile(rb"(?i)more[:\s]*\(?[yY]\)?.*\(?[cC]\)?\s*(?:ontinuous|ont)") # Match DSR (Device Status Report) request: ESC [ 6 n. # Servers send this to detect ANSI-capable terminals; we reply with a @@ -194,9 +186,7 @@ } #: Encodings that require ``force_binary`` for high-bit bytes. -_SYNCTERM_BINARY_ENCODINGS = frozenset({ - "petscii", "atascii", -}) +_SYNCTERM_BINARY_ENCODINGS = frozenset({"petscii", "atascii"}) log = logging.getLogger(__name__) @@ -223,9 +213,7 @@ def detect_syncterm_font(data: bytes) -> str | None: #: Encodings where standard telnet CR+LF must be re-encoded to the #: codec's native EOL byte. The codec's ``encode()`` handles the #: actual CR → LF normalization; we just gate the re-encoding step. -_RETRO_EOL_ENCODINGS = frozenset({ - 'atascii', 'atari8bit', 'atari_8bit', -}) +_RETRO_EOL_ENCODINGS = frozenset({"atascii", "atari8bit", "atari_8bit"}) def _reencode_prompt(response: bytes, encoding: str) -> bytes: @@ -240,11 +228,11 @@ def _reencode_prompt(response: bytes, encoding: str) -> bytes: :param encoding: Remote server encoding name. :returns: Response bytes suitable for the server's encoding. """ - normalized = encoding.lower().replace('-', '_') + normalized = encoding.lower().replace("-", "_") if normalized not in _RETRO_EOL_ENCODINGS: return response try: - text = response.decode('ascii') + text = response.decode("ascii") return text.encode(encoding) except (UnicodeDecodeError, UnicodeEncodeError, LookupError): return response @@ -346,9 +334,7 @@ class _PromptResult(NamedTuple): encoding: str | None = None -def _detect_yn_prompt( # pylint: disable=too-many-return-statements - banner: bytes, -) -> _PromptResult: +def _detect_yn_prompt(banner: bytes) -> _PromptResult: # pylint: disable=too-many-return-statements r""" Return an appropriate first-prompt response based on banner content. @@ -483,14 +469,22 @@ async def _fingerprint_session( # noqa: E501 ; pylint: disable=too-many-locals, # 1. Let straggler negotiation settle — read (and respond to DSR) # instead of sleeping blind so early DSR requests get a CPR reply. settle_data = await _read_banner_until_quiet( - reader, quiet_time=_NEGOTIATION_SETTLE, max_wait=_NEGOTIATION_SETTLE, - max_bytes=banner_max_bytes, writer=writer, cursor=cursor, + reader, + quiet_time=_NEGOTIATION_SETTLE, + max_wait=_NEGOTIATION_SETTLE, + max_bytes=banner_max_bytes, + writer=writer, + cursor=cursor, ) # 2. Read banner (pre-return) — wait until output stops banner_before_raw = await _read_banner_until_quiet( - reader, quiet_time=banner_quiet_time, max_wait=banner_max_wait, - max_bytes=banner_max_bytes, writer=writer, cursor=cursor, + reader, + quiet_time=banner_quiet_time, + max_wait=banner_max_wait, + max_bytes=banner_max_bytes, + writer=writer, + cursor=cursor, ) banner_before = settle_data + banner_before_raw @@ -505,15 +499,12 @@ async def _fingerprint_session( # noqa: E501 ; pylint: disable=too-many-locals, detected = prompt_result.response # Skip if the ESC response was already sent inline during banner # collection (time-sensitive botcheck countdowns). - if detected in (b"\x1b\x1b", b"\x1b") and getattr( - writer, '_esc_inline', False - ): + if detected in (b"\x1b\x1b", b"\x1b") and getattr(writer, "_esc_inline", False): # pylint: disable-next=protected-access writer._esc_inline = False # type: ignore[attr-defined] detected = None prompt_response = _reencode_prompt( - detected if detected is not None else b"\r\n", - writer.environ_encoding, + detected if detected is not None else b"\r\n", writer.environ_encoding ) writer.write(prompt_response) await writer.drain() @@ -528,8 +519,12 @@ async def _fingerprint_session( # noqa: E501 ; pylint: disable=too-many-locals, protocol.force_binary = True previous_banner = latest_banner latest_banner = await _read_banner_until_quiet( - reader, quiet_time=banner_quiet_time, max_wait=banner_max_wait, - max_bytes=banner_max_bytes, writer=writer, cursor=cursor, + reader, + quiet_time=banner_quiet_time, + max_wait=banner_max_wait, + max_bytes=banner_max_bytes, + writer=writer, + cursor=cursor, ) after_chunks.append(latest_banner) if writer.is_closing() or not latest_banner: @@ -567,14 +562,8 @@ async def _fingerprint_session( # noqa: E501 ; pylint: disable=too-many-locals, { "scan_type": scan_type, "encoding": writer.environ_encoding, - "banner_before_return": _format_banner( - banner_before, - encoding=writer.environ_encoding, - ), - "banner_after_return": _format_banner( - banner_after, - encoding=writer.environ_encoding, - ), + "banner_before_return": _format_banner(banner_before, encoding=writer.environ_encoding), + "banner_after_return": _format_banner(banner_after, encoding=writer.environ_encoding), "timing": {"probe": probe_time, "total": time.time() - start_time}, "dsr_requests": cursor.dsr_requests, "dsr_replies": cursor.dsr_replies, @@ -885,11 +874,11 @@ def _format_banner(data: bytes, encoding: str = "utf-8") -> str: text = data.decode("latin-1") if encoding.lower() in ("petscii", "cbm", "commodore", "c64", "c128"): - # local from .color_filter import PetsciiColorFilter # pylint: disable=import-outside-toplevel + text = PetsciiColorFilter().filter(text) # PETSCII uses CR (0x0D) as line terminator; normalize to LF. - text = text.replace('\r\n', '\n').replace('\r', '\n') + text = text.replace("\r\n", "\n").replace("\r", "\n") return text @@ -925,9 +914,7 @@ async def _read_banner( return data -def _respond_to_dsr( - chunk: bytes, writer: TelnetWriter, cursor: _VirtualCursor | None -) -> None: +def _respond_to_dsr(chunk: bytes, writer: TelnetWriter, cursor: _VirtualCursor | None) -> None: """ Send CPR response(s) for each DSR found in *chunk*. @@ -942,7 +929,7 @@ def _respond_to_dsr( pos = 0 for match in _DSR_RE.finditer(chunk): cursor.dsr_requests += 1 - cursor.advance(chunk[pos:match.start()]) + cursor.advance(chunk[pos : match.start()]) writer.write(cursor.cpr()) pos = match.end() cursor.advance(chunk[pos:]) @@ -1007,17 +994,16 @@ async def _read_banner_until_quiet( # noqa: E501 ; pylint: disable=too-many-pos font_enc = detect_syncterm_font(chunk) if font_enc is not None: log.debug("SyncTERM font switch detected: %s", font_enc) - if getattr(writer, '_encoding_explicit', False): + if getattr(writer, "_encoding_explicit", False): log.debug( - "ignoring font switch, explicit encoding: %s", - writer.environ_encoding) + "ignoring font switch, explicit encoding: %s", writer.environ_encoding + ) else: writer.environ_encoding = font_enc if cursor is not None: cursor.encoding = font_enc protocol = writer.protocol - if (protocol is not None - and font_enc in _SYNCTERM_BINARY_ENCODINGS): + if protocol is not None and font_enc in _SYNCTERM_BINARY_ENCODINGS: protocol.force_binary = True if not esc_responded: stripped_chunk = _ANSI_STRIP_RE.sub(b"", chunk) diff --git a/telnetlib3/server_pty_shell.py b/telnetlib3/server_pty_shell.py index 948f29c5..e6f21058 100644 --- a/telnetlib3/server_pty_shell.py +++ b/telnetlib3/server_pty_shell.py @@ -105,7 +105,6 @@ def start(self) -> None: :raises PTYSpawnError: If the child process fails to exec. """ # pylint: disable=import-outside-toplevel - # std imports import pty import fcntl @@ -222,7 +221,6 @@ def _setup_child( """Child process setup before exec.""" # Note: pty.fork() already calls setsid() for the child, so we don't need to # pylint: disable=import-outside-toplevel - # std imports import fcntl import termios @@ -263,7 +261,6 @@ def _setup_child( def _setup_parent(self) -> None: """Parent process setup after fork.""" # pylint: disable=import-outside-toplevel - # std imports import fcntl assert self.master_fd is not None @@ -295,7 +292,6 @@ def _fire_naws_update(self) -> None: def _set_window_size(self, rows: int, cols: int) -> None: """Set PTY window size and send SIGWINCH to child.""" # pylint: disable=import-outside-toplevel - # std imports import fcntl import signal import termios @@ -312,7 +308,6 @@ def _set_window_size(self, rows: int, cols: int) -> None: async def run(self) -> None: """Bridge loop between telnet and PTY.""" # pylint: disable=import-outside-toplevel - # std imports import errno loop = asyncio.get_event_loop() @@ -532,7 +527,6 @@ def _terminate(self, force: bool = False) -> bool: :returns: True if child was terminated, False otherwise. """ # pylint: disable=import-outside-toplevel - # std imports import signal if not self._isalive(): diff --git a/telnetlib3/stream_writer.py b/telnetlib3/stream_writer.py index d4dea88a..78f882e8 100644 --- a/telnetlib3/stream_writer.py +++ b/telnetlib3/stream_writer.py @@ -106,6 +106,9 @@ #: MUD options that allow empty SB payloads (e.g. ``IAC SB MXP IAC SE``). _EMPTY_SB_OK = frozenset({MXP, MSP, ZMP, AARDWOLF, ATCP}) +#: MUD protocol options that a plain telnet client should decline by default. +_MUD_PROTOCOL_OPTIONS = frozenset({GMCP, MSDP, MSSP, MSP, MXP, ZMP, AARDWOLF, ATCP}) + class TelnetWriter: """ @@ -232,6 +235,11 @@ def __init__( #: DONT rejection in :meth:`handle_will`. self.always_do: set[bytes] = set() + #: Whether the encoding was explicitly set (not just the default + #: ``"ascii"``). Used by fingerprinting and client connection logic + #: to decide whether to negotiate CHARSET. + self._encoding_explicit: bool = False + #: Set of option byte(s) for WILL received from remote end #: that were rejected with DONT (unhandled options). self.rejected_will: set[bytes] = set() @@ -1825,6 +1833,17 @@ def handle_do(self, opt: bytes) -> bool: AARDWOLF, ATCP, ): + # Client declines MUD protocols unless explicitly opted in. + if self.client and opt in _MUD_PROTOCOL_OPTIONS: + if opt in self.always_will: + if not self.local_option.enabled(opt): + self.iac(WILL, opt) + return True + self.log.debug("DO %s: MUD protocol, declining on client.", name_command(opt)) + if not self.local_option.enabled(opt): + self.iac(WONT, opt) + return False + # first time we've agreed, respond accordingly. if not self.local_option.enabled(opt): self.iac(WILL, opt) @@ -1923,6 +1942,15 @@ def handle_will(self, opt: bytes) -> None: self.log.debug("recv WILL %s on client end, refusing.", name_command(opt)) self.iac(DONT, opt) return + # Client declines MUD protocols unless explicitly opted in. + if self.client and opt in _MUD_PROTOCOL_OPTIONS: + if opt in self.always_do: + if not self.remote_option.enabled(opt): + self.iac(DO, opt) + self.remote_option[opt] = True + return + self.iac(DONT, opt) + return if not self.remote_option.enabled(opt): self.iac(DO, opt) self.remote_option[opt] = True @@ -2005,7 +2033,7 @@ def handle_will(self, opt: bytes) -> None: else: self.iac(DONT, opt) self.rejected_will.add(opt) - self.log.warning("Unhandled: WILL %s.", name_command(opt)) + self.log.debug("Unhandled: WILL %s.", name_command(opt)) if self.pending_option.enabled(DO + opt): self.pending_option[DO + opt] = False @@ -2117,9 +2145,7 @@ def _write(self, buf: bytes, escape_iac: bool = True) -> None: buf = self._escape_iac(buf) if self.log.isEnabledFor(TRACE): - self.log.log( - TRACE, "send %d bytes\n%s", len(buf), hexdump(buf, prefix=">> ") - ) + self.log.log(TRACE, "send %d bytes\n%s", len(buf), hexdump(buf, prefix=">> ")) self._transport.write(buf) if hasattr(self._protocol, "_tx_bytes"): self._protocol._tx_bytes += len(buf) diff --git a/telnetlib3/sync.py b/telnetlib3/sync.py index f2bf0b0a..a4476b4d 100644 --- a/telnetlib3/sync.py +++ b/telnetlib3/sync.py @@ -133,7 +133,6 @@ async def _async_connect(self) -> None: # is programmatic, not a terminal app, so it should use the cols/rows # parameters rather than reading the real terminal size. if "client_factory" not in kwargs: - # local from .client import TelnetClient # pylint: disable=import-outside-toplevel kwargs["client_factory"] = TelnetClient diff --git a/telnetlib3/telnetlib.py b/telnetlib3/telnetlib.py index 41f8bfa7..e27c1741 100644 --- a/telnetlib3/telnetlib.py +++ b/telnetlib3/telnetlib.py @@ -687,7 +687,6 @@ def expect(self, list, timeout=None): # pylint: disable=redefined-builtin for i in indices: if not hasattr(list[i], "search"): if not re: - # std imports import re # pylint: disable=import-outside-toplevel list[i] = re.compile(list[i]) if timeout is not None: diff --git a/telnetlib3/tests/accessories.py b/telnetlib3/tests/accessories.py index 262f7956..606a339b 100644 --- a/telnetlib3/tests/accessories.py +++ b/telnetlib3/tests/accessories.py @@ -20,7 +20,6 @@ def init_subproc_coverage(run_note=None): :returns: Coverage instance or None. """ try: - # 3rd party import coverage except ImportError: return None @@ -76,7 +75,6 @@ async def connection_context(reader, writer): async def create_server(*args, **kwargs): """Create a telnetlib3 server with automatic cleanup.""" # local import to avoid circular import - # local import telnetlib3 server = await telnetlib3.create_server(*args, **kwargs) @@ -91,7 +89,6 @@ async def create_server(*args, **kwargs): async def open_connection(*args, **kwargs): """Open a telnetlib3 connection with automatic cleanup.""" # local import to avoid circular import - # local import telnetlib3 # Force deterministic client: TelnetTerminalClient reads the real terminal @@ -99,7 +96,6 @@ async def open_connection(*args, **kwargs): # tests get consistent behavior regardless of whether stdin is a TTY. if "client_factory" not in kwargs: # local import to avoid circular import - # local from telnetlib3.client import TelnetClient kwargs["client_factory"] = TelnetClient diff --git a/telnetlib3/tests/conftest.py b/telnetlib3/tests/conftest.py index ad6238b9..ec8dc728 100644 --- a/telnetlib3/tests/conftest.py +++ b/telnetlib3/tests/conftest.py @@ -4,7 +4,6 @@ import pytest try: - # 3rd party from pytest_codspeed import BenchmarkFixture # noqa: F401 pylint:disable=unused-import except ImportError: # Provide a no-op benchmark fixture when pytest-codspeed is not installed diff --git a/telnetlib3/tests/pty_helper.py b/telnetlib3/tests/pty_helper.py index 1891e83c..b52a8eff 100644 --- a/telnetlib3/tests/pty_helper.py +++ b/telnetlib3/tests/pty_helper.py @@ -45,7 +45,6 @@ def echo_mode(args): def stty_size_mode(): """Print terminal size.""" # imported locally to avoid error on import with windows systems - # std imports import fcntl import struct import termios diff --git a/telnetlib3/tests/test_accessories_extra.py b/telnetlib3/tests/test_accessories_extra.py index 341bac2f..bbced45f 100644 --- a/telnetlib3/tests/test_accessories_extra.py +++ b/telnetlib3/tests/test_accessories_extra.py @@ -48,7 +48,7 @@ def test_hexdump_prefix(): def test_hexdump_empty(): - assert hexdump(b"") == "" + assert not hexdump(b"") def test_make_logger_trace_level(): diff --git a/telnetlib3/tests/test_atascii_codec.py b/telnetlib3/tests/test_atascii_codec.py index 7f28446a..c7c81420 100644 --- a/telnetlib3/tests/test_atascii_codec.py +++ b/telnetlib3/tests/test_atascii_codec.py @@ -7,233 +7,245 @@ import pytest # local -import telnetlib3 # noqa: F401 - registers codecs +import telnetlib3 # noqa: F401 from telnetlib3.encodings import atascii def test_codec_lookup(): - info = codecs.lookup('atascii') - assert info.name == 'atascii' + info = codecs.lookup("atascii") + assert info.name == "atascii" -@pytest.mark.parametrize("alias", ['atari8bit', 'atari_8bit']) +@pytest.mark.parametrize("alias", ["atari8bit", "atari_8bit"]) def test_codec_aliases(alias): info = codecs.lookup(alias) - assert info.name == 'atascii' + assert info.name == "atascii" def test_ascii_letters_uppercase(): data = bytes(range(0x41, 0x5B)) - assert data.decode('atascii') == 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + assert data.decode("atascii") == "ABCDEFGHIJKLMNOPQRSTUVWXYZ" def test_ascii_letters_lowercase(): data = bytes(range(0x61, 0x7B)) - assert data.decode('atascii') == 'abcdefghijklmnopqrstuvwxyz' + assert data.decode("atascii") == "abcdefghijklmnopqrstuvwxyz" def test_digits(): data = bytes(range(0x30, 0x3A)) - assert data.decode('atascii') == '0123456789' + assert data.decode("atascii") == "0123456789" def test_space(): - assert b'\x20'.decode('atascii') == ' ' - - -@pytest.mark.parametrize("byte_val,expected", [ - pytest.param(0x00, '\u2665', id='heart'), - pytest.param(0x01, '\u251c', id='box_vert_right'), - pytest.param(0x08, '\u25e2', id='lower_right_triangle'), - pytest.param(0x09, '\u2597', id='quadrant_lower_right'), - pytest.param(0x10, '\u2663', id='club'), - pytest.param(0x12, '\u2500', id='horizontal_line'), - pytest.param(0x14, '\u25cf', id='black_circle'), - pytest.param(0x15, '\u2584', id='lower_half_block'), - pytest.param(0x19, '\u258c', id='left_half_block'), - pytest.param(0x1B, '\u241b', id='symbol_for_escape'), -]) + assert b"\x20".decode("atascii") == " " + + +@pytest.mark.parametrize( + "byte_val,expected", + [ + pytest.param(0x00, "\u2665", id="heart"), + pytest.param(0x01, "\u251c", id="box_vert_right"), + pytest.param(0x08, "\u25e2", id="lower_right_triangle"), + pytest.param(0x09, "\u2597", id="quadrant_lower_right"), + pytest.param(0x10, "\u2663", id="club"), + pytest.param(0x12, "\u2500", id="horizontal_line"), + pytest.param(0x14, "\u25cf", id="black_circle"), + pytest.param(0x15, "\u2584", id="lower_half_block"), + pytest.param(0x19, "\u258c", id="left_half_block"), + pytest.param(0x1B, "\u241b", id="symbol_for_escape"), + ], +) def test_graphics_chars(byte_val, expected): - assert bytes([byte_val]).decode('atascii') == expected - - -@pytest.mark.parametrize("byte_val,expected", [ - pytest.param(0x1C, '\u2191', id='cursor_up'), - pytest.param(0x1D, '\u2193', id='cursor_down'), - pytest.param(0x1E, '\u2190', id='cursor_left'), - pytest.param(0x1F, '\u2192', id='cursor_right'), -]) + assert bytes([byte_val]).decode("atascii") == expected + + +@pytest.mark.parametrize( + "byte_val,expected", + [ + pytest.param(0x1C, "\u2191", id="cursor_up"), + pytest.param(0x1D, "\u2193", id="cursor_down"), + pytest.param(0x1E, "\u2190", id="cursor_left"), + pytest.param(0x1F, "\u2192", id="cursor_right"), + ], +) def test_cursor_arrows(byte_val, expected): - assert bytes([byte_val]).decode('atascii') == expected - - -@pytest.mark.parametrize("byte_val,expected", [ - pytest.param(0x60, '\u2666', id='diamond'), - pytest.param(0x7B, '\u2660', id='spade'), - pytest.param(0x7C, '|', id='pipe'), - pytest.param(0x7D, '\u21b0', id='clear_screen'), - pytest.param(0x7E, '\u25c0', id='backspace_triangle'), - pytest.param(0x7F, '\u25b6', id='tab_triangle'), -]) + assert bytes([byte_val]).decode("atascii") == expected + + +@pytest.mark.parametrize( + "byte_val,expected", + [ + pytest.param(0x60, "\u2666", id="diamond"), + pytest.param(0x7B, "\u2660", id="spade"), + pytest.param(0x7C, "|", id="pipe"), + pytest.param(0x7D, "\u21b0", id="clear_screen"), + pytest.param(0x7E, "\u25c0", id="backspace_triangle"), + pytest.param(0x7F, "\u25b6", id="tab_triangle"), + ], +) def test_special_glyphs(byte_val, expected): - assert bytes([byte_val]).decode('atascii') == expected + assert bytes([byte_val]).decode("atascii") == expected def test_atascii_eol(): - assert b'\x9b'.decode('atascii') == '\n' - - -@pytest.mark.parametrize("byte_val,expected", [ - pytest.param(0x82, '\u258a', id='left_three_quarters'), - pytest.param(0x88, '\u25e4', id='upper_left_triangle'), - pytest.param(0x89, '\u259b', id='quadrant_UL_UR_LL'), - pytest.param(0x8A, '\u25e5', id='upper_right_triangle'), - pytest.param(0x8B, '\u2599', id='quadrant_UL_LL_LR'), - pytest.param(0x8C, '\u259f', id='quadrant_UR_LL_LR'), - pytest.param(0x8D, '\u2586', id='lower_three_quarters'), - pytest.param(0x8E, '\U0001fb85', id='upper_three_quarters'), - pytest.param(0x8F, '\u259c', id='quadrant_UL_UR_LR'), - pytest.param(0x94, '\u25d8', id='inverse_bullet'), - pytest.param(0x95, '\u2580', id='upper_half_block'), - pytest.param(0x96, '\U0001fb8a', id='right_three_quarters'), - pytest.param(0x99, '\u2590', id='right_half_block'), - pytest.param(0xA0, '\u2588', id='full_block'), -]) + assert b"\x9b".decode("atascii") == "\n" + + +@pytest.mark.parametrize( + "byte_val,expected", + [ + pytest.param(0x82, "\u258a", id="left_three_quarters"), + pytest.param(0x88, "\u25e4", id="upper_left_triangle"), + pytest.param(0x89, "\u259b", id="quadrant_UL_UR_LL"), + pytest.param(0x8A, "\u25e5", id="upper_right_triangle"), + pytest.param(0x8B, "\u2599", id="quadrant_UL_LL_LR"), + pytest.param(0x8C, "\u259f", id="quadrant_UR_LL_LR"), + pytest.param(0x8D, "\u2586", id="lower_three_quarters"), + pytest.param(0x8E, "\U0001fb85", id="upper_three_quarters"), + pytest.param(0x8F, "\u259c", id="quadrant_UL_UR_LR"), + pytest.param(0x94, "\u25d8", id="inverse_bullet"), + pytest.param(0x95, "\u2580", id="upper_half_block"), + pytest.param(0x96, "\U0001fb8a", id="right_three_quarters"), + pytest.param(0x99, "\u2590", id="right_half_block"), + pytest.param(0xA0, "\u2588", id="full_block"), + ], +) def test_inverse_distinct_glyphs(byte_val, expected): - assert bytes([byte_val]).decode('atascii') == expected + assert bytes([byte_val]).decode("atascii") == expected def test_inverse_shared_glyphs(): for byte_val in (0x80, 0x81, 0x83, 0x84, 0x85, 0x86, 0x87): - normal = bytes([byte_val & 0x7F]).decode('atascii') - inverse = bytes([byte_val]).decode('atascii') + normal = bytes([byte_val & 0x7F]).decode("atascii") + inverse = bytes([byte_val]).decode("atascii") assert inverse == normal def test_inverse_ascii_range(): for byte_val in range(0xA1, 0xFB): - normal = bytes([byte_val & 0x7F]).decode('atascii') - inverse = bytes([byte_val]).decode('atascii') + normal = bytes([byte_val & 0x7F]).decode("atascii") + inverse = bytes([byte_val]).decode("atascii") assert inverse == normal def test_full_decode_no_crash(): data = bytes(range(256)) - result = data.decode('atascii') + result = data.decode("atascii") assert len(result) == 256 def test_encode_eol(): - encoded, length = codecs.lookup('atascii').encode('\n') - assert encoded == b'\x9b' + encoded, length = codecs.lookup("atascii").encode("\n") + assert encoded == b"\x9b" assert length == 1 def test_encode_unique_chars(): - encoded, _ = codecs.lookup('atascii').encode('\u258a') - assert encoded == b'\x82' - encoded, _ = codecs.lookup('atascii').encode('\u25d8') - assert encoded == b'\x94' - encoded, _ = codecs.lookup('atascii').encode('\u2588') - assert encoded == b'\xa0' + encoded, _ = codecs.lookup("atascii").encode("\u258a") + assert encoded == b"\x82" + encoded, _ = codecs.lookup("atascii").encode("\u25d8") + assert encoded == b"\x94" + encoded, _ = codecs.lookup("atascii").encode("\u2588") + assert encoded == b"\xa0" def test_encode_charmap_prefers_normal_byte(): - encoded, _ = codecs.lookup('atascii').encode('\u2665') - assert encoded == b'\x00' - encoded, _ = codecs.lookup('atascii').encode('A') - assert encoded == b'\x41' + encoded, _ = codecs.lookup("atascii").encode("\u2665") + assert encoded == b"\x00" + encoded, _ = codecs.lookup("atascii").encode("A") + assert encoded == b"\x41" def test_incremental_decoder(): - decoder = codecs.getincrementaldecoder('atascii')() - assert decoder.decode(b'\x00', False) == '\u2665' - assert decoder.decode(b'AB', True) == 'AB' + decoder = codecs.getincrementaldecoder("atascii")() + assert decoder.decode(b"\x00", False) == "\u2665" + assert decoder.decode(b"AB", True) == "AB" def test_incremental_encoder(): - encoder = codecs.getincrementalencoder('atascii')() - assert encoder.encode('\u258a', False) == b'\x82' - assert encoder.encode('\n', True) == b'\x9b' + encoder = codecs.getincrementalencoder("atascii")() + assert encoder.encode("\u258a", False) == b"\x82" + assert encoder.encode("\n", True) == b"\x9b" def test_strict_error_on_unencodable(): with pytest.raises(UnicodeEncodeError): - '\u00e9'.encode('atascii') + "\u00e9".encode("atascii") def test_replace_error_mode(): - result = '\u00e9'.encode('atascii', errors='replace') - assert result == b'\x3f' + result = "\u00e9".encode("atascii", errors="replace") + assert result == b"\x3f" def test_ignore_error_mode(): - result = '\u00e9'.encode('atascii', errors='ignore') - assert result == b'' + result = "\u00e9".encode("atascii", errors="ignore") + assert result == b"" def test_encode_cr_as_eol(): - encoded, length = codecs.lookup('atascii').encode('\r') - assert encoded == b'\x9b' + encoded, length = codecs.lookup("atascii").encode("\r") + assert encoded == b"\x9b" assert length == 1 def test_encode_crlf_as_single_eol(): - encoded, length = codecs.lookup('atascii').encode('\r\n') - assert encoded == b'\x9b' + encoded, length = codecs.lookup("atascii").encode("\r\n") + assert encoded == b"\x9b" assert length == 1 def test_encode_mixed_line_endings(): - encoded, _ = codecs.lookup('atascii').encode('hello\r\nworld\r') - hello_eol = 'hello\n'.encode('atascii') - world_eol = 'world\n'.encode('atascii') + encoded, _ = codecs.lookup("atascii").encode("hello\r\nworld\r") + hello_eol = "hello\n".encode("atascii") + world_eol = "world\n".encode("atascii") assert encoded == hello_eol + world_eol def test_incremental_encoder_cr_then_lf(): - encoder = codecs.getincrementalencoder('atascii')() - result = encoder.encode('hello\r', final=False) - assert result == 'hello'.encode('atascii') - result = encoder.encode('\nworld', final=True) - assert result == '\nworld'.encode('atascii') + encoder = codecs.getincrementalencoder("atascii")() + result = encoder.encode("hello\r", final=False) + assert result == "hello".encode("atascii") + result = encoder.encode("\nworld", final=True) + assert result == "\nworld".encode("atascii") def test_incremental_encoder_cr_then_other(): - encoder = codecs.getincrementalencoder('atascii')() - result = encoder.encode('A\r', final=False) - assert result == 'A'.encode('atascii') - result = encoder.encode('B', final=True) - assert result == '\nB'.encode('atascii') + encoder = codecs.getincrementalencoder("atascii")() + result = encoder.encode("A\r", final=False) + assert result == "A".encode("atascii") + result = encoder.encode("B", final=True) + assert result == "\nB".encode("atascii") def test_incremental_encoder_cr_final(): - encoder = codecs.getincrementalencoder('atascii')() - result = encoder.encode('end\r', final=True) - assert result == 'end\n'.encode('atascii') + encoder = codecs.getincrementalencoder("atascii")() + result = encoder.encode("end\r", final=True) + assert result == "end\n".encode("atascii") def test_incremental_encoder_reset(): - encoder = codecs.getincrementalencoder('atascii')() - encoder.encode('A\r', final=False) + encoder = codecs.getincrementalencoder("atascii")() + encoder.encode("A\r", final=False) encoder.reset() assert encoder.getstate() == 0 def test_incremental_encoder_getstate_setstate(): - encoder = codecs.getincrementalencoder('atascii')() - encoder.encode('A\r', final=False) + encoder = codecs.getincrementalencoder("atascii")() + encoder.encode("A\r", final=False) assert encoder.getstate() == 1 state = encoder.getstate() - encoder2 = codecs.getincrementalencoder('atascii')() + encoder2 = codecs.getincrementalencoder("atascii")() encoder2.setstate(state) - result = encoder2.encode('\n', final=True) - assert result == b'\x9b' + result = encoder2.encode("\n", final=True) + assert result == b"\x9b" def test_native_graphics_0x0a_0x0d(): - assert b'\x0a'.decode('atascii') == '\u25e3' - assert b'\x0d'.decode('atascii') == '\U0001fb82' + assert b"\x0a".decode("atascii") == "\u25e3" + assert b"\x0d".decode("atascii") == "\U0001fb82" def test_decoding_table_length(): diff --git a/telnetlib3/tests/test_charset.py b/telnetlib3/tests/test_charset.py index 9a60e7dc..8175a365 100644 --- a/telnetlib3/tests/test_charset.py +++ b/telnetlib3/tests/test_charset.py @@ -9,7 +9,7 @@ import telnetlib3.stream_writer from telnetlib3.telopt import DO, SB, SE, IAC, WILL, WONT, TTYPE, CHARSET, REQUEST, ACCEPTED from telnetlib3.stream_writer import TelnetWriter -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( bind_host, create_server, asyncio_server, diff --git a/telnetlib3/tests/test_client_shell.py b/telnetlib3/tests/test_client_shell.py index 8b15be29..67bc871f 100644 --- a/telnetlib3/tests/test_client_shell.py +++ b/telnetlib3/tests/test_client_shell.py @@ -3,15 +3,17 @@ # std imports import sys import types +import asyncio +from unittest import mock # 3rd party import pytest +import pexpect if sys.platform == "win32": pytest.skip("POSIX-only tests", allow_module_level=True) # std imports -# std imports (POSIX only) import termios # noqa: E402 # local @@ -20,17 +22,34 @@ _INPUT_SEQ_XLAT, Terminal, InputFilter, + _send_stdin, ) -def _make_writer(will_echo: bool = False, raw_mode: bool = False) -> object: +class _MockOption: + """Minimal mock for stream_writer.Option.""" + + def __init__(self, opts: "dict[bytes, bool]") -> None: + self._opts = opts + + def enabled(self, key: bytes) -> bool: + return self._opts.get(key, False) + + +def _make_writer( + will_echo: bool = False, raw_mode: "bool | None" = False, will_sga: bool = False +) -> object: """Build a minimal mock writer with the attributes Terminal needs.""" + from telnetlib3.telopt import SGA # pylint: disable=import-outside-toplevel + writer = types.SimpleNamespace( will_echo=will_echo, + client=True, + remote_option=_MockOption({SGA: will_sga}), log=types.SimpleNamespace(debug=lambda *a, **kw: None), ) - if raw_mode: - writer._raw_mode = True + if raw_mode is not False: + writer._raw_mode = raw_mode return writer @@ -43,172 +62,619 @@ def _cooked_mode() -> "Terminal.ModeDef": lflag=termios.ICANON | termios.ECHO | termios.ISIG | termios.IEXTEN, ispeed=termios.B38400, ospeed=termios.B38400, - cc=[b'\x00'] * termios.NCCS, + cc=[b"\x00"] * termios.NCCS, ) -class TestDetermineMode: - def test_linemode_when_no_echo_no_raw(self) -> None: - writer = _make_writer(will_echo=False, raw_mode=False) - term = Terminal.__new__(Terminal) - term.telnet_writer = writer - mode = _cooked_mode() - result = term.determine_mode(mode) - assert result is mode - - def test_raw_mode_when_will_echo(self) -> None: - writer = _make_writer(will_echo=True, raw_mode=False) - term = Terminal.__new__(Terminal) - term.telnet_writer = writer - mode = _cooked_mode() - result = term.determine_mode(mode) - assert result is not mode - assert not (result.lflag & termios.ICANON) - assert not (result.lflag & termios.ECHO) - - def test_raw_mode_when_force_raw(self) -> None: - writer = _make_writer(will_echo=False, raw_mode=True) - term = Terminal.__new__(Terminal) - term.telnet_writer = writer - mode = _cooked_mode() - result = term.determine_mode(mode) - assert result is not mode - assert not (result.lflag & termios.ICANON) - assert not (result.lflag & termios.ECHO) - assert not (result.oflag & termios.OPOST) - - def test_raw_mode_when_both_echo_and_raw(self) -> None: - writer = _make_writer(will_echo=True, raw_mode=True) - term = Terminal.__new__(Terminal) - term.telnet_writer = writer - mode = _cooked_mode() - result = term.determine_mode(mode) - assert result is not mode - assert not (result.lflag & termios.ICANON) - assert not (result.lflag & termios.ECHO) - - -class TestInputXlat: - def test_atascii_del_to_backspace(self) -> None: - assert _INPUT_XLAT["atascii"][0x7F] == 0x7E - - def test_atascii_bs_to_backspace(self) -> None: - assert _INPUT_XLAT["atascii"][0x08] == 0x7E - - def test_atascii_cr_to_eol(self) -> None: - assert _INPUT_XLAT["atascii"][0x0D] == 0x9B - - def test_atascii_lf_to_eol(self) -> None: - assert _INPUT_XLAT["atascii"][0x0A] == 0x9B - - def test_petscii_del_to_backspace(self) -> None: - assert _INPUT_XLAT["petscii"][0x7F] == 0x14 - - def test_petscii_bs_to_backspace(self) -> None: - assert _INPUT_XLAT["petscii"][0x08] == 0x14 - - def test_normal_bytes_not_in_xlat(self) -> None: - xlat = _INPUT_XLAT["atascii"] - for b in (ord('a'), ord('A'), ord('1'), ord(' ')): - assert b not in xlat - - -class TestInputFilterAtascii: - @staticmethod - def _make_filter() -> InputFilter: - return InputFilter( - _INPUT_SEQ_XLAT["atascii"], _INPUT_XLAT["atascii"] - ) - - @pytest.mark.parametrize("seq,expected", list(_INPUT_SEQ_XLAT["atascii"].items())) - def test_sequence_translated(self, seq: bytes, expected: bytes) -> None: - f = self._make_filter() - assert f.feed(seq) == expected - - def test_passthrough_ascii(self) -> None: - f = self._make_filter() - assert f.feed(b"hello") == b"hello" - - def test_mixed_text_and_sequence(self) -> None: - f = self._make_filter() - assert f.feed(b"hi\x1b[Alo") == b"hi\x1clo" - - def test_multiple_sequences(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1b[A\x1b[B\x1b[C\x1b[D") == b"\x1c\x1d\x1f\x1e" - - def test_single_byte_xlat_applied(self) -> None: - f = self._make_filter() - assert f.feed(b"\x7f") == b"\x7e" - assert f.feed(b"\x08") == b"\x7e" - - def test_cr_to_atascii_eol(self) -> None: - f = self._make_filter() - assert f.feed(b"\r") == b"\x9b" - - def test_lf_to_atascii_eol(self) -> None: - f = self._make_filter() - assert f.feed(b"\n") == b"\x9b" - - def test_text_with_enter(self) -> None: - f = self._make_filter() - assert f.feed(b"hello\r") == b"hello\x9b" - - def test_split_sequence_buffered(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1b") == b"" - assert f.feed(b"[A") == b"\x1c" - - def test_split_sequence_mid_csi(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1b[") == b"" - assert f.feed(b"A") == b"\x1c" - - def test_bare_esc_flushed_on_non_prefix(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1b") == b"" - assert f.feed(b"x") == b"\x1bx" - - def test_delete_key_sequence(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1b[3~") == b"\x7e" - - def test_ss3_arrow_keys(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1bOA") == b"\x1c" - assert f.feed(b"\x1bOD") == b"\x1e" - - -class TestInputFilterPetscii: - @staticmethod - def _make_filter() -> InputFilter: - return InputFilter( - _INPUT_SEQ_XLAT["petscii"], _INPUT_XLAT["petscii"] - ) +def _make_term(writer: object) -> Terminal: + """Build a Terminal instance without __init__ side effects.""" + term = Terminal.__new__(Terminal) + term.telnet_writer = writer + return term + + +def _make_atascii_filter() -> InputFilter: + return InputFilter(_INPUT_SEQ_XLAT["atascii"], _INPUT_XLAT["atascii"]) + + +def _make_petscii_filter() -> InputFilter: + return InputFilter(_INPUT_SEQ_XLAT["petscii"], _INPUT_XLAT["petscii"]) + + +@pytest.mark.parametrize( + "will_echo,raw_mode,will_sga", + [(False, None, False), (False, False, False), (True, False, False), (False, False, True)], +) +def test_determine_mode_unchanged(will_echo: bool, raw_mode: "bool | None", will_sga: bool) -> None: + term = _make_term(_make_writer(will_echo=will_echo, raw_mode=raw_mode, will_sga=will_sga)) + mode = _cooked_mode() + assert term.determine_mode(mode) is mode + + +@pytest.mark.parametrize( + "will_echo,raw_mode,will_sga", + [ + (True, None, False), + (False, None, True), + (True, None, True), + (False, True, False), + (True, True, False), + ], +) +def test_determine_mode_goes_raw(will_echo: bool, raw_mode: "bool | None", will_sga: bool) -> None: + term = _make_term(_make_writer(will_echo=will_echo, raw_mode=raw_mode, will_sga=will_sga)) + mode = _cooked_mode() + result = term.determine_mode(mode) + assert result is not mode + assert not result.lflag & termios.ICANON + assert not result.lflag & termios.ECHO + + +def test_determine_mode_sga_sets_software_echo() -> None: + term = _make_term(_make_writer(will_sga=True, raw_mode=None)) + term.determine_mode(_cooked_mode()) + assert term.software_echo is True + + +def test_make_raw_suppress_echo() -> None: + term = _make_term(_make_writer(raw_mode=None)) + result = term._make_raw(_cooked_mode(), suppress_echo=True) + assert not result.lflag & termios.ICANON + assert not result.lflag & termios.ECHO + assert not result.oflag & termios.OPOST + assert result.cc[termios.VMIN] == 1 + assert result.cc[termios.VTIME] == 0 + + +def test_make_raw_keep_echo() -> None: + term = _make_term(_make_writer(raw_mode=None)) + result = term._make_raw(_cooked_mode(), suppress_echo=False) + assert not result.lflag & termios.ICANON + assert result.lflag & termios.ECHO + assert not result.oflag & termios.OPOST + + +def test_echo_toggle_password_flow() -> None: + writer = _make_writer(raw_mode=None) + term = _make_term(writer) + mode = _cooked_mode() + + r1 = term.determine_mode(mode) + assert r1.lflag & termios.ECHO + + writer.will_echo = True + r2 = term.determine_mode(mode) + assert not r2.lflag & termios.ECHO + assert not r2.lflag & termios.ICANON + + writer.will_echo = False + r3 = term.determine_mode(mode) + assert r3.lflag & termios.ECHO + assert r3.lflag & termios.ICANON + + +def test_echo_toggle_sga_keeps_raw() -> None: + writer = _make_writer(will_sga=True, raw_mode=None) + term = _make_term(writer) + mode = _cooked_mode() + + writer.will_echo = True + r1 = term.determine_mode(mode) + assert not r1.lflag & termios.ECHO + assert not r1.lflag & termios.ICANON + + writer.will_echo = False + r2 = term.determine_mode(mode) + assert not r2.lflag & termios.ECHO + assert not r2.lflag & termios.ICANON + + +def test_make_raw_toggle_echo_flag() -> None: + term = _make_term(_make_writer(raw_mode=None)) + mode = _cooked_mode() + suppressed = term._make_raw(mode, suppress_echo=True) + assert not suppressed.lflag & termios.ECHO + restored = term._make_raw(mode, suppress_echo=False) + assert restored.lflag & termios.ECHO + assert not restored.lflag & termios.ICANON + + +@pytest.mark.parametrize( + "encoding,key,expected", + [ + ("atascii", 0x7F, 0x7E), + ("atascii", 0x08, 0x7E), + ("atascii", 0x0D, 0x9B), + ("atascii", 0x0A, 0x9B), + ("petscii", 0x7F, 0x14), + ("petscii", 0x08, 0x14), + ], +) +def test_input_xlat(encoding: str, key: int, expected: int) -> None: + assert _INPUT_XLAT[encoding][key] == expected + + +def test_input_xlat_normal_bytes_absent() -> None: + xlat = _INPUT_XLAT["atascii"] + for b in (ord("a"), ord("A"), ord("1"), ord(" ")): + assert b not in xlat + + +@pytest.mark.parametrize("seq,expected", list(_INPUT_SEQ_XLAT["atascii"].items())) +def test_atascii_sequence_translated(seq: bytes, expected: bytes) -> None: + assert _make_atascii_filter().feed(seq) == expected + + +@pytest.mark.parametrize("seq,expected", list(_INPUT_SEQ_XLAT["petscii"].items())) +def test_petscii_sequence_translated(seq: bytes, expected: bytes) -> None: + assert _make_petscii_filter().feed(seq) == expected + + +@pytest.mark.parametrize( + "data,expected", + [ + (b"hello", b"hello"), + (b"hi\x1b[Alo", b"hi\x1clo"), + (b"\x1b[A\x1b[B\x1b[C\x1b[D", b"\x1c\x1d\x1f\x1e"), + (b"\x7f", b"\x7e"), + (b"\x08", b"\x7e"), + (b"\r", b"\x9b"), + (b"\n", b"\x9b"), + (b"hello\r", b"hello\x9b"), + ], +) +def test_atascii_filter_feed(data: bytes, expected: bytes) -> None: + assert _make_atascii_filter().feed(data) == expected + + +def test_petscii_filter_byte_xlat() -> None: + assert _make_petscii_filter().feed(b"\x7f") == b"\x14" + + +@pytest.mark.parametrize( + "chunks,expected_chunks", + [ + ((b"\x1b", b"[A"), (b"", b"\x1c")), + ((b"\x1b[", b"A"), (b"", b"\x1c")), + ((b"\x1b", b"x"), (b"", b"\x1bx")), + ], +) +def test_atascii_filter_split_feed( + chunks: "tuple[bytes, ...]", expected_chunks: "tuple[bytes, ...]" +) -> None: + f = _make_atascii_filter() + for chunk, expected in zip(chunks, expected_chunks): + assert f.feed(chunk) == expected + + +def test_filter_no_xlat_passthrough() -> None: + assert InputFilter({}, {}).feed(b"hello\x1b[Aworld") == b"hello\x1b[Aworld" - @pytest.mark.parametrize("seq,expected", list(_INPUT_SEQ_XLAT["petscii"].items())) - def test_sequence_translated(self, seq: bytes, expected: bytes) -> None: - f = self._make_filter() - assert f.feed(seq) == expected - def test_home_key(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1b[H") == b"\x13" +def test_filter_empty_feed() -> None: + assert InputFilter({}, {}).feed(b"") == b"" - def test_insert_key(self) -> None: - f = self._make_filter() - assert f.feed(b"\x1b[2~") == b"\x94" - - def test_single_byte_xlat_applied(self) -> None: - f = self._make_filter() - assert f.feed(b"\x7f") == b"\x14" - - -class TestInputFilterEmpty: - def test_no_xlat_passthrough(self) -> None: - f = InputFilter({}, {}) - assert f.feed(b"hello\x1b[Aworld") == b"hello\x1b[Aworld" - - def test_empty_feed(self) -> None: - f = InputFilter({}, {}) - assert f.feed(b"") == b"" + +@pytest.mark.parametrize("data,pending", [(b"\x1b", True), (b"\x1b[A", False), (b"x", False)]) +def test_filter_has_pending(data: bytes, pending: bool) -> None: + f = _make_atascii_filter() + f.feed(data) + assert f.has_pending == pending + + +@pytest.mark.parametrize("data,expected", [(b"\x1b", b"\x1b"), (b"\x1b[", b"\x1b[")]) +def test_filter_flush(data: bytes, expected: bytes) -> None: + f = _make_atascii_filter() + f.feed(data) + assert f.flush() == expected + assert not f.has_pending + + +def test_filter_flush_empty() -> None: + assert _make_atascii_filter().flush() == b"" + + +def test_filter_flush_applies_byte_xlat() -> None: + f = InputFilter({b"\x1b[A": b"\x1c"}, {0x1B: 0xFF}) + f.feed(b"\x1b") + assert f.flush() == b"\xff" + + +def test_filter_default_esc_delay() -> None: + assert _make_atascii_filter().esc_delay == 0.35 + + +def test_filter_custom_esc_delay() -> None: + assert InputFilter({}, {}, esc_delay=0.1).esc_delay == 0.1 + + +def test_filter_ansi_passthrough_with_empty_seq_xlat() -> None: + f = InputFilter({}, _INPUT_XLAT["atascii"]) + assert f.feed(b"\x1b[A") == b"\x1b[A" + + +def test_filter_byte_xlat_without_seq_xlat() -> None: + f = InputFilter({}, _INPUT_XLAT["atascii"]) + assert f.feed(b"\x7f") == b"\x7e" + + +def test_filter_esc_not_buffered_without_seq_xlat() -> None: + f = InputFilter({}, _INPUT_XLAT["atascii"]) + assert f.feed(b"\x1b") == b"\x1b" + assert not f.has_pending + + +@pytest.mark.parametrize("data,expected", [(b"\r", b"\r"), (b"\n", b"\n"), (b"\x7f", b"\x7e")]) +def test_filter_without_eol_xlat(data: bytes, expected: bytes) -> None: + byte_xlat = dict(_INPUT_XLAT["atascii"]) + byte_xlat.pop(0x0D, None) + byte_xlat.pop(0x0A, None) + f = InputFilter(_INPUT_SEQ_XLAT["atascii"], byte_xlat) + assert f.feed(data) == expected + + +# std imports +import os # noqa: E402 +import time as _time # noqa: E402 +import select # noqa: E402 +import tempfile # noqa: E402 +import contextlib # noqa: E402 +import subprocess # noqa: E402 + +# local +from telnetlib3.tests.accessories import ( # noqa: E402 + bind_host, + asyncio_server, + unused_tcp_port, +) + +_IAC = b"\xff" +_WILL = b"\xfb" +_WONT = b"\xfc" +_ECHO = b"\x01" +_SGA = b"\x03" + + +def _strip_iac(data: bytes) -> bytes: + """Remove IAC command sequences from raw data for protocol servers.""" + result = bytearray() + i = 0 + while i < len(data): + if data[i] == 0xFF and i + 2 < len(data): + i += 3 + else: + result.append(data[i]) + i += 1 + return bytes(result) + + +def _client_cmd(host: str, port: int, extra: "list[str] | None" = None) -> "list[str]": + prog = pexpect.which("telnetlib3-client") + assert prog is not None + args = [ + prog, + host, + str(port), + "--connect-minwait=0.05", + "--connect-maxwait=0.5", + "--colormatch=none", + ] + if extra: + args.extend(extra) + return args + + +def _pty_read( + master_fd: int, + proc: "subprocess.Popen[bytes] | None" = None, + marker: "bytes | None" = None, + timeout: float = 8.0, +) -> bytes: + """Read from PTY master until *marker* appears, process exits, or timeout.""" + buf = b"" + deadline = _time.monotonic() + timeout + while _time.monotonic() < deadline: + remaining = deadline - _time.monotonic() + if remaining <= 0: + break + ready, _, _ = select.select([master_fd], [], [], min(remaining, 0.1)) + if ready: + try: + chunk = os.read(master_fd, 4096) + except OSError: + break + if not chunk: + break + buf += chunk + if marker is not None and marker in buf: + return buf + elif proc is not None and proc.poll() is not None: + while True: + r, _, _ = select.select([master_fd], [], [], 0) + if not r: + break + try: + chunk = os.read(master_fd, 4096) + except OSError: + break + if not chunk: + break + buf += chunk + break + return buf + + +def _coverage_env() -> "dict[str, str]": + """Build env dict that enables coverage tracking in subprocess.""" + env = os.environ.copy() + project_root = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) + coveragerc = os.path.join(project_root, "tox.ini") + if os.path.exists(coveragerc): + env["COVERAGE_PROCESS_START"] = os.path.abspath(coveragerc) + return env + + +@contextlib.contextmanager +def _pty_client(cmd: "list[str]"): + """ + Spawn client with stdin/stdout on a PTY; yields (proc, master_fd). + + When coverage.py is available, sets COVERAGE_PROCESS_START so the + subprocess records coverage data (requires ``coverage_subprocess.pth`` + or equivalent installed in site-packages). + """ + master_fd, slave_fd = os.openpty() + tmpdir = tempfile.mkdtemp(prefix="telnetlib3_cov_") + sitecust = os.path.join(tmpdir, "sitecustomize.py") + with open(sitecust, "w", encoding="utf-8") as f: + f.write("import coverage\ncoverage.process_startup()\n") + env = _coverage_env() + pythonpath = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = tmpdir + (os.pathsep + pythonpath if pythonpath else "") + proc = subprocess.Popen( + cmd, stdin=slave_fd, stdout=slave_fd, stderr=subprocess.DEVNULL, close_fds=True, env=env + ) + os.close(slave_fd) + try: + yield proc, master_fd + finally: + os.close(master_fd) + try: + proc.wait(timeout=3) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=2) + try: + os.unlink(sitecust) + os.rmdir(tmpdir) + except OSError: + pass + + +@pytest.mark.parametrize( + "server_msg,delay,extra_args,expected", + [ + ( + b"hello from server\r\n", + 0.5, + None, + [b"Escape character", b"hello from server", b"Connection closed by foreign host."], + ), + (b"raw server\r\n", 0.1, ["--raw-mode"], [b"raw server"]), + ], +) +async def test_simple_server_output( + bind_host: str, + unused_tcp_port: int, + server_msg: bytes, + delay: float, + extra_args: "list[str] | None", + expected: "list[bytes]", +) -> None: + class Proto(asyncio.Protocol): + def connection_made(self, transport): + super().connection_made(transport) + transport.write(server_msg) + asyncio.get_event_loop().call_later(delay, transport.close) + + async with asyncio_server(Proto, bind_host, unused_tcp_port): + cmd = _client_cmd(bind_host, unused_tcp_port, extra_args) + with _pty_client(cmd) as (proc, master_fd): + output = await asyncio.to_thread(_pty_read, master_fd, proc=proc) + for marker in expected: + assert marker in output + + +@pytest.mark.parametrize( + "prompt,response,extra_args,send,expected", + [ + (b"login: ", b"\r\nwelcome!\r\n", None, b"user\r", [b"login:", b"welcome!"]), + (b"prompt> ", b"\r\ngot it\r\n", ["--line-mode"], b"hello\r", [b"got it", b"hello"]), + ], +) +async def test_echo_sga_interaction( + bind_host: str, + unused_tcp_port: int, + prompt: bytes, + response: bytes, + extra_args: "list[str] | None", + send: bytes, + expected: "list[bytes]", +) -> None: + class Proto(asyncio.Protocol): + def connection_made(self, transport): + super().connection_made(transport) + transport.write(_IAC + _WILL + _ECHO + _IAC + _WILL + _SGA + prompt) + self._transport = transport + + def data_received(self, data): + self._transport.write(response) + asyncio.get_event_loop().call_later(0.1, self._transport.close) + + async with asyncio_server(Proto, bind_host, unused_tcp_port): + cmd = _client_cmd(bind_host, unused_tcp_port, extra_args) + + def _interact(master_fd, proc): + buf = _pty_read(master_fd, marker=prompt.rstrip()) + os.write(master_fd, send) + buf += _pty_read(master_fd, proc=proc) + return buf + + with _pty_client(cmd) as (proc, master_fd): + output = await asyncio.to_thread(_interact, master_fd, proc) + for marker in expected: + assert marker in output + + +async def test_password_hidden_then_echo_restored(bind_host: str, unused_tcp_port: int) -> None: + class Proto(asyncio.Protocol): + def __init__(self): + self._state = "name" + self._buf = b"" + + def connection_made(self, transport): + super().connection_made(transport) + self._transport = transport + transport.write(b"Name: ") + + def data_received(self, data): + clean = _strip_iac(data) + if not clean: + return + self._buf += clean + if b"\r" not in self._buf and b"\n" not in self._buf: + return + self._buf = b"" + if self._state == "name": + self._state = "pass" + self._transport.write(_IAC + _WILL + _ECHO + b"\r\nPassword: ") + elif self._state == "pass": + self._state = "done" + self._transport.write(_IAC + _WONT + _ECHO + b"\r\nLogged in.\r\n") + asyncio.get_event_loop().call_later(0.2, self._transport.close) + + async with asyncio_server(Proto, bind_host, unused_tcp_port): + cmd = _client_cmd(bind_host, unused_tcp_port) + + def _interact(master_fd, proc): + buf = _pty_read(master_fd, marker=b"Name:", timeout=10.0) + os.write(master_fd, b"admin\r") + buf += _pty_read(master_fd, marker=b"Password:", timeout=10.0) + os.write(master_fd, b"secret\r") + buf += _pty_read(master_fd, proc=proc) + return buf + + with _pty_client(cmd) as (proc, master_fd): + output = await asyncio.to_thread(_interact, master_fd, proc) + assert b"Logged in." in output + after_prompt = output.split(b"Password:")[-1] + assert b"secret" not in after_prompt + + +async def test_backspace_visual_erase(bind_host: str, unused_tcp_port: int) -> None: + class Proto(asyncio.Protocol): + def __init__(self): + self._state = "login" + self._buf = b"" + + def connection_made(self, transport): + super().connection_made(transport) + self._transport = transport + transport.write(_IAC + _WILL + _ECHO + _IAC + _WILL + _SGA + b"login: ") + + def data_received(self, data): + clean = _strip_iac(data) + if not clean: + return + self._buf += clean + if b"\r" not in self._buf and b"\n" not in self._buf: + return + self._buf = b"" + if self._state == "login": + self._state = "cmd" + self._transport.write(_IAC + _WONT + _ECHO + b"\r\nType here> ") + elif self._state == "cmd": + self._state = "done" + self._transport.write(b"\r\ndone\r\n") + asyncio.get_event_loop().call_later(0.2, self._transport.close) + + async with asyncio_server(Proto, bind_host, unused_tcp_port): + cmd = _client_cmd(bind_host, unused_tcp_port) + + def _interact(master_fd, proc): + buf = _pty_read(master_fd, marker=b"login:", timeout=10.0) + os.write(master_fd, b"user\r") + buf += _pty_read(master_fd, marker=b"Type here>", timeout=10.0) + os.write(master_fd, b"ab\x7fc\r") + buf += _pty_read(master_fd, proc=proc) + return buf + + with _pty_client(cmd) as (proc, master_fd): + output = await asyncio.to_thread(_interact, master_fd, proc) + after_prompt = output.split(b"Type here>")[-1] + assert b"\x08 \x08" in after_prompt + assert b"^?" not in after_prompt + + +def test_check_auto_mode_not_istty() -> None: + """check_auto_mode returns None when not attached to a TTY.""" + writer = _make_writer(will_echo=True, will_sga=True) + term = _make_term(writer) + term._istty = False + assert term.check_auto_mode(switched_to_raw=False, last_will_echo=False) is None + + +async def test_setup_winch_registers_handler() -> None: + """setup_winch registers SIGWINCH handler when istty is True.""" + writer = _make_writer() + writer.local_option = _MockOption({}) + writer.is_closing = lambda: False + term = _make_term(writer) + term._istty = True + term._winch_handle = None + term.setup_winch() + assert term._remove_winch is True + term.cleanup_winch() + assert term._remove_winch is False + + +async def test_send_stdin_with_input_filter() -> None: + """_send_stdin feeds bytes through input filter and writes translated.""" + inf = InputFilter(_INPUT_SEQ_XLAT["atascii"], _INPUT_XLAT["atascii"]) + + writer = _make_writer() + writer._input_filter = inf + writer._write = mock.Mock() + stdout = mock.Mock() + + new_timer, pending = _send_stdin(b"\x1b[A", writer, stdout, local_echo=False) + assert not pending + assert new_timer is None + writer._write.assert_called_once_with(b"\x1c") + + +async def test_send_stdin_with_pending_sequence() -> None: + """_send_stdin returns pending=True when partial sequence is buffered.""" + inf = InputFilter(_INPUT_SEQ_XLAT["atascii"], _INPUT_XLAT["atascii"]) + + writer = _make_writer() + writer._input_filter = inf + writer._write = mock.Mock() + stdout = mock.Mock() + + new_timer, pending = _send_stdin(b"\x1b", writer, stdout, local_echo=False) + assert pending is True + assert new_timer is not None + new_timer.cancel() + + +async def test_send_stdin_no_filter() -> None: + """_send_stdin without input filter calls writer._write directly.""" + writer = _make_writer() + writer._write = mock.Mock() + stdout = mock.Mock() + + new_timer, pending = _send_stdin(b"hello", writer, stdout, local_echo=False) + assert not pending + assert new_timer is None + writer._write.assert_called_once_with(b"hello") diff --git a/telnetlib3/tests/test_client_unit.py b/telnetlib3/tests/test_client_unit.py index 82921615..da04031a 100644 --- a/telnetlib3/tests/test_client_unit.py +++ b/telnetlib3/tests/test_client_unit.py @@ -6,7 +6,7 @@ # local from telnetlib3 import client as cl -from telnetlib3.tests.accessories import ( # noqa: F401 # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( # noqa: F401 bind_host, create_server, ) @@ -126,7 +126,6 @@ async def test_send_xdisploc(): @pytest.mark.skipif(sys.platform == "win32", reason="requires fcntl") def test_terminal_client_winsize_success(monkeypatch): - # std imports import fcntl import struct @@ -137,7 +136,6 @@ def test_terminal_client_winsize_success(monkeypatch): @pytest.mark.skipif(sys.platform == "win32", reason="requires fcntl") def test_terminal_client_winsize_ioerror(monkeypatch): - # std imports import fcntl monkeypatch.setenv("LINES", "30") @@ -153,7 +151,6 @@ def _raise(*args, **kwargs): @pytest.mark.skipif(sys.platform == "win32", reason="requires fcntl") @pytest.mark.asyncio async def test_terminal_client_send_naws(monkeypatch): - # std imports import fcntl monkeypatch.setenv("LINES", "48") @@ -165,7 +162,6 @@ async def test_terminal_client_send_naws(monkeypatch): @pytest.mark.skipif(sys.platform == "win32", reason="requires fcntl") @pytest.mark.asyncio async def test_terminal_client_send_env(monkeypatch): - # std imports import fcntl def _raise(*args, **kwargs): diff --git a/telnetlib3/tests/test_color_filter.py b/telnetlib3/tests/test_color_filter.py index d8139b49..2ec475c3 100644 --- a/telnetlib3/tests/test_color_filter.py +++ b/telnetlib3/tests/test_color_filter.py @@ -467,24 +467,27 @@ def _make_filter(self, **kwargs: object) -> PetsciiColorFilter: cfg = ColorConfig(brightness=1.0, contrast=1.0, **kwargs) # type: ignore[arg-type] return PetsciiColorFilter(cfg) - @pytest.mark.parametrize("ctrl_char,palette_idx", [ - ('\x05', 1), - ('\x1c', 2), - ('\x1e', 5), - ('\x1f', 6), - ('\x81', 8), - ('\x90', 0), - ('\x95', 9), - ('\x96', 10), - ('\x97', 11), - ('\x98', 12), - ('\x99', 13), - ('\x9a', 14), - ('\x9b', 15), - ('\x9c', 4), - ('\x9e', 7), - ('\x9f', 3), - ]) + @pytest.mark.parametrize( + "ctrl_char,palette_idx", + [ + ("\x05", 1), + ("\x1c", 2), + ("\x1e", 5), + ("\x1f", 6), + ("\x81", 8), + ("\x90", 0), + ("\x95", 9), + ("\x96", 10), + ("\x97", 11), + ("\x98", 12), + ("\x99", 13), + ("\x9a", 14), + ("\x9b", 15), + ("\x9c", 4), + ("\x9e", 7), + ("\x9f", 3), + ], + ) def test_color_code_to_24bit(self, ctrl_char: str, palette_idx: int) -> None: f = self._make_filter() result = f.filter(f"hello{ctrl_char}world") @@ -539,7 +542,7 @@ def test_cursor_controls_translated(self) -> None: def test_flush_returns_empty(self) -> None: f = self._make_filter() - assert f.flush() == "" + assert not f.flush() def test_brightness_contrast_applied(self) -> None: f_full = PetsciiColorFilter(ColorConfig(brightness=1.0, contrast=1.0)) @@ -555,15 +558,18 @@ def test_default_config(self) -> None: class TestAtasciiControlFilter: - @pytest.mark.parametrize("glyph,expected", [ - ('\u25c0', '\x08\x1b[P'), - ('\u25b6', '\t'), - ('\u21b0', '\x1b[2J\x1b[H'), - ('\u2191', '\x1b[A'), - ('\u2193', '\x1b[B'), - ('\u2190', '\x1b[D'), - ('\u2192', '\x1b[C'), - ]) + @pytest.mark.parametrize( + "glyph,expected", + [ + ("\u25c0", "\x08\x1b[P"), + ("\u25b6", "\t"), + ("\u21b0", "\x1b[2J\x1b[H"), + ("\u2191", "\x1b[A"), + ("\u2193", "\x1b[B"), + ("\u2190", "\x1b[D"), + ("\u2192", "\x1b[C"), + ], + ) def test_control_glyph_translated(self, glyph: str, expected: str) -> None: f = AtasciiControlFilter() result = f.filter(f"before{glyph}after") @@ -585,7 +591,7 @@ def test_atascii_graphics_unchanged(self) -> None: def test_flush_returns_empty(self) -> None: f = AtasciiControlFilter() - assert f.flush() == "" + assert not f.flush() def test_multiple_controls_in_one_string(self) -> None: f = AtasciiControlFilter() diff --git a/telnetlib3/tests/test_core.py b/telnetlib3/tests/test_core.py index 3b7510d9..fde7458c 100644 --- a/telnetlib3/tests/test_core.py +++ b/telnetlib3/tests/test_core.py @@ -15,7 +15,7 @@ # local import telnetlib3 from telnetlib3.telopt import DO, SB, IAC, SGA, NAWS, WILL, WONT, TTYPE, BINARY, CHARSET -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( bind_host, create_server, asyncio_server, @@ -499,7 +499,7 @@ async def shell(reader, writer): logfile_output = f.read().splitlines() assert stdout == ( b"Escape character is '^]'.\n" - b"Press Return to continue:\r\ngoodbye.\r\n" + b"Press Return to continue:\ngoodbye.\n" b"\x1b[m\nConnection closed by foreign host.\n" ) assert len(logfile_output) in (2, 3), logfile diff --git a/telnetlib3/tests/test_encoding.py b/telnetlib3/tests/test_encoding.py index f244c47d..0b3b9aa4 100644 --- a/telnetlib3/tests/test_encoding.py +++ b/telnetlib3/tests/test_encoding.py @@ -10,7 +10,7 @@ import telnetlib3 import telnetlib3.stream_writer from telnetlib3.telopt import DO, IS, SB, SE, IAC, WILL, WONT, TTYPE, BINARY, NEW_ENVIRON -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, asyncio_server, diff --git a/telnetlib3/tests/test_environ.py b/telnetlib3/tests/test_environ.py index 8086dce1..9ee8e79c 100644 --- a/telnetlib3/tests/test_environ.py +++ b/telnetlib3/tests/test_environ.py @@ -10,7 +10,7 @@ import telnetlib3 import telnetlib3.stream_writer from telnetlib3.telopt import DO, IS, SB, SE, IAC, VAR, WILL, TTYPE, USERVAR, NEW_ENVIRON -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, diff --git a/telnetlib3/tests/test_fingerprinting.py b/telnetlib3/tests/test_fingerprinting.py index 35a19897..128ce481 100644 --- a/telnetlib3/tests/test_fingerprinting.py +++ b/telnetlib3/tests/test_fingerprinting.py @@ -15,14 +15,13 @@ from telnetlib3 import fingerprinting as fps if sys.platform != "win32": - # local from telnetlib3 import server_pty_shell from telnetlib3 import fingerprinting_display as fpd else: server_pty_shell = None # type: ignore[assignment] # local -from telnetlib3.tests.accessories import ( # noqa: F401 # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( # noqa: F401 bind_host, create_server, open_connection, @@ -957,7 +956,6 @@ async def fake_pty_shell(reader, writer, exe, args, raw_mode=False): ], ) def test_cooked_input(monkeypatch, input_fn, expected): - # std imports import termios fake_attrs = [0, 0, 0, 0, 0, 0, [b"\x00"] * 32] @@ -1061,7 +1059,6 @@ def test_fingerprint_server_main_data_dir_flag(tmp_path, monkeypatch): monkeypatch.setattr("telnetlib3.fingerprinting.asyncio.run", _noop_asyncio_run) captured: dict = {} - # local from telnetlib3.server import parse_server_args original_parse = parse_server_args diff --git a/telnetlib3/tests/test_guard_integration.py b/telnetlib3/tests/test_guard_integration.py index 80e059c8..a1d3d1eb 100644 --- a/telnetlib3/tests/test_guard_integration.py +++ b/telnetlib3/tests/test_guard_integration.py @@ -11,7 +11,6 @@ ConnectionCounter, _read_line, busy_shell, - robot_check, robot_shell, _latin1_reading, ) @@ -409,7 +408,6 @@ def enc(**kw): async def test_fingerprint_scanner_defeats_robot_check(unused_tcp_port): """Fingerprint scanner's virtual cursor defeats the server's robot_check.""" - # local from telnetlib3.guard_shells import _TEST_CHAR, _measure_width # noqa: PLC0415 from telnetlib3.tests.accessories import create_server # noqa: PLC0415 @@ -426,12 +424,8 @@ async def guarded_shell(reader, writer): await writer.wait_closed() async with create_server( - host="127.0.0.1", - port=unused_tcp_port, - shell=guarded_shell, - connect_maxwait=0.5, + host="127.0.0.1", port=unused_tcp_port, shell=guarded_shell, connect_maxwait=0.5 ): - # local import telnetlib3 # noqa: PLC0415 shell = functools.partial( @@ -443,11 +437,7 @@ async def guarded_shell(reader, writer): banner_max_wait=5.0, ) reader, writer = await telnetlib3.open_connection( - host="127.0.0.1", - port=unused_tcp_port, - encoding=False, - shell=shell, - connect_minwait=0.5, + host="127.0.0.1", port=unused_tcp_port, encoding=False, shell=shell, connect_minwait=0.5 ) # Shell runs as a background task — wait for it to finish. await asyncio.wait_for(writer.protocol.waiter_closed, timeout=10.0) diff --git a/telnetlib3/tests/test_linemode.py b/telnetlib3/tests/test_linemode.py index 25e5c814..5d817775 100644 --- a/telnetlib3/tests/test_linemode.py +++ b/telnetlib3/tests/test_linemode.py @@ -8,7 +8,7 @@ import telnetlib3.stream_writer from telnetlib3.slc import LMODE_MODE, LMODE_MODE_ACK, LMODE_MODE_LOCAL from telnetlib3.telopt import DO, SB, SE, IAC, WILL, LINEMODE -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( bind_host, create_server, unused_tcp_port, diff --git a/telnetlib3/tests/test_mud_negotiation.py b/telnetlib3/tests/test_mud_negotiation.py index 0ee1534b..ea0907dc 100644 --- a/telnetlib3/tests/test_mud_negotiation.py +++ b/telnetlib3/tests/test_mud_negotiation.py @@ -7,7 +7,23 @@ import pytest # local -from telnetlib3.telopt import DO, SB, SE, IAC, MSP, MXP, ZMP, ATCP, GMCP, MSDP, MSSP, WILL, AARDWOLF +from telnetlib3.telopt import ( + DO, + SB, + SE, + IAC, + MSP, + MXP, + ZMP, + ATCP, + DONT, + GMCP, + MSDP, + MSSP, + WILL, + WONT, + AARDWOLF, +) from telnetlib3.stream_writer import TelnetWriter @@ -136,7 +152,6 @@ def callback(variables): w.set_ext_callback(MSDP, callback) w.pending_option[SB + MSDP] = True - # local from telnetlib3.telopt import MSDP_VAL, MSDP_VAR payload = MSDP_VAR + b"HEALTH" + MSDP_VAL + b"100" @@ -157,7 +172,6 @@ def callback(variables): w.set_ext_callback(MSSP, callback) w.pending_option[SB + MSSP] = True - # local from telnetlib3.telopt import MSSP_VAL, MSSP_VAR payload = MSSP_VAR + b"NAME" + MSSP_VAL + b"TestMUD" @@ -179,7 +193,6 @@ def test_sb_mssp_dispatch_stores_data(): w, t, p = new_writer(server=True) w.pending_option[SB + MSSP] = True - # local from telnetlib3.telopt import MSSP_VAL, MSSP_VAR payload = MSSP_VAR + b"NAME" + MSSP_VAL + b"TestMUD" + MSSP_VAR + b"PLAYERS" + MSSP_VAL + b"5" @@ -193,7 +206,6 @@ def test_sb_mssp_latin1_fallback(): w, t, p = new_writer(server=True) w.pending_option[SB + MSSP] = True - # local from telnetlib3.telopt import MSSP_VAL, MSSP_VAR # 0xC9 is 'É' in latin-1 but invalid as a lone UTF-8 lead byte @@ -220,7 +232,6 @@ def test_sb_msdp_latin1_fallback(): w, t, p = new_writer(server=True) w.pending_option[SB + MSDP] = True - # local from telnetlib3.telopt import MSDP_VAL, MSDP_VAR received_args: list[object] = [] @@ -249,7 +260,6 @@ def test_send_msdp(): w, t, p = new_writer(server=True) w.local_option[MSDP] = True - # local from telnetlib3.telopt import MSDP_VAL, MSDP_VAR w.send_msdp({"HEALTH": "100"}) @@ -261,7 +271,6 @@ def test_send_mssp(): w, t, p = new_writer(server=True) w.local_option[MSSP] = True - # local from telnetlib3.telopt import MSSP_VAL, MSSP_VAR w.send_mssp({"NAME": "TestMUD"}) @@ -361,11 +370,47 @@ def test_handle_do_mxp_sets_pending_sb(): assert w.pending_option.get(SB + MXP) is True -def test_handle_will_mxp_client_sets_pending_sb(): +def test_handle_will_mxp_client_declines(): w, t, _p = new_writer(server=False, client=True) w.handle_will(MXP) + assert IAC + DONT + MXP in t.writes + assert w.remote_option.get(MXP) is not True + + +def test_handle_will_mxp_client_always_do(): + w, t, _p = new_writer(server=False, client=True) + w.always_do.add(MXP) + w.handle_will(MXP) assert IAC + DO + MXP in t.writes - assert w.pending_option.get(SB + MXP) is True + assert w.remote_option.get(MXP) is True + + +_MUD_ALL = [GMCP, MSDP, MSSP, MSP, MXP, ZMP, AARDWOLF, ATCP] +_MUD_ALL_IDS = ["GMCP", "MSDP", "MSSP", "MSP", "MXP", "ZMP", "AARDWOLF", "ATCP"] + + +@pytest.mark.parametrize("opt", _MUD_ALL, ids=_MUD_ALL_IDS) +def test_handle_will_mud_client_declines(opt): + w, t, _p = new_writer(server=False, client=True) + w.handle_will(opt) + assert IAC + DONT + opt in t.writes + + +@pytest.mark.parametrize("opt", _MUD_ALL, ids=_MUD_ALL_IDS) +def test_handle_do_mud_client_declines(opt): + w, t, _p = new_writer(server=False, client=True) + result = w.handle_do(opt) + assert result is False + assert IAC + WONT + opt in t.writes + + +@pytest.mark.parametrize("opt", _MUD_ALL, ids=_MUD_ALL_IDS) +def test_handle_do_mud_client_always_will(opt): + w, t, _p = new_writer(server=False, client=True) + w.always_will.add(opt) + result = w.handle_do(opt) + assert result is True + assert IAC + WILL + opt in t.writes def test_sb_zmp_dispatch(): diff --git a/telnetlib3/tests/test_naws.py b/telnetlib3/tests/test_naws.py index 2d1b5df6..9152b9b2 100644 --- a/telnetlib3/tests/test_naws.py +++ b/telnetlib3/tests/test_naws.py @@ -17,7 +17,7 @@ # local import telnetlib3 from telnetlib3.telopt import SB, SE, IAC, NAWS, WILL -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, diff --git a/telnetlib3/tests/test_petscii_codec.py b/telnetlib3/tests/test_petscii_codec.py index 40bb4842..9e89a4e3 100644 --- a/telnetlib3/tests/test_petscii_codec.py +++ b/telnetlib3/tests/test_petscii_codec.py @@ -7,94 +7,97 @@ import pytest # local -import telnetlib3 # noqa: F401 - registers codecs +import telnetlib3 # noqa: F401 from telnetlib3.encodings import petscii def test_codec_lookup(): - info = codecs.lookup('petscii') - assert info.name == 'petscii' + info = codecs.lookup("petscii") + assert info.name == "petscii" -@pytest.mark.parametrize("alias", ['cbm', 'commodore', 'c64', 'c128']) +@pytest.mark.parametrize("alias", ["cbm", "commodore", "c64", "c128"]) def test_codec_aliases(alias): info = codecs.lookup(alias) - assert info.name == 'petscii' + assert info.name == "petscii" def test_digits(): data = bytes(range(0x30, 0x3A)) - assert data.decode('petscii') == '0123456789' + assert data.decode("petscii") == "0123456789" def test_space(): - assert b'\x20'.decode('petscii') == ' ' + assert b"\x20".decode("petscii") == " " def test_lowercase_at_41_5A(): data = bytes(range(0x41, 0x5B)) - assert data.decode('petscii') == 'abcdefghijklmnopqrstuvwxyz' + assert data.decode("petscii") == "abcdefghijklmnopqrstuvwxyz" def test_uppercase_at_C1_DA(): data = bytes(range(0xC1, 0xDB)) - assert data.decode('petscii') == 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + assert data.decode("petscii") == "ABCDEFGHIJKLMNOPQRSTUVWXYZ" def test_return(): - assert b'\x0d'.decode('petscii') == '\r' - - -@pytest.mark.parametrize("byte_val,expected", [ - pytest.param(0x5C, '\u00a3', id='pound_sign'), - pytest.param(0x5E, '\u2191', id='up_arrow'), - pytest.param(0x5F, '\u2190', id='left_arrow'), - pytest.param(0x61, '\u2660', id='spade'), - pytest.param(0x7E, '\u03c0', id='pi'), - pytest.param(0x78, '\u2663', id='club'), - pytest.param(0x7A, '\u2666', id='diamond'), - pytest.param(0x73, '\u2665', id='heart'), -]) + assert b"\x0d".decode("petscii") == "\r" + + +@pytest.mark.parametrize( + "byte_val,expected", + [ + pytest.param(0x5C, "\u00a3", id="pound_sign"), + pytest.param(0x5E, "\u2191", id="up_arrow"), + pytest.param(0x5F, "\u2190", id="left_arrow"), + pytest.param(0x61, "\u2660", id="spade"), + pytest.param(0x7E, "\u03c0", id="pi"), + pytest.param(0x78, "\u2663", id="club"), + pytest.param(0x7A, "\u2666", id="diamond"), + pytest.param(0x73, "\u2665", id="heart"), + ], +) def test_graphics_chars(byte_val, expected): - assert bytes([byte_val]).decode('petscii') == expected + assert bytes([byte_val]).decode("petscii") == expected def test_full_decode_no_crash(): data = bytes(range(256)) - result = data.decode('petscii') + result = data.decode("petscii") assert len(result) == 256 def test_encode_lowercase(): - encoded, length = codecs.lookup('petscii').encode('hello') - assert encoded == bytes([0x48, 0x45, 0x4c, 0x4c, 0x4f]) + encoded, length = codecs.lookup("petscii").encode("hello") + assert encoded == bytes([0x48, 0x45, 0x4C, 0x4C, 0x4F]) assert length == 5 def test_encode_uppercase(): - encoded, length = codecs.lookup('petscii').encode('HELLO') - assert encoded == b'\xc8\xc5\xcc\xcc\xcf' + encoded, length = codecs.lookup("petscii").encode("HELLO") + assert encoded == b"\xc8\xc5\xcc\xcc\xcf" assert length == 5 def test_round_trip_digits(): for byte_val in range(0x30, 0x3A): original = bytes([byte_val]) - decoded = original.decode('petscii') - re_encoded = decoded.encode('petscii') + decoded = original.decode("petscii") + re_encoded = decoded.encode("petscii") assert re_encoded == original def test_incremental_decoder(): - decoder = codecs.getincrementaldecoder('petscii')() - assert decoder.decode(b'\xc1', False) == 'A' - assert decoder.decode(b'\x42\x43', True) == 'bc' + decoder = codecs.getincrementaldecoder("petscii")() + assert decoder.decode(b"\xc1", False) == "A" + assert decoder.decode(b"\x42\x43", True) == "bc" def test_incremental_encoder(): - encoder = codecs.getincrementalencoder('petscii')() - assert encoder.encode('A', False) == b'\xc1' - assert encoder.encode('bc', True) == b'\x42\x43' + encoder = codecs.getincrementalencoder("petscii")() + assert encoder.encode("A", False) == b"\xc1" + assert encoder.encode("bc", True) == b"\x42\x43" def test_decoding_table_length(): diff --git a/telnetlib3/tests/test_pty_shell.py b/telnetlib3/tests/test_pty_shell.py index 6277910e..c0537460 100644 --- a/telnetlib3/tests/test_pty_shell.py +++ b/telnetlib3/tests/test_pty_shell.py @@ -22,7 +22,7 @@ _platform_check, _wait_for_terminal_info, ) -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, @@ -84,7 +84,6 @@ def _create(extra_info=None, capture_writes=False): @_ignore_forkpty_deprecation async def test_pty_shell_integration(bind_host, unused_tcp_port, require_no_capture): """Test PTY shell with various helper modes: cat, env, stty_size.""" - # local from telnetlib3 import make_pty_shell # Test 1: cat mode - echo input back @@ -173,7 +172,6 @@ async def client_shell(reader, writer): @_ignore_forkpty_deprecation async def test_pty_shell_lifecycle(bind_host, unused_tcp_port, require_no_capture): """Test PTY shell lifecycle: child exit and client disconnect.""" - # local from telnetlib3 import make_pty_shell # Test 1: child exit closes connection gracefully @@ -252,7 +250,6 @@ def test_platform_check_not_windows(): def test_make_pty_shell_returns_callable(): """Test that make_pty_shell returns a callable.""" - # local from telnetlib3 import make_pty_shell shell = make_pty_shell(sys.executable) @@ -327,7 +324,6 @@ def mock_ioctl(fd, cmd, data): winch_calls = [] def mock_killpg_winch(pgid, sig): - # std imports import signal as signal_mod if sig == signal_mod.SIGWINCH: @@ -675,7 +671,6 @@ async def test_pty_session_isalive_scenarios(child_pid, waitpid_behavior, expect async def test_pty_session_terminate_scenarios(): """Test _terminate handles various termination scenarios.""" - # std imports import signal reader = MagicMock() diff --git a/telnetlib3/tests/test_reader.py b/telnetlib3/tests/test_reader.py index 331861c4..e058d029 100644 --- a/telnetlib3/tests/test_reader.py +++ b/telnetlib3/tests/test_reader.py @@ -8,7 +8,7 @@ # local import telnetlib3 -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, diff --git a/telnetlib3/tests/test_server_api.py b/telnetlib3/tests/test_server_api.py index edda9f39..1455d898 100644 --- a/telnetlib3/tests/test_server_api.py +++ b/telnetlib3/tests/test_server_api.py @@ -1,4 +1,3 @@ -# pylint: disable=unused-import # std imports import asyncio diff --git a/telnetlib3/tests/test_server_fingerprinting.py b/telnetlib3/tests/test_server_fingerprinting.py index ec24e95c..685ee2fe 100644 --- a/telnetlib3/tests/test_server_fingerprinting.py +++ b/telnetlib3/tests/test_server_fingerprinting.py @@ -854,11 +854,7 @@ async def test_probe_skipped_when_closing(tmp_path): ), pytest.param(b"3) utf-8\r\nChoose: ", b"3\r\n", id="menu_utf8_lowercase"), pytest.param(b"Choose encoding: 1) UTF8", b"1\r\n", id="menu_utf8_no_hyphen"), - pytest.param( - b"12) UTF-8\r\nSelect: ", - b"12\r\n", - id="menu_utf8_multidigit", - ), + pytest.param(b"12) UTF-8\r\nSelect: ", b"12\r\n", id="menu_utf8_multidigit"), pytest.param(b"[5] UTF-8\r\nSelect: ", b"5\r\n", id="menu_utf8_brackets"), pytest.param(b"[2] utf-8\r\n", b"2\r\n", id="menu_utf8_brackets_lower"), pytest.param(b"3. UTF-8\r\n", b"3\r\n", id="menu_utf8_dot"), @@ -873,19 +869,11 @@ async def test_probe_skipped_when_closing(tmp_path): pytest.param(b"3. English/ANSI\r\n", b"3\r\n", id="menu_english_ansi"), pytest.param(b"2. English/ANSI\r\n", b"2\r\n", id="menu_english_ansi_2"), pytest.param( - b" 1 ... English/ANSI The standard\r\n", - b"1\r\n", - id="menu_ansi_ellipsis", - ), - pytest.param( - b" 2 .. English/ANSI\r\n", - b"2\r\n", - id="menu_ansi_double_dot", + b" 1 ... English/ANSI The standard\r\n", b"1\r\n", id="menu_ansi_ellipsis" ), + pytest.param(b" 2 .. English/ANSI\r\n", b"2\r\n", id="menu_ansi_double_dot"), pytest.param( - b"1) ASCII\r\n2) UTF-8\r\n(3) Ansi\r\n", - b"2\r\n", - id="menu_utf8_preferred_over_ansi", + b"1) ASCII\r\n2) UTF-8\r\n(3) Ansi\r\n", b"2\r\n", id="menu_utf8_preferred_over_ansi" ), pytest.param( b"1. ASCII\r\n2. UTF-8\r\n3. English/ANSI\r\n", @@ -902,20 +890,10 @@ async def test_probe_skipped_when_closing(tmp_path): b"\x1b\x1b", id="esc_twice_mystic", ), + pytest.param(b"Press [ESC] twice to continue", b"\x1b\x1b", id="esc_twice_no_dots"), + pytest.param(b"Press ESC twice to continue", b"\x1b\x1b", id="esc_twice_bare"), pytest.param( - b"Press [ESC] twice to continue", - b"\x1b\x1b", - id="esc_twice_no_dots", - ), - pytest.param( - b"Press ESC twice to continue", - b"\x1b\x1b", - id="esc_twice_bare", - ), - pytest.param( - b"Press twice for the BBS ... ", - b"\x1b\x1b", - id="esc_twice_angle_brackets", + b"Press twice for the BBS ... ", b"\x1b\x1b", id="esc_twice_angle_brackets" ), pytest.param( b"\x1b[33mPress [.ESC.] twice within 10 seconds\x1b[0m", @@ -927,25 +905,11 @@ async def test_probe_skipped_when_closing(tmp_path): b"\x1b\x1b", id="esc_twice_after_clear_screen", ), + pytest.param(b"Please press [ESC] to continue", b"\x1b", id="esc_once_brackets"), + pytest.param(b"Press ESC to continue", b"\x1b", id="esc_once_bare"), + pytest.param(b"press to continue", b"\x1b", id="esc_once_angle_brackets"), pytest.param( - b"Please press [ESC] to continue", - b"\x1b", - id="esc_once_brackets", - ), - pytest.param( - b"Press ESC to continue", - b"\x1b", - id="esc_once_bare", - ), - pytest.param( - b"press to continue", - b"\x1b", - id="esc_once_angle_brackets", - ), - pytest.param( - b"\x1b[33mPress [ESC] to continue\x1b[0m", - b"\x1b", - id="esc_once_ansi_wrapped", + b"\x1b[33mPress [ESC] to continue\x1b[0m", b"\x1b", id="esc_once_ansi_wrapped" ), pytest.param(b"HIT RETURN:", b"\r\n", id="hit_return"), pytest.param(b"Hit Return.", b"\r\n", id="hit_return_lower"), @@ -953,100 +917,40 @@ async def test_probe_skipped_when_closing(tmp_path): pytest.param(b"Press Enter:", b"\r\n", id="press_enter"), pytest.param(b"press enter", b"\r\n", id="press_enter_lower"), pytest.param(b"Hit Enter to continue", b"\r\n", id="hit_enter"), + pytest.param(b"\x1b[1mHIT RETURN:\x1b[0m", b"\r\n", id="hit_return_ansi_wrapped"), + pytest.param(b"\x1b[31mColor? \x1b[0m", b"y\r\n", id="color_ansi_wrapped"), + pytest.param(b"\x1b[1mContinue? (y/n)\x1b[0m ", b"y\r\n", id="yn_ansi_wrapped"), pytest.param( - b"\x1b[1mHIT RETURN:\x1b[0m", - b"\r\n", - id="hit_return_ansi_wrapped", + b"Do you support the ANSI color standard (Yn)? ", b"y\r\n", id="yn_paren_capital_y" ), + pytest.param(b"Continue? [Yn]", b"y\r\n", id="yn_bracket_capital_y"), + pytest.param(b"Do something (yN)", b"y\r\n", id="yn_paren_capital_n"), + pytest.param(b"More: (Y)es, (N)o, (C)ontinuous?", b"C\r\n", id="more_continuous"), pytest.param( - b"\x1b[31mColor? \x1b[0m", - b"y\r\n", - id="color_ansi_wrapped", - ), - pytest.param( - b"\x1b[1mContinue? (y/n)\x1b[0m ", - b"y\r\n", - id="yn_ansi_wrapped", - ), - pytest.param( - b"Do you support the ANSI color standard (Yn)? ", - b"y\r\n", - id="yn_paren_capital_y", - ), - pytest.param( - b"Continue? [Yn]", - b"y\r\n", - id="yn_bracket_capital_y", - ), - pytest.param( - b"Do something (yN)", - b"y\r\n", - id="yn_paren_capital_n", - ), - pytest.param( - b"More: (Y)es, (N)o, (C)ontinuous?", - b"C\r\n", - id="more_continuous", - ), - pytest.param( - b"\x1b[33mMore: (Y)es, (N)o, (C)ontinuous?\x1b[0m", - b"C\r\n", - id="more_continuous_ansi", - ), - pytest.param( - b"more (Y/N/C)ontinuous: ", - b"C\r\n", - id="more_ync_compact", + b"\x1b[33mMore: (Y)es, (N)o, (C)ontinuous?\x1b[0m", b"C\r\n", id="more_continuous_ansi" ), + pytest.param(b"more (Y/N/C)ontinuous: ", b"C\r\n", id="more_ync_compact"), pytest.param( b"Press the BACKSPACE key to detect your terminal type: ", b"\x08", id="backspace_key_telnetbible", ), pytest.param( - b"\x1b[1mPress the BACKSPACE key\x1b[0m", - b"\x08", - id="backspace_key_ansi_wrapped", - ), - pytest.param( - b"\x0cpress del/backspace:", - b"\x14", - id="petscii_del_backspace", - ), - pytest.param( - b"\x0c\r\npress del/backspace:", - b"\x14", - id="petscii_del_backspace_crlf", - ), - pytest.param( - b"press backspace:", - b"\x14", - id="petscii_backspace_only", - ), - pytest.param( - b"press del:", - b"\x14", - id="petscii_del_only", - ), - pytest.param( - b"PRESS DEL/BACKSPACE.", - b"\x14", - id="petscii_del_backspace_upper", - ), - pytest.param( - b"press backspace/del:", - b"\x14", - id="petscii_backspace_del_reversed", + b"\x1b[1mPress the BACKSPACE key\x1b[0m", b"\x08", id="backspace_key_ansi_wrapped" ), + pytest.param(b"\x0cpress del/backspace:", b"\x14", id="petscii_del_backspace"), + pytest.param(b"\x0c\r\npress del/backspace:", b"\x14", id="petscii_del_backspace_crlf"), + pytest.param(b"press backspace:", b"\x14", id="petscii_backspace_only"), + pytest.param(b"press del:", b"\x14", id="petscii_del_only"), + pytest.param(b"PRESS DEL/BACKSPACE.", b"\x14", id="petscii_del_backspace_upper"), + pytest.param(b"press backspace/del:", b"\x14", id="petscii_backspace_del_reversed"), pytest.param( b"PLEASE HIT YOUR BACKSPACE/DELETE\r\nKEY FOR C/G DETECT:", b"\x14", id="petscii_hit_your_backspace_delete", ), pytest.param( - b"hit your delete/backspace key:", - b"\x14", - id="petscii_hit_your_delete_backspace_key", + b"hit your delete/backspace key:", b"\x14", id="petscii_hit_your_delete_backspace_key" ), ], ) @@ -1167,11 +1071,9 @@ async def test_fingerprinting_shell_multi_prompt(tmp_path): """Server asks color first, then presents a UTF-8 charset menu.""" save_path = str(tmp_path / "result.json") writer = MockWriter(will_options=[fps.SGA]) - reader = InteractiveMockReader([ - b"Color? ", - b"Select charset:\r\n1) ASCII\r\n2) UTF-8\r\n", - b"Welcome!\r\n", - ], writer) + reader = InteractiveMockReader( + [b"Color? ", b"Select charset:\r\n1) ASCII\r\n2) UTF-8\r\n", b"Welcome!\r\n"], writer + ) await sfp.fingerprinting_client_shell( reader, @@ -1196,10 +1098,7 @@ async def test_fingerprinting_shell_multi_prompt_stops_on_bare_return(tmp_path): """Loop stops after a bare \\r\\n response (no prompt detected).""" save_path = str(tmp_path / "result.json") writer = MockWriter(will_options=[fps.SGA]) - reader = InteractiveMockReader([ - b"Color? ", - b"Welcome!\r\n", - ], writer) + reader = InteractiveMockReader([b"Color? ", b"Welcome!\r\n"], writer) await sfp.fingerprinting_client_shell( reader, @@ -1224,8 +1123,7 @@ async def test_fingerprinting_shell_multi_prompt_max_replies(tmp_path): """Loop does not exceed _MAX_PROMPT_REPLIES rounds.""" save_path = str(tmp_path / "result.json") writer = MockWriter(will_options=[fps.SGA]) - banners = [f"Color? (round {i}) ".encode() - for i in range(sfp._MAX_PROMPT_REPLIES + 1)] + banners = [f"Color? (round {i}) ".encode() for i in range(sfp._MAX_PROMPT_REPLIES + 1)] reader = InteractiveMockReader(banners, writer) await sfp.fingerprinting_client_shell( @@ -1274,7 +1172,7 @@ async def test_read_banner_until_quiet_responds_to_dsr(): reader = MockReader([b"Hello\x1b[6nWorld"]) writer = MockWriter() result = await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, + reader, quiet_time=0.01, max_wait=0.05, writer=writer ) assert result == b"Hello\x1b[6nWorld" assert b"\x1b[1;1R" in writer._writes @@ -1285,9 +1183,7 @@ async def test_read_banner_until_quiet_multiple_dsr(): """Multiple DSR requests each get a CPR response.""" reader = MockReader([b"\x1b[6n", b"banner\x1b[6n"]) writer = MockWriter() - await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, - ) + await sfp._read_banner_until_quiet(reader, quiet_time=0.01, max_wait=0.05, writer=writer) cpr_count = sum(1 for w in writer._writes if w == b"\x1b[1;1R") assert cpr_count == 2 @@ -1297,9 +1193,7 @@ async def test_read_banner_until_quiet_no_dsr_no_write(): """No DSR in banner means no CPR writes.""" reader = MockReader([b"Welcome to BBS\r\n"]) writer = MockWriter() - await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, - ) + await sfp._read_banner_until_quiet(reader, quiet_time=0.01, max_wait=0.05, writer=writer) assert not writer._writes @@ -1307,9 +1201,7 @@ async def test_read_banner_until_quiet_no_dsr_no_write(): async def test_read_banner_until_quiet_no_writer_ignores_dsr(): """Without a writer, DSR is silently ignored.""" reader = MockReader([b"Hello\x1b[6n"]) - result = await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, - ) + result = await sfp._read_banner_until_quiet(reader, quiet_time=0.01, max_wait=0.05) assert result == b"Hello\x1b[6n" @@ -1362,13 +1254,18 @@ async def test_fingerprinting_shell_ansi_ellipsis_menu(tmp_path): """Worldgroup/MajorBBS ellipsis-menu selects first numbered option.""" save_path = str(tmp_path / "result.json") writer = MockWriter(will_options=[fps.SGA, fps.ECHO]) - reader = InteractiveMockReader([ - (b"Please choose one of these languages/protocols:\r\n\r\n" - b" 1 ... English/ANSI The standard English language version\r\n" - b" 2 ... English/RIP The English version of RIPscrip graphics\r\n" - b"\r\nChoose a number from 1 to 2: "), - b"Welcome!\r\n", - ], writer) + reader = InteractiveMockReader( + [ + ( + b"Please choose one of these languages/protocols:\r\n\r\n" + b" 1 ... English/ANSI The standard English language version\r\n" + b" 2 ... English/RIP The English version of RIPscrip graphics\r\n" + b"\r\nChoose a number from 1 to 2: " + ), + b"Welcome!\r\n", + ], + writer, + ) await sfp.fingerprinting_client_shell( reader, @@ -1388,15 +1285,15 @@ async def test_fingerprinting_shell_ansi_ellipsis_menu(tmp_path): @pytest.mark.asyncio async def test_read_banner_inline_esc_twice(): """ESC-twice botcheck is responded to inline during banner collection.""" - reader = MockReader([ - b"Mystic BBS v1.12\r\n", - b"Press [.ESC.] twice within 15 seconds to CONTINUE...\r\n", - b"Press [.ESC.] twice within 14 seconds to CONTINUE...\r\n", - ]) - writer = MockWriter() - await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, + reader = MockReader( + [ + b"Mystic BBS v1.12\r\n", + b"Press [.ESC.] twice within 15 seconds to CONTINUE...\r\n", + b"Press [.ESC.] twice within 14 seconds to CONTINUE...\r\n", + ] ) + writer = MockWriter() + await sfp._read_banner_until_quiet(reader, quiet_time=0.01, max_wait=0.05, writer=writer) assert b"\x1b\x1b" in writer._writes esc_count = sum(1 for w in writer._writes if w == b"\x1b\x1b") assert esc_count == 1 @@ -1407,9 +1304,7 @@ async def test_read_banner_inline_esc_once(): """ESC-once prompt is responded to inline during banner collection.""" reader = MockReader([b"Press [ESC] to continue\r\n"]) writer = MockWriter() - await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, - ) + await sfp._read_banner_until_quiet(reader, quiet_time=0.01, max_wait=0.05, writer=writer) assert b"\x1b" in writer._writes @@ -1418,10 +1313,13 @@ async def test_fingerprinting_shell_esc_inline_no_duplicate(tmp_path): """Inline ESC response prevents duplicate in the prompt loop.""" save_path = str(tmp_path / "result.json") writer = MockWriter(will_options=[fps.SGA]) - reader = InteractiveMockReader([ - b"Press [.ESC.] twice within 15 seconds to CONTINUE...\r\n", - b"Welcome to Mystic BBS!\r\nLogin: ", - ], writer) + reader = InteractiveMockReader( + [ + b"Press [.ESC.] twice within 15 seconds to CONTINUE...\r\n", + b"Welcome to Mystic BBS!\r\nLogin: ", + ], + writer, + ) await sfp.fingerprinting_client_shell( reader, @@ -1444,11 +1342,14 @@ async def test_fingerprinting_shell_delayed_prompt(tmp_path): """Bare-return banner followed by ESC-twice prompt still gets answered.""" save_path = str(tmp_path / "result.json") writer = MockWriter(will_options=[fps.SGA]) - reader = InteractiveMockReader([ - b"Starting BBS-DOS...\r\n", - b"Press [.ESC.] twice within 15 seconds to CONTINUE...", - b"Welcome!\r\n", - ], writer) + reader = InteractiveMockReader( + [ + b"Starting BBS-DOS...\r\n", + b"Press [.ESC.] twice within 15 seconds to CONTINUE...", + b"Welcome!\r\n", + ], + writer, + ) await sfp.fingerprinting_client_shell( reader, @@ -1472,7 +1373,7 @@ async def test_read_banner_virtual_cursor_defeats_robot_check(): writer = MockWriter() cursor = sfp._VirtualCursor() await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, cursor=cursor, + reader, quiet_time=0.01, max_wait=0.05, writer=writer, cursor=cursor ) cpr_writes = [w for w in writer._writes if b"R" in w] assert cpr_writes[0] == b"\x1b[1;1R" @@ -1486,7 +1387,7 @@ async def test_read_banner_virtual_cursor_separate_chunks(): writer = MockWriter() cursor = sfp._VirtualCursor() await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, cursor=cursor, + reader, quiet_time=0.01, max_wait=0.05, writer=writer, cursor=cursor ) cpr_writes = [w for w in writer._writes if b"R" in w] assert cpr_writes[0] == b"\x1b[1;1R" @@ -1500,7 +1401,7 @@ async def test_read_banner_virtual_cursor_wide_char(): writer = MockWriter() cursor = sfp._VirtualCursor() await sfp._read_banner_until_quiet( - reader, quiet_time=0.01, max_wait=0.05, writer=writer, cursor=cursor, + reader, quiet_time=0.01, max_wait=0.05, writer=writer, cursor=cursor ) cpr_writes = [w for w in writer._writes if b"R" in w] assert cpr_writes[0] == b"\x1b[1;1R" @@ -1528,16 +1429,19 @@ def test_virtual_cursor_ansi_stripped(): assert cursor.col == 2 -@pytest.mark.parametrize("response,encoding,expected", [ - pytest.param(b"\r\n", "atascii", b"\x9b", id="atascii_bare_return"), - pytest.param(b"yes\r\n", "atascii", b"yes\x9b", id="atascii_yes"), - pytest.param(b"y\r\n", "atascii", b"y\x9b", id="atascii_y"), - pytest.param(b"\r\n", "ascii", b"\r\n", id="ascii_unchanged"), - pytest.param(b"\r\n", "utf-8", b"\r\n", id="utf8_unchanged"), - pytest.param(b"yes\r\n", "utf-8", b"yes\r\n", id="utf8_yes_unchanged"), - pytest.param(b"\x1b\x1b", "atascii", b"\x1b\x1b", id="atascii_esc_esc"), -]) +@pytest.mark.parametrize( + "response,encoding,expected", + [ + pytest.param(b"\r\n", "atascii", b"\x9b", id="atascii_bare_return"), + pytest.param(b"yes\r\n", "atascii", b"yes\x9b", id="atascii_yes"), + pytest.param(b"y\r\n", "atascii", b"y\x9b", id="atascii_y"), + pytest.param(b"\r\n", "ascii", b"\r\n", id="ascii_unchanged"), + pytest.param(b"\r\n", "utf-8", b"\r\n", id="utf8_unchanged"), + pytest.param(b"yes\r\n", "utf-8", b"yes\r\n", id="utf8_yes_unchanged"), + pytest.param(b"\x1b\x1b", "atascii", b"\x1b\x1b", id="atascii_esc_esc"), + ], +) def test_reencode_prompt(response, encoding, expected): - # local - import telnetlib3 # noqa: F401 - registers codecs + import telnetlib3 # noqa: F401 + assert sfp._reencode_prompt(response, encoding) == expected diff --git a/telnetlib3/tests/test_server_shell_unit.py b/telnetlib3/tests/test_server_shell_unit.py index 641d232a..b381e635 100644 --- a/telnetlib3/tests/test_server_shell_unit.py +++ b/telnetlib3/tests/test_server_shell_unit.py @@ -132,8 +132,13 @@ def test_get_slcdata_contains_expected_sections(): @pytest.mark.skipif(sys.platform == "win32", reason="requires termios") async def test_terminal_determine_mode(monkeypatch): monkeypatch.setattr(sys, "stdin", types.SimpleNamespace(fileno=lambda: 0)) + _mock_opt = types.SimpleNamespace(enabled=lambda key: False) tw = types.SimpleNamespace( - will_echo=False, log=types.SimpleNamespace(debug=lambda *a, **k: None) + will_echo=False, + _raw_mode=None, + client=True, + remote_option=_mock_opt, + log=types.SimpleNamespace(debug=lambda *a, **k: None), ) term = cs.Terminal(tw) mode = cs.Terminal.ModeDef(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 38400, 38400, [0] * 32) diff --git a/telnetlib3/tests/test_shell.py b/telnetlib3/tests/test_shell.py index dc6c790c..521ff678 100644 --- a/telnetlib3/tests/test_shell.py +++ b/telnetlib3/tests/test_shell.py @@ -7,7 +7,7 @@ # local from telnetlib3 import accessories, telnet_server_shell from telnetlib3.telopt import DO, IAC, SGA, ECHO, WILL, WONT, TTYPE, BINARY -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( bind_host, create_server, asyncio_server, diff --git a/telnetlib3/tests/test_status_logger.py b/telnetlib3/tests/test_status_logger.py index e84ad3e5..691ab497 100644 --- a/telnetlib3/tests/test_status_logger.py +++ b/telnetlib3/tests/test_status_logger.py @@ -1,4 +1,3 @@ -# pylint: disable=unused-import # std imports import sys import asyncio diff --git a/telnetlib3/tests/test_stream_writer_full.py b/telnetlib3/tests/test_stream_writer_full.py index 72b993cd..b5b1796e 100644 --- a/telnetlib3/tests/test_stream_writer_full.py +++ b/telnetlib3/tests/test_stream_writer_full.py @@ -1190,7 +1190,6 @@ def test_linemode_mode_without_negotiation_ignored(): def test_name_option_distinguishes_commands_from_options(): """name_option renders IAC command bytes as repr, not their command names.""" - # local from telnetlib3.telopt import name_option, name_command assert name_option(WONT) == repr(WONT) diff --git a/telnetlib3/tests/test_sync.py b/telnetlib3/tests/test_sync.py index c98981d2..a0c4caf7 100644 --- a/telnetlib3/tests/test_sync.py +++ b/telnetlib3/tests/test_sync.py @@ -1,7 +1,5 @@ """Tests for the synchronous (blocking) interface.""" -# pylint: disable=unused-import - # std imports import time import threading diff --git a/telnetlib3/tests/test_telnetlib.py b/telnetlib3/tests/test_telnetlib.py index 922ba23b..dc9bebd8 100644 --- a/telnetlib3/tests/test_telnetlib.py +++ b/telnetlib3/tests/test_telnetlib.py @@ -93,7 +93,6 @@ def fileno(self): """Provide a real OS-level file descriptor so selectors and any code that calls fileno() can work, even though the network I/O is mocked.""" s = getattr(self, "_fileno_sock", None) - # pylint: disable=attribute-defined-outside-init if s is None: try: s1, s2 = socket.socketpair() @@ -109,7 +108,6 @@ def fileno(self): def close(self): # Close the internal fileno() provider sockets, but leave the mocked self.sock alone - # pylint: disable=attribute-defined-outside-init try: if getattr(self, "_fileno_sock", None) is not None: try: @@ -183,7 +181,6 @@ def make_telnet(reads=(), cls=TelnetAlike): assert isinstance(x, bytes) with mocktest_socket(reads): telnet = cls("dummy", 0) - # pylint: disable=attribute-defined-outside-init telnet._messages = "" # debuglevel output return telnet @@ -440,7 +437,6 @@ def test_debug_accepts_str_port(self): # Issue 10695 with mocktest_socket([]): telnet = TelnetAlike("dummy", "0") - # pylint: disable=attribute-defined-outside-init telnet._messages = "" telnet.set_debuglevel(1) telnet.msg("test") diff --git a/telnetlib3/tests/test_timeout.py b/telnetlib3/tests/test_timeout.py index ab61bd99..e2c59c6e 100644 --- a/telnetlib3/tests/test_timeout.py +++ b/telnetlib3/tests/test_timeout.py @@ -10,7 +10,7 @@ # local from telnetlib3.client import _transform_args, _get_argument_parser from telnetlib3.telopt import DO, IAC, WONT, TTYPE -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, diff --git a/telnetlib3/tests/test_tspeed.py b/telnetlib3/tests/test_tspeed.py index 60190105..d1dd610d 100644 --- a/telnetlib3/tests/test_tspeed.py +++ b/telnetlib3/tests/test_tspeed.py @@ -7,7 +7,7 @@ import telnetlib3 import telnetlib3.stream_writer from telnetlib3.telopt import DO, IS, SB, SE, IAC, WILL, TSPEED -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, diff --git a/telnetlib3/tests/test_ttype.py b/telnetlib3/tests/test_ttype.py index 6429e644..2630b69f 100644 --- a/telnetlib3/tests/test_ttype.py +++ b/telnetlib3/tests/test_ttype.py @@ -7,7 +7,7 @@ import telnetlib3 import telnetlib3.stream_writer from telnetlib3.telopt import IS, SB, SE, IAC, WILL, TTYPE -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, unused_tcp_port, diff --git a/telnetlib3/tests/test_uvloop_integration.py b/telnetlib3/tests/test_uvloop_integration.py index 205e361a..6023b0f6 100644 --- a/telnetlib3/tests/test_uvloop_integration.py +++ b/telnetlib3/tests/test_uvloop_integration.py @@ -7,7 +7,6 @@ import pytest try: - # 3rd party import uvloop HAS_UVLOOP = True @@ -16,7 +15,7 @@ # local import telnetlib3 -from telnetlib3.tests.accessories import bind_host, unused_tcp_port # pylint: disable=unused-import +from telnetlib3.tests.accessories import bind_host, unused_tcp_port pytestmark = pytest.mark.skipif(not HAS_UVLOOP, reason="uvloop not installed") diff --git a/telnetlib3/tests/test_writer.py b/telnetlib3/tests/test_writer.py index 3a26dbda..b00dffd2 100644 --- a/telnetlib3/tests/test_writer.py +++ b/telnetlib3/tests/test_writer.py @@ -26,7 +26,7 @@ CMD_EOR, option_from_name, ) -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, diff --git a/telnetlib3/tests/test_xdisploc.py b/telnetlib3/tests/test_xdisploc.py index d3300bbf..0c858e53 100644 --- a/telnetlib3/tests/test_xdisploc.py +++ b/telnetlib3/tests/test_xdisploc.py @@ -7,7 +7,7 @@ import telnetlib3 import telnetlib3.stream_writer from telnetlib3.telopt import DO, IS, SB, SE, IAC, WILL, XDISPLOC -from telnetlib3.tests.accessories import ( # pylint: disable=unused-import; pylint: disable=unused-import, +from telnetlib3.tests.accessories import ( bind_host, create_server, open_connection, diff --git a/tox.ini b/tox.ini index 95ec2135..6fa26e96 100644 --- a/tox.ini +++ b/tox.ini @@ -138,6 +138,9 @@ commands = --disable=missing-class-docstring \ --disable=unused-variable \ --disable=unnecessary-dunder-call \ + --disable=unused-import \ + --disable=attribute-defined-outside-init \ + --disable=too-many-positional-arguments \ {posargs} telnetlib3/tests [testenv:codespell] @@ -233,6 +236,7 @@ import_heading_firstparty = local import_heading_localfolder = local sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER no_lines_before = LOCALFOLDER +indented_import_headings = false atomic = true [pydocstyle]