https://github.com/python/cpython/commit/3db6d82a4d589a758a119e1de5118ad2e4c9588c
commit: 3db6d82a4d589a758a119e1de5118ad2e4c9588c
branch: 3.13
author: Ɓukasz Langa <[email protected]>
committer: ambv <[email protected]>
date: 2025-09-17T18:25:40+02:00
summary:

[3.13] gh-134466: Don't run when termios is inaccessible (GH-138911) (GH-139030)

Without the ability to set required capabilities, the REPL cannot
function properly (syntax highlighting and multiline editing can't
work).

We refuse to work in this degraded state.
(cherry picked from commit 2fc7004d5437e7bb0a1f5b962be441ef0ee7434b)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-14-04-56.gh-issue-134466.yR4fYW.rst
M Lib/_pyrepl/fancy_termios.py
M Lib/_pyrepl/unix_console.py

diff --git a/Lib/_pyrepl/fancy_termios.py b/Lib/_pyrepl/fancy_termios.py
index 0468b9a2670267..8d5bd183f21339 100644
--- a/Lib/_pyrepl/fancy_termios.py
+++ b/Lib/_pyrepl/fancy_termios.py
@@ -20,19 +20,25 @@
 import termios
 
 
+TYPE_CHECKING = False
+
+if TYPE_CHECKING:
+    from typing import cast
+else:
+    cast = lambda typ, val: val
+
+
 class TermState:
-    def __init__(self, tuples):
-        (
-            self.iflag,
-            self.oflag,
-            self.cflag,
-            self.lflag,
-            self.ispeed,
-            self.ospeed,
-            self.cc,
-        ) = tuples
+    def __init__(self, attrs: list[int | list[bytes]]) -> None:
+        self.iflag = cast(int, attrs[0])
+        self.oflag = cast(int, attrs[1])
+        self.cflag = cast(int, attrs[2])
+        self.lflag = cast(int, attrs[3])
+        self.ispeed = cast(int, attrs[4])
+        self.ospeed = cast(int, attrs[5])
+        self.cc = cast(list[bytes], attrs[6])
 
-    def as_list(self):
+    def as_list(self) -> list[int | list[bytes]]:
         return [
             self.iflag,
             self.oflag,
@@ -45,32 +51,32 @@ def as_list(self):
             self.cc[:],
         ]
 
-    def copy(self):
+    def copy(self) -> "TermState":
         return self.__class__(self.as_list())
 
 
-def tcgetattr(fd):
+def tcgetattr(fd: int) -> TermState:
     return TermState(termios.tcgetattr(fd))
 
 
-def tcsetattr(fd, when, attrs):
+def tcsetattr(fd: int, when: int, attrs: TermState) -> None:
     termios.tcsetattr(fd, when, attrs.as_list())
 
 
 class Term(TermState):
     TS__init__ = TermState.__init__
 
-    def __init__(self, fd=0):
+    def __init__(self, fd: int = 0) -> None:
         self.TS__init__(termios.tcgetattr(fd))
         self.fd = fd
-        self.stack = []
+        self.stack: list[list[int | list[bytes]]] = []
 
-    def save(self):
+    def save(self) -> None:
         self.stack.append(self.as_list())
 
-    def set(self, when=termios.TCSANOW):
+    def set(self, when: int = termios.TCSANOW) -> None:
         termios.tcsetattr(self.fd, when, self.as_list())
 
-    def restore(self):
+    def restore(self) -> None:
         self.TS__init__(self.stack.pop())
         self.set()
diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py
index feb0fc3f5ce3c0..f0cb950170a6ba 100644
--- a/Lib/_pyrepl/unix_console.py
+++ b/Lib/_pyrepl/unix_console.py
@@ -34,7 +34,7 @@
 
 from . import curses
 from .console import Console, Event
-from .fancy_termios import tcgetattr, tcsetattr
+from .fancy_termios import tcgetattr, tcsetattr, TermState
 from .trace import trace
 from .unix_eventqueue import EventQueue
 from .utils import wlen
@@ -44,16 +44,19 @@
 
 # types
 if TYPE_CHECKING:
-    from typing import IO, Literal, overload
+    from typing import AbstractSet, IO, Literal, overload, cast
 else:
     overload = lambda func: None
+    cast = lambda typ, val: val
 
 
 class InvalidTerminal(RuntimeError):
-    pass
+    def __init__(self, message: str) -> None:
+        super().__init__(errno.EIO, message)
 
 
 _error = (termios.error, curses.error, InvalidTerminal)
+_error_codes_to_ignore = frozenset([errno.EIO, errno.ENXIO, errno.EPERM])
 
 SIGWINCH_EVENT = "repaint"
 
@@ -118,12 +121,13 @@ def __init__(self):
 
         def register(self, fd, flag):
             self.fd = fd
+
         # note: The 'timeout' argument is received as *milliseconds*
         def poll(self, timeout: float | None = None) -> list[int]:
             if timeout is None:
                 r, w, e = select.select([self.fd], [], [])
             else:
-                r, w, e = select.select([self.fd], [], [], timeout/1000)
+                r, w, e = select.select([self.fd], [], [], timeout / 1000)
             return r
 
     poll = MinimalPoll  # type: ignore[assignment]
@@ -159,8 +163,15 @@ def __init__(
             and os.getenv("TERM_PROGRAM") == "Apple_Terminal"
         )
 
+        try:
+            self.__input_fd_set(tcgetattr(self.input_fd), ignore=frozenset())
+        except _error as e:
+            raise RuntimeError(f"termios failure ({e.args[1]})")
+
         @overload
-        def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: 
...
+        def _my_getstr(
+            cap: str, optional: Literal[False] = False
+        ) -> bytes: ...
 
         @overload
         def _my_getstr(cap: str, optional: bool) -> bytes | None: ...
@@ -226,7 +237,6 @@ def __read(self, n: int) -> bytes:
             self.input_buffer_pos = 0
         return ret
 
-
     def change_encoding(self, encoding: str) -> None:
         """
         Change the encoding used for I/O operations.
@@ -338,6 +348,8 @@ def prepare(self):
         """
         Prepare the console for input/output operations.
         """
+        self.__buffer = []
+
         self.__svtermstate = tcgetattr(self.input_fd)
         raw = self.__svtermstate.copy()
         raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
@@ -349,14 +361,7 @@ def prepare(self):
         raw.lflag |= termios.ISIG
         raw.cc[termios.VMIN] = 1
         raw.cc[termios.VTIME] = 0
-        try:
-            tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
-        except termios.error as e:
-            if e.args[0] != errno.EIO:
-                # gh-135329: when running under external programs (like 
strace),
-                # tcsetattr may fail with EIO. We can safely ignore this
-                # and continue with default terminal settings.
-                raise
+        self.__input_fd_set(raw)
 
         # In macOS terminal we need to deactivate line wrap via ANSI escape 
code
         if self.is_apple_terminal:
@@ -365,8 +370,6 @@ def prepare(self):
         self.screen = []
         self.height, self.width = self.getheightwidth()
 
-        self.__buffer = []
-
         self.posxy = 0, 0
         self.__gone_tall = 0
         self.__move = self.__move_short
@@ -388,11 +391,7 @@ def restore(self):
         self.__disable_bracketed_paste()
         self.__maybe_write_code(self._rmkx)
         self.flushoutput()
-        try:
-            tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
-        except termios.error as e:
-            if e.args[0] != errno.EIO:
-                raise
+        self.__input_fd_set(self.__svtermstate)
 
         if self.is_apple_terminal:
             os.write(self.output_fd, b"\033[?7h")
@@ -831,3 +830,17 @@ def __tputs(self, fmt, prog=delayprog):
                 os.write(self.output_fd, self._pad * nchars)
             else:
                 time.sleep(float(delay) / 1000.0)
+
+    def __input_fd_set(
+        self,
+        state: TermState,
+        ignore: AbstractSet[int] = _error_codes_to_ignore,
+    ) -> bool:
+        try:
+            tcsetattr(self.input_fd, termios.TCSADRAIN, state)
+        except termios.error as te:
+            if te.args[0] not in ignore:
+                raise
+            return False
+        else:
+            return True
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-14-04-56.gh-issue-134466.yR4fYW.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-14-04-56.gh-issue-134466.yR4fYW.rst
new file mode 100644
index 00000000000000..4fae7e0d6f6326
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-14-04-56.gh-issue-134466.yR4fYW.rst
@@ -0,0 +1,2 @@
+Don't run PyREPL in a degraded environment where setting termios attributes
+is not allowed.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to