From 9f6085801d43b55486bce60c79453290c870e2df Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 21 Jul 2025 09:58:45 +0200 Subject: [PATCH 1/3] Use bytes.index instead of comparing each byte --- Lib/_pyrepl/terminfo.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/terminfo.py b/Lib/_pyrepl/terminfo.py index 063a285bb9900c..cd645b5b905fc5 100644 --- a/Lib/_pyrepl/terminfo.py +++ b/Lib/_pyrepl/terminfo.py @@ -437,10 +437,8 @@ def _parse_terminfo_file(self, terminal_name: str) -> None: strings.append(CANCELLED_STRING) elif off < len(string_table): # Find null terminator - end = off - while end < len(string_table) and string_table[end] != 0: - end += 1 - if end <= len(string_table): + end = string_table.find(0, off) + if end >= 0: strings.append(string_table[off:end]) else: strings.append(ABSENT_STRING) From 71ae3c00d7f64abf2d7948b45f20e16dafb26b46 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 21 Jul 2025 10:36:29 +0200 Subject: [PATCH 2/3] Skip parsing/storing stuff we don't need --- Lib/_pyrepl/terminfo.py | 99 +++++++++++++---------------------------- 1 file changed, 30 insertions(+), 69 deletions(-) diff --git a/Lib/_pyrepl/terminfo.py b/Lib/_pyrepl/terminfo.py index cd645b5b905fc5..b3ee67027471f0 100644 --- a/Lib/_pyrepl/terminfo.py +++ b/Lib/_pyrepl/terminfo.py @@ -322,10 +322,6 @@ class TermInfo: terminal_name: str | bytes | None fallback: bool = True - _names: list[str] = field(default_factory=list) - _booleans: list[int] = field(default_factory=list) - _numbers: list[int] = field(default_factory=list) - _strings: list[bytes | None] = field(default_factory=list) _capabilities: dict[str, bytes] = field(default_factory=dict) def __post_init__(self) -> None: @@ -362,9 +358,12 @@ def __post_init__(self) -> None: def _parse_terminfo_file(self, terminal_name: str) -> None: """Parse a terminfo file. + Populate the _capabilities dict for easy retrieval + Based on ncurses implementation in: - ncurses/tinfo/read_entry.c:_nc_read_termtype() - ncurses/tinfo/read_entry.c:_nc_read_file_entry() + - ncurses/tinfo/lib_ti.c:tigetstr() """ data = _read_terminfo_file(terminal_name) too_short = f"TermInfo file for {terminal_name!r} too short" @@ -377,53 +376,36 @@ def _parse_terminfo_file(self, terminal_name: str) -> None: ) if magic == MAGIC16: - number_format = " len(data): - raise ValueError(too_short) - names = data[offset : offset + name_size - 1].decode( - "ascii", errors="ignore" - ) + # Skip data than PyREPL doesn't need: + # - names (`|`-separated ASCII strings) + # - boolean capabilities (bytes with value 0 or 1) + # - numbers (little-endian integers, `number_size` bytes each) offset += name_size - - # Read boolean capabilities - if offset + bool_count > len(data): - raise ValueError(too_short) - booleans = list(data[offset : offset + bool_count]) offset += bool_count - - # Align to even byte boundary for numbers if offset % 2: + # Align to even byte boundary for numbers offset += 1 - - # Read numeric capabilities - numbers = [] - for i in range(num_count): - if offset + number_size > len(data): - raise ValueError(too_short) - num = struct.unpack( - number_format, data[offset : offset + number_size] - )[0] - numbers.append(num) - offset += number_size + offset += num_count * number_size + if offset > len(data): + raise ValueError(too_short) # Read string offsets - string_offsets = [] - for i in range(str_count): - if offset + 2 > len(data): - raise ValueError(too_short) - off = struct.unpack(" len(data): + raise ValueError(too_short) + string_offset_data = data[offset:end_offset] + string_offsets = [ + off for [off] in struct.iter_unpack(" len(data): @@ -431,51 +413,30 @@ def _parse_terminfo_file(self, terminal_name: str) -> None: string_table = data[offset : offset + str_size] # Extract strings from string table - strings: list[bytes | None] = [] - for off in string_offsets: + capabilities = {} + for cap, off in zip(_STRING_CAPABILITY_NAMES, string_offsets): if off < 0: - strings.append(CANCELLED_STRING) + # CANCELLED_STRING; we do not store those + continue elif off < len(string_table): # Find null terminator end = string_table.find(0, off) if end >= 0: - strings.append(string_table[off:end]) - else: - strings.append(ABSENT_STRING) - else: - strings.append(ABSENT_STRING) + capabilities[cap] = string_table[off:end] + # in other cases this is ABSENT_STRING; we don't store those. - self._names = names.split("|") - self._booleans = booleans - self._numbers = numbers - self._strings = strings + # Note: we don't support extended capabilities since PyREPL doesn't + # need them. + + self._capabilities = capabilities def get(self, cap: str) -> bytes | None: """Get terminal capability string by name. - - Based on ncurses implementation in: - - ncurses/tinfo/lib_ti.c:tigetstr() - - The ncurses version searches through compiled terminfo data structures. - This version first checks parsed terminfo data, then falls back to - hardcoded capabilities. """ if not isinstance(cap, str): raise TypeError(f"`cap` must be a string, not {type(cap)}") - if self._capabilities: - # Fallbacks populated, use them - return self._capabilities.get(cap) - - # Look up in standard capabilities first - if cap in _STRING_CAPABILITY_NAMES: - index = _STRING_CAPABILITY_NAMES[cap] - if index < len(self._strings): - return self._strings[index] - - # Note: we don't support extended capabilities since PyREPL doesn't - # need them. - return None + return self._capabilities.get(cap) def tparm(cap_bytes: bytes, *params: int) -> bytes: From 086b761fe5f081805e368c6d5c76dd21ddedc1f1 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 21 Jul 2025 11:57:04 +0200 Subject: [PATCH 3/3] Remove _STRING_CAPABILITY_NAMES --- Lib/_pyrepl/terminfo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/_pyrepl/terminfo.py b/Lib/_pyrepl/terminfo.py index b3ee67027471f0..d02ef69cce0bd8 100644 --- a/Lib/_pyrepl/terminfo.py +++ b/Lib/_pyrepl/terminfo.py @@ -71,7 +71,6 @@ "OTGV", "OTGC","meml", "memu", "box1" ) # fmt: on -_STRING_CAPABILITY_NAMES = {name: i for i, name in enumerate(_STRING_NAMES)} def _get_terminfo_dirs() -> list[Path]: @@ -414,7 +413,7 @@ def _parse_terminfo_file(self, terminal_name: str) -> None: # Extract strings from string table capabilities = {} - for cap, off in zip(_STRING_CAPABILITY_NAMES, string_offsets): + for cap, off in zip(_STRING_NAMES, string_offsets): if off < 0: # CANCELLED_STRING; we do not store those continue