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/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 1e0f99f5f97c83a..b1df04309829baf 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1099,6 +1099,23 @@ def test_get_completions_sorts_colored_matches_by_visible_text(self): ) +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): + 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) + self.assertIn("tty required", proc.stderr) + + class TestPyReplReadlineSetup(TestCase): def test_setup_ignores_basic_completer_env_when_env_is_disabled(self): class FakeFancyCompleter: 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.