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]