From 0d2ff2113d6679a39cde34d04ea38b2bb9f5b1a6 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 13 Oct 2026 10:50:27 -0700 Subject: [PATCH 1/3] gh-159323: PyREPL on Windows should exit when STDIN is NUL On Windows, when stdin is `NUL`, starting the REPL results on an exception loop because it is not caught by `os.isatty` --- Lib/_pyrepl/readline.py | 7 ++++++- Lib/test/test_pyrepl/test_pyrepl.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 1d00de3683db231..ae07db5f7961d6e 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -612,7 +612,12 @@ def _setup(namespace: Mapping[str, Any]) -> None: f_out = sys.stdout.fileno() except (AttributeError, ValueError): return - if not os.isatty(f_in) or not os.isatty(f_out): + if ( + not os.isatty(f_in) + or not os.isatty(f_out) + or os.device_encoding(f_in) is None + or os.device_encoding(f_out) is None + ): return _wrapper.f_in = f_in diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 1e0f99f5f97c83a..b494660c135c7a7 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1100,6 +1100,20 @@ def test_get_completions_sorts_colored_matches_by_visible_text(self): class TestPyReplReadlineSetup(TestCase): + @skipUnless(has_subprocess_support, "test requires subprocess support") + @skipUnless(sys.platform == "win32", "Windows-specific test") + def test_nul_stdin_exits_cleanly(self): + proc = subprocess.run( + [sys.executable, "-I"], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + timeout=SHORT_TIMEOUT, + ) + + self.assertEqual(proc.returncode, 0, proc.stderr) + def test_setup_ignores_basic_completer_env_when_env_is_disabled(self): class FakeFancyCompleter: def __init__(self, namespace): @@ -1129,6 +1143,11 @@ class FakeBasicCompleter(FakeFancyCompleter): patch.object(pyrepl_readline.sys, "stdout", stdout), patch.object(pyrepl_readline.sys, "flags", Mock(ignore_environment=True)), patch.object(pyrepl_readline.os, "isatty", return_value=True), + patch.object( + pyrepl_readline.os, + "device_encoding", + return_value="utf-8", + ), patch.object(pyrepl_readline.os, "getenv") as mock_getenv, patch("builtins.input", lambda prompt="": prompt), ): From 378c2c7acae88dcaf6f0f7f62019312d977a5ef0 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 13 Oct 2026 11:05:02 -0700 Subject: [PATCH 2/3] Move fix to main.py --- Lib/_pyrepl/main.py | 9 ++++++++- Lib/_pyrepl/readline.py | 7 +------ Lib/test/test_pyrepl/test_pyrepl.py | 10 ++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/_pyrepl/main.py b/Lib/_pyrepl/main.py index 447eb1e551e7743..b4cbf8f64c8767d 100644 --- a/Lib/_pyrepl/main.py +++ b/Lib/_pyrepl/main.py @@ -9,7 +9,14 @@ try: if sys.platform == "win32" and sys.getwindowsversion().build < 10586: raise RuntimeError("Windows 10 TH2 or later required") - if not os.isatty(sys.stdin.fileno()): + stdin_fd = sys.stdin.fileno() + if ( + not os.isatty(stdin_fd) + or ( + sys.platform == "win32" + and os.device_encoding(stdin_fd) is None + ) + ): raise OSError(errno.ENOTTY, "tty required", "stdin") from .simple_interact import check if err := check(): diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index ae07db5f7961d6e..1d00de3683db231 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -612,12 +612,7 @@ def _setup(namespace: Mapping[str, Any]) -> None: f_out = sys.stdout.fileno() except (AttributeError, ValueError): return - if ( - not os.isatty(f_in) - or not os.isatty(f_out) - or os.device_encoding(f_in) is None - or os.device_encoding(f_out) is None - ): + if not os.isatty(f_in) or not os.isatty(f_out): return _wrapper.f_in = f_in diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index b494660c135c7a7..b1df04309829baf 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1099,7 +1099,7 @@ def test_get_completions_sorts_colored_matches_by_visible_text(self): ) -class TestPyReplReadlineSetup(TestCase): +class TestPyReplMain(TestCase): @skipUnless(has_subprocess_support, "test requires subprocess support") @skipUnless(sys.platform == "win32", "Windows-specific test") def test_nul_stdin_exits_cleanly(self): @@ -1113,7 +1113,10 @@ def test_nul_stdin_exits_cleanly(self): ) self.assertEqual(proc.returncode, 0, proc.stderr) + self.assertIn("tty required", proc.stderr) + +class TestPyReplReadlineSetup(TestCase): def test_setup_ignores_basic_completer_env_when_env_is_disabled(self): class FakeFancyCompleter: def __init__(self, namespace): @@ -1143,11 +1146,6 @@ class FakeBasicCompleter(FakeFancyCompleter): patch.object(pyrepl_readline.sys, "stdout", stdout), patch.object(pyrepl_readline.sys, "flags", Mock(ignore_environment=True)), patch.object(pyrepl_readline.os, "isatty", return_value=True), - patch.object( - pyrepl_readline.os, - "device_encoding", - return_value="utf-8", - ), patch.object(pyrepl_readline.os, "getenv") as mock_getenv, patch("builtins.input", lambda prompt="": prompt), ): From bb0d9502ef962b97966b87520c2fe57b43d84692 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:57:50 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-10-13-21-57-48.gh-issue-159323.9S_NE2.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-13-21-57-48.gh-issue-159323.9S_NE2.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-13-21-57-48.gh-issue-159323.9S_NE2.rst b/Misc/NEWS.d/next/Library/2026-10-13-21-57-48.gh-issue-159323.9S_NE2.rst new file mode 100644 index 000000000000000..f41067bfdccc670 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-13-21-57-48.gh-issue-159323.9S_NE2.rst @@ -0,0 +1 @@ +PyREPL on Windows now exits when ``NUL`` is passed as ``stdin`` instead of showing stream of tracebacks.