From 88d4cd7658472c7c41ddcf78b503c7e542c3332d Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Thu, 25 Jun 2026 20:32:06 +0800 Subject: [PATCH 1/6] Fix Py_RunMain: don't call exit() on sys.exit() in run_command/run_filename paths --- Include/internal/pycore_pythonrun.h | 6 +++ Modules/main.c | 30 +++++++++--- Python/pythonrun.c | 76 +++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h index 66dd7cd843b04f..a556fc078e6e59 100644 --- a/Include/internal/pycore_pythonrun.h +++ b/Include/internal/pycore_pythonrun.h @@ -20,6 +20,12 @@ extern int _PyRun_AnyFileObject( int closeit, PyCompilerFlags *flags); +extern PyObject * _PyRun_SimpleFileObjectEx( + FILE *fp, + PyObject *filename, + int closeit, + PyCompilerFlags *flags); + extern int _PyRun_InteractiveLoopObject( FILE *fp, PyObject *filename, diff --git a/Modules/main.c b/Modules/main.c index a4dfddd98e257e..98e8811d09f465 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -10,7 +10,7 @@ #include "pycore_pathconfig.h" // _PyPathConfig_ComputeSysPath0() #include "pycore_pylifecycle.h" // _Py_PreInitializeFromPyArgv() #include "pycore_pystate.h" // _PyInterpreterState_GET() -#include "pycore_pythonrun.h" // _PyRun_AnyFileObject() +#include "pycore_pythonrun.h" // _PyRun_SimpleFileObjectEx() #include "pycore_tuple.h" // _PyTuple_FromPair #include "pycore_unicodeobject.h" // _PyUnicode_Dedent() @@ -235,7 +235,6 @@ static int pymain_run_command(wchar_t *command) { PyObject *unicode, *bytes; - int ret; unicode = PyUnicode_FromWideChar(command, -1); if (unicode == NULL) { @@ -259,9 +258,21 @@ pymain_run_command(wchar_t *command) PyCompilerFlags cf = _PyCompilerFlags_INIT; cf.cf_flags |= PyCF_IGNORE_COOKIE; - ret = _PyRun_SimpleStringFlagsWithName(PyBytes_AsString(bytes), "", &cf); + PyObject *main_module = PyImport_AddModuleRef("__main__"); + if (main_module == NULL) { + Py_DECREF(bytes); + return pymain_exit_err_print(); + } + PyObject *dict = PyModule_GetDict(main_module); + PyObject *res = PyRun_StringFlags(PyBytes_AsString(bytes), Py_file_input, + dict, dict, &cf); + Py_DECREF(main_module); Py_DECREF(bytes); - return (ret != 0); + if (res == NULL) { + return pymain_exit_err_print(); + } + Py_DECREF(res); + return 0; error: PySys_WriteStderr("Unable to decode the command from the command line:\n"); @@ -406,10 +417,15 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename, return pymain_exit_err_print(); } - /* PyRun_AnyFileExFlags(closeit=1) calls fclose(fp) before running code */ + /* Use _PyRun_SimpleFileObjectEx which returns PyObject* without calling + PyErr_Print(), so we can handle SystemExit properly via pymain_exit_err_print. */ PyCompilerFlags cf = _PyCompilerFlags_INIT; - int run = _PyRun_AnyFileObject(fp, filename, 1, &cf); - return (run != 0); + PyObject *v = _PyRun_SimpleFileObjectEx(fp, filename, 1, &cf); + if (v == NULL) { + return pymain_exit_err_print(); + } + Py_DECREF(v); + return 0; } static int diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 971ab064777a41..af1ed4a629d07d 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -538,6 +538,82 @@ _PyRun_SimpleFileObject(FILE *fp, PyObject *filename, int closeit, } +/* Variant of _PyRun_SimpleFileObject that returns the result object + instead of calling PyErr_Print() on failure. The caller (typically + pymain_run_file_obj in Modules/main.c) should handle the error + with _Py_HandleSystemExitAndKeyboardInterrupt or pymain_exit_err_print. */ +PyObject * +_PyRun_SimpleFileObjectEx(FILE *fp, PyObject *filename, int closeit, + PyCompilerFlags *flags) +{ + PyObject *v = NULL; + + PyObject *main_module = PyImport_AddModuleRef("__main__"); + if (main_module == NULL) + return NULL; + PyObject *dict = PyModule_GetDict(main_module); // borrowed ref + + int set_file_name = 0; + int has_file = PyDict_ContainsString(dict, "__file__"); + if (has_file < 0) { + goto done; + } + if (!has_file) { + if (PyDict_SetItemString(dict, "__file__", filename) < 0) { + goto done; + } + set_file_name = 1; + } + + int pyc = maybe_pyc_file(fp, filename, closeit); + if (pyc < 0) { + goto done; + } + + if (pyc) { + FILE *pyc_fp; + /* Try to run a pyc file. First, re-open in binary */ + if (closeit) { + fclose(fp); + closeit = 0; // already closed + } + + pyc_fp = Py_fopen(filename, "rb"); + if (pyc_fp == NULL) { + fprintf(stderr, "python: Can't reopen .pyc file\n"); + goto done; + } + + if (set_main_loader(dict, filename, "SourcelessFileLoader") < 0) { + fprintf(stderr, "python: failed to set __main__.__loader__\n"); + fclose(pyc_fp); + goto done; + } + v = run_pyc_file(pyc_fp, dict, dict, flags); + } else { + /* When running from stdin, leave __main__.__loader__ alone */ + if ((!PyUnicode_Check(filename) || !PyUnicode_EqualToUTF8(filename, "")) && + set_main_loader(dict, filename, "SourceFileLoader") < 0) { + fprintf(stderr, "python: failed to set __main__.__loader__\n"); + goto done; + } + v = pyrun_file(fp, filename, Py_file_input, dict, dict, + closeit, flags); + } + flush_io(); + +done: + if (set_file_name) { + if (PyDict_PopString(dict, "__file__", NULL) < 0) { + /* Non-fatal cleanup error; just clear it */ + PyErr_Clear(); + } + } + Py_XDECREF(main_module); + return v; +} + + int PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit, PyCompilerFlags *flags) From 220349e82c3243c6e8bdfc3a9645ba4d8656ecec Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Thu, 25 Jun 2026 20:37:21 +0800 Subject: [PATCH 2/6] Handle sys.exit() in Py_RunMain command and file paths --- .../next/C_API/2026-06-25-20-35-38.gh-issue-152132.Vj4s5T.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/C_API/2026-06-25-20-35-38.gh-issue-152132.Vj4s5T.rst diff --git a/Misc/NEWS.d/next/C_API/2026-06-25-20-35-38.gh-issue-152132.Vj4s5T.rst b/Misc/NEWS.d/next/C_API/2026-06-25-20-35-38.gh-issue-152132.Vj4s5T.rst new file mode 100644 index 00000000000000..45da4fbb83689c --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-06-25-20-35-38.gh-issue-152132.Vj4s5T.rst @@ -0,0 +1,3 @@ +Fix :c:func:`Py_RunMain` incorrectly calling ``exit()`` on +:exc:`SystemExit` when running a command or file. The exit code +is now returned to the caller. From e88db2f1ac568c51446f0fa7606122b9d4a4c37d Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Thu, 25 Jun 2026 21:33:12 +0800 Subject: [PATCH 3/6] gh-152132: Fix missing traceback source lines and lost exceptions Two issues introduced by the Py_RunMain SystemExit fix: 1. pymain_run_command lost traceback source lines for -c commands because PyRun_StringFlags does not register source in linecache. Add _PyRun_SimpleStringFlagsEx, which returns PyObject* without calling PyErr_Print(), and use it in pymain_run_command with the "" name so source linecache registration is preserved. 2. _PyRun_SimpleFileObjectEx could lose exceptions during cleanup when PyDict_PopString fails (e.g. under low-memory conditions). Save and restore the original exception around cleanup code when the main code failed; use PyErr_Print() for cleanup errors when the main code succeeded to match legacy behavior. --- Include/internal/pycore_pylifecycle.h | 5 +++ Modules/main.c | 10 +----- Python/pythonrun.c | 45 +++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index 12e1e78526db35..d01ec4f540230c 100644 --- a/Include/internal/pycore_pylifecycle.h +++ b/Include/internal/pycore_pylifecycle.h @@ -111,6 +111,11 @@ PyAPI_FUNC(char*) _Py_SetLocaleFromEnv(int category); // Export for special main.c string compiling with source tracebacks int _PyRun_SimpleStringFlagsWithName(const char *command, const char* name, PyCompilerFlags *flags); +// Like _PyRun_SimpleStringFlagsWithName but returns the result object +// instead of calling PyErr_Print() on failure. The caller should handle +// the error with _Py_HandleSystemExitAndKeyboardInterrupt or PyErr_Print. +PyObject *_PyRun_SimpleStringFlagsEx(const char *command, const char* name, PyCompilerFlags *flags); + /* interpreter config */ diff --git a/Modules/main.c b/Modules/main.c index 98e8811d09f465..658937d8e93949 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -258,15 +258,7 @@ pymain_run_command(wchar_t *command) PyCompilerFlags cf = _PyCompilerFlags_INIT; cf.cf_flags |= PyCF_IGNORE_COOKIE; - PyObject *main_module = PyImport_AddModuleRef("__main__"); - if (main_module == NULL) { - Py_DECREF(bytes); - return pymain_exit_err_print(); - } - PyObject *dict = PyModule_GetDict(main_module); - PyObject *res = PyRun_StringFlags(PyBytes_AsString(bytes), Py_file_input, - dict, dict, &cf); - Py_DECREF(main_module); + PyObject *res = _PyRun_SimpleStringFlagsEx(PyBytes_AsString(bytes), "", &cf); Py_DECREF(bytes); if (res == NULL) { return pymain_exit_err_print(); diff --git a/Python/pythonrun.c b/Python/pythonrun.c index af1ed4a629d07d..94ed7e79caa1ff 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -604,9 +604,21 @@ _PyRun_SimpleFileObjectEx(FILE *fp, PyObject *filename, int closeit, done: if (set_file_name) { - if (PyDict_PopString(dict, "__file__", NULL) < 0) { - /* Non-fatal cleanup error; just clear it */ - PyErr_Clear(); + if (v == NULL) { + // Main code failed: save the exception before cleanup + // so PyDict_PopString doesn't overwrite it + PyObject *saved_exc = PyErr_GetRaisedException(); + if (PyDict_PopString(dict, "__file__", NULL) < 0) { + /* Non-fatal cleanup error; just clear it */ + PyErr_Clear(); + } + PyErr_SetRaisedException(saved_exc); + } else { + // Main code succeeded: if cleanup fails, print it + // to match legacy behavior + if (PyDict_PopString(dict, "__file__", NULL) < 0) { + PyErr_Print(); + } } } Py_XDECREF(main_module); @@ -659,6 +671,33 @@ _PyRun_SimpleStringFlagsWithName(const char *command, const char* name, PyCompil return 0; } +PyObject * +_PyRun_SimpleStringFlagsEx(const char *command, const char* name, PyCompilerFlags *flags) +{ + PyObject *main_module = PyImport_AddModuleRef("__main__"); + if (main_module == NULL) { + return NULL; + } + PyObject *dict = PyModule_GetDict(main_module); // borrowed ref + + PyObject *res = NULL; + if (name == NULL) { + res = PyRun_StringFlags(command, Py_file_input, dict, dict, flags); + } else { + PyObject* the_name = PyUnicode_FromString(name); + if (!the_name) { + Py_DECREF(main_module); + return NULL; + } + res = _PyRun_StringFlagsWithName(command, the_name, Py_file_input, + dict, dict, flags, 0); + Py_DECREF(the_name); + } + Py_DECREF(main_module); + return res; +} + + int PyRun_SimpleStringFlags(const char *command, PyCompilerFlags *flags) { From 1f395196436b5ff3a1031bcfaf9eedddca3caeda Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Fri, 26 Jun 2026 09:57:09 +0800 Subject: [PATCH 4/6] =?UTF-8?q?gh-152132:=20Address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20rename=20helpers,=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename _PyRun_SimpleFileObjectEx → _PyRun_SimpleFileObjectNoPrint to avoid the deprecated 'Ex' suffix. Make _PyRun_SimpleFileObject a thin wrapper around it to avoid code duplication. - Rename _PyRun_SimpleStringFlagsEx → _PyRun_SimpleStringFlagsNoPrint and make _PyRun_SimpleStringFlagsWithName a thin wrapper similarly. - Rename single-letter variable 'v' and 'res' to 'result' throughout. - Add test cases for sys.exit() handling via -c, file, -m and message. --- Include/internal/pycore_pylifecycle.h | 2 +- Include/internal/pycore_pythonrun.h | 2 +- Lib/test/test_runpy.py | 39 ++++++ Modules/main.c | 16 +-- Python/pythonrun.c | 181 ++++++++------------------ 5 files changed, 101 insertions(+), 139 deletions(-) diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index d01ec4f540230c..c4a5c9422c256b 100644 --- a/Include/internal/pycore_pylifecycle.h +++ b/Include/internal/pycore_pylifecycle.h @@ -114,7 +114,7 @@ int _PyRun_SimpleStringFlagsWithName(const char *command, const char* name, PyCo // Like _PyRun_SimpleStringFlagsWithName but returns the result object // instead of calling PyErr_Print() on failure. The caller should handle // the error with _Py_HandleSystemExitAndKeyboardInterrupt or PyErr_Print. -PyObject *_PyRun_SimpleStringFlagsEx(const char *command, const char* name, PyCompilerFlags *flags); +PyObject *_PyRun_SimpleStringFlagsNoPrint(const char *command, const char* name, PyCompilerFlags *flags); /* interpreter config */ diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h index a556fc078e6e59..592605130650f9 100644 --- a/Include/internal/pycore_pythonrun.h +++ b/Include/internal/pycore_pythonrun.h @@ -20,7 +20,7 @@ extern int _PyRun_AnyFileObject( int closeit, PyCompilerFlags *flags); -extern PyObject * _PyRun_SimpleFileObjectEx( +extern PyObject * _PyRun_SimpleFileObjectNoPrint( FILE *fp, PyObject *filename, int closeit, diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py index 55b9673ef6c91c..e2b2e8e623beb2 100644 --- a/Lib/test/test_runpy.py +++ b/Lib/test/test_runpy.py @@ -916,6 +916,45 @@ def test_pymain_run_module(self): ham = self.ham self.assertSigInt(["-m", ham.stem], cwd=ham.parent) + # Tests for sys.exit() handling (gh-152132) + @requires_subprocess() + def test_sys_exit_run_command(self): + cmd = [sys.executable, '-c', 'import sys; sys.exit(42)'] + proc = subprocess.run(cmd, text=True, stderr=subprocess.PIPE) + self.assertEqual(proc.returncode, 42) + + @requires_subprocess() + def test_sys_exit_run_command_default(self): + cmd = [sys.executable, '-c', 'import sys; sys.exit()'] + proc = subprocess.run(cmd, text=True, stderr=subprocess.PIPE) + self.assertEqual(proc.returncode, 0) + + @requires_subprocess() + def test_sys_exit_run_command_message(self): + cmd = [sys.executable, '-c', "import sys; sys.exit('error message')"] + proc = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + self.assertEqual(proc.returncode, 1) + self.assertIn('error message', proc.stderr) + + @requires_subprocess() + def test_sys_exit_run_file(self): + with self.tmp_path() as tmp: + script = tmp / "exit_script.py" + script.write_text("import sys; sys.exit(42)") + cmd = [sys.executable, str(script)] + proc = subprocess.run(cmd, text=True, stderr=subprocess.PIPE) + self.assertEqual(proc.returncode, 42) + + @requires_subprocess() + def test_sys_exit_run_module(self): + with self.tmp_path() as tmp: + script = tmp / "exit_mod.py" + script.write_text("import sys; sys.exit(42)") + cmd = [sys.executable, '-m', 'exit_mod'] + proc = subprocess.run(cmd, cwd=tmp, text=True, stderr=subprocess.PIPE) + self.assertEqual(proc.returncode, 42) + if __name__ == "__main__": unittest.main() diff --git a/Modules/main.c b/Modules/main.c index 658937d8e93949..177a17bb03bdc2 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -10,7 +10,7 @@ #include "pycore_pathconfig.h" // _PyPathConfig_ComputeSysPath0() #include "pycore_pylifecycle.h" // _Py_PreInitializeFromPyArgv() #include "pycore_pystate.h" // _PyInterpreterState_GET() -#include "pycore_pythonrun.h" // _PyRun_SimpleFileObjectEx() +#include "pycore_pythonrun.h" // _PyRun_SimpleFileObjectNoPrint() #include "pycore_tuple.h" // _PyTuple_FromPair #include "pycore_unicodeobject.h" // _PyUnicode_Dedent() @@ -258,12 +258,12 @@ pymain_run_command(wchar_t *command) PyCompilerFlags cf = _PyCompilerFlags_INIT; cf.cf_flags |= PyCF_IGNORE_COOKIE; - PyObject *res = _PyRun_SimpleStringFlagsEx(PyBytes_AsString(bytes), "", &cf); + PyObject *result = _PyRun_SimpleStringFlagsNoPrint(PyBytes_AsString(bytes), "", &cf); Py_DECREF(bytes); - if (res == NULL) { + if (result == NULL) { return pymain_exit_err_print(); } - Py_DECREF(res); + Py_DECREF(result); return 0; error: @@ -409,14 +409,14 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename, return pymain_exit_err_print(); } - /* Use _PyRun_SimpleFileObjectEx which returns PyObject* without calling + /* Use _PyRun_SimpleFileObjectNoPrint which returns PyObject* without calling PyErr_Print(), so we can handle SystemExit properly via pymain_exit_err_print. */ PyCompilerFlags cf = _PyCompilerFlags_INIT; - PyObject *v = _PyRun_SimpleFileObjectEx(fp, filename, 1, &cf); - if (v == NULL) { + PyObject *result = _PyRun_SimpleFileObjectNoPrint(fp, filename, 1, &cf); + if (result == NULL) { return pymain_exit_err_print(); } - Py_DECREF(v); + Py_DECREF(result); return 0; } diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 94ed7e79caa1ff..9c8dbcacf8da48 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -458,95 +458,15 @@ set_main_loader(PyObject *d, PyObject *filename, const char *loader_name) } -int -_PyRun_SimpleFileObject(FILE *fp, PyObject *filename, int closeit, - PyCompilerFlags *flags) -{ - int ret = -1; - - PyObject *main_module = PyImport_AddModuleRef("__main__"); - if (main_module == NULL) - return -1; - PyObject *dict = PyModule_GetDict(main_module); // borrowed ref - - int set_file_name = 0; - int has_file = PyDict_ContainsString(dict, "__file__"); - if (has_file < 0) { - goto done; - } - if (!has_file) { - if (PyDict_SetItemString(dict, "__file__", filename) < 0) { - goto done; - } - set_file_name = 1; - } - - int pyc = maybe_pyc_file(fp, filename, closeit); - if (pyc < 0) { - goto done; - } - - PyObject *v; - if (pyc) { - FILE *pyc_fp; - /* Try to run a pyc file. First, re-open in binary */ - if (closeit) { - fclose(fp); - } - - pyc_fp = Py_fopen(filename, "rb"); - if (pyc_fp == NULL) { - fprintf(stderr, "python: Can't reopen .pyc file\n"); - goto done; - } - - if (set_main_loader(dict, filename, "SourcelessFileLoader") < 0) { - fprintf(stderr, "python: failed to set __main__.__loader__\n"); - ret = -1; - fclose(pyc_fp); - goto done; - } - v = run_pyc_file(pyc_fp, dict, dict, flags); - } else { - /* When running from stdin, leave __main__.__loader__ alone */ - if ((!PyUnicode_Check(filename) || !PyUnicode_EqualToUTF8(filename, "")) && - set_main_loader(dict, filename, "SourceFileLoader") < 0) { - fprintf(stderr, "python: failed to set __main__.__loader__\n"); - ret = -1; - goto done; - } - v = pyrun_file(fp, filename, Py_file_input, dict, dict, - closeit, flags); - } - flush_io(); - if (v == NULL) { - Py_CLEAR(main_module); - PyErr_Print(); - goto done; - } - Py_DECREF(v); - ret = 0; - - done: - if (set_file_name) { - if (PyDict_PopString(dict, "__file__", NULL) < 0) { - PyErr_Print(); - } - } - Py_XDECREF(main_module); - return ret; -} - - -/* Variant of _PyRun_SimpleFileObject that returns the result object - instead of calling PyErr_Print() on failure. The caller (typically - pymain_run_file_obj in Modules/main.c) should handle the error - with _Py_HandleSystemExitAndKeyboardInterrupt or pymain_exit_err_print. */ +/* Run a simple file object. Returns the result PyObject* on success, + or NULL on failure. Does NOT call PyErr_Print(); the caller must + handle the error (e.g. with PyErr_Print() or + _Py_HandleSystemExitAndKeyboardInterrupt()). */ PyObject * -_PyRun_SimpleFileObjectEx(FILE *fp, PyObject *filename, int closeit, - PyCompilerFlags *flags) +_PyRun_SimpleFileObjectNoPrint(FILE *fp, PyObject *filename, int closeit, + PyCompilerFlags *flags) { - PyObject *v = NULL; + PyObject *result = NULL; PyObject *main_module = PyImport_AddModuleRef("__main__"); if (main_module == NULL) @@ -589,7 +509,7 @@ _PyRun_SimpleFileObjectEx(FILE *fp, PyObject *filename, int closeit, fclose(pyc_fp); goto done; } - v = run_pyc_file(pyc_fp, dict, dict, flags); + result = run_pyc_file(pyc_fp, dict, dict, flags); } else { /* When running from stdin, leave __main__.__loader__ alone */ if ((!PyUnicode_Check(filename) || !PyUnicode_EqualToUTF8(filename, "")) && @@ -597,14 +517,14 @@ _PyRun_SimpleFileObjectEx(FILE *fp, PyObject *filename, int closeit, fprintf(stderr, "python: failed to set __main__.__loader__\n"); goto done; } - v = pyrun_file(fp, filename, Py_file_input, dict, dict, - closeit, flags); + result = pyrun_file(fp, filename, Py_file_input, dict, dict, + closeit, flags); } flush_io(); done: if (set_file_name) { - if (v == NULL) { + if (result == NULL) { // Main code failed: save the exception before cleanup // so PyDict_PopString doesn't overwrite it PyObject *saved_exc = PyErr_GetRaisedException(); @@ -622,7 +542,22 @@ _PyRun_SimpleFileObjectEx(FILE *fp, PyObject *filename, int closeit, } } Py_XDECREF(main_module); - return v; + return result; +} + + +int +_PyRun_SimpleFileObject(FILE *fp, PyObject *filename, int closeit, + PyCompilerFlags *flags) +{ + PyObject *result = _PyRun_SimpleFileObjectNoPrint(fp, filename, closeit, + flags); + if (result == NULL) { + PyErr_Print(); + return -1; + } + Py_DECREF(result); + return 0; } @@ -640,39 +575,13 @@ PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit, } -int -_PyRun_SimpleStringFlagsWithName(const char *command, const char* name, PyCompilerFlags *flags) { - PyObject *main_module = PyImport_AddModuleRef("__main__"); - if (main_module == NULL) { - return -1; - } - PyObject *dict = PyModule_GetDict(main_module); // borrowed ref - - PyObject *res = NULL; - if (name == NULL) { - res = PyRun_StringFlags(command, Py_file_input, dict, dict, flags); - } else { - PyObject* the_name = PyUnicode_FromString(name); - if (!the_name) { - PyErr_Print(); - Py_DECREF(main_module); - return -1; - } - res = _PyRun_StringFlagsWithName(command, the_name, Py_file_input, dict, dict, flags, 0); - Py_DECREF(the_name); - } - Py_DECREF(main_module); - if (res == NULL) { - PyErr_Print(); - return -1; - } - - Py_DECREF(res); - return 0; -} - +/* Run a simple string. Returns the result PyObject* on success, + or NULL on failure. Does NOT call PyErr_Print(); the caller must + handle the error (e.g. with PyErr_Print() or + _Py_HandleSystemExitAndKeyboardInterrupt()). */ PyObject * -_PyRun_SimpleStringFlagsEx(const char *command, const char* name, PyCompilerFlags *flags) +_PyRun_SimpleStringFlagsNoPrint(const char *command, const char* name, + PyCompilerFlags *flags) { PyObject *main_module = PyImport_AddModuleRef("__main__"); if (main_module == NULL) { @@ -680,21 +589,35 @@ _PyRun_SimpleStringFlagsEx(const char *command, const char* name, PyCompilerFlag } PyObject *dict = PyModule_GetDict(main_module); // borrowed ref - PyObject *res = NULL; + PyObject *result = NULL; if (name == NULL) { - res = PyRun_StringFlags(command, Py_file_input, dict, dict, flags); + result = PyRun_StringFlags(command, Py_file_input, dict, dict, flags); } else { PyObject* the_name = PyUnicode_FromString(name); if (!the_name) { Py_DECREF(main_module); return NULL; } - res = _PyRun_StringFlagsWithName(command, the_name, Py_file_input, - dict, dict, flags, 0); + result = _PyRun_StringFlagsWithName(command, the_name, Py_file_input, + dict, dict, flags, 0); Py_DECREF(the_name); } Py_DECREF(main_module); - return res; + return result; +} + + +int +_PyRun_SimpleStringFlagsWithName(const char *command, const char* name, + PyCompilerFlags *flags) +{ + PyObject *result = _PyRun_SimpleStringFlagsNoPrint(command, name, flags); + if (result == NULL) { + PyErr_Print(); + return -1; + } + Py_DECREF(result); + return 0; } From 18b79106d3a91754a6b4d15ce3cdb64b390b7aa0 Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Sat, 27 Jun 2026 15:03:17 +0800 Subject: [PATCH 5/6] gh-152132: Restore _Py_FdIsInteractive check and add embed tests --- Lib/test/test_embed.py | 22 ++++++++++++++++ Lib/test/test_runpy.py | 2 ++ Modules/main.c | 11 +++++++- Programs/_testembed.c | 57 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 2d1533c46b98f3..507b238cd4a1e9 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -417,6 +417,28 @@ def test_run_main_loop(self): self.assertEqual(out, "Py_RunMain(): sys.argv=['-c', 'arg2']\n" * nloop) self.assertEqual(err, '') + def test_run_main_system_exit_command(self): + # gh-152132: Py_RunMain() must return the SystemExit code to the + # embedding application, not call exit() directly. + out, err = self.run_embedded_interpreter( + "test_run_main_system_exit_command") + self.assertEqual(out, '') + self.assertEqual(err, '') + + def test_run_main_system_exit_file(self): + # gh-152132: same as above, but for the run_filename code path. + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', + delete=False) as f: + f.write("import sys; sys.exit(42)\n") + filename = f.name + try: + out, err = self.run_embedded_interpreter( + "test_run_main_system_exit_file", filename) + self.assertEqual(out, '') + self.assertEqual(err, '') + finally: + os.unlink(filename) + def test_finalize_structseq(self): # bpo-46417: Py_Finalize() clears structseq static types. Check that # sys attributes using struct types still work when diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py index e2b2e8e623beb2..7f0162c56acedd 100644 --- a/Lib/test/test_runpy.py +++ b/Lib/test/test_runpy.py @@ -917,6 +917,8 @@ def test_pymain_run_module(self): self.assertSigInt(["-m", ham.stem], cwd=ham.parent) # Tests for sys.exit() handling (gh-152132) + # These only exercise the standard command-line path; the embedding API + # path is tested in Lib/test/test_embed.py. @requires_subprocess() def test_sys_exit_run_command(self): cmd = [sys.executable, '-c', 'import sys; sys.exit(42)'] diff --git a/Modules/main.c b/Modules/main.c index 177a17bb03bdc2..5c603dc50e74f6 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -409,9 +409,18 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename, return pymain_exit_err_print(); } + PyCompilerFlags cf = _PyCompilerFlags_INIT; + + if (_Py_FdIsInteractive(fp, filename)) { + // Preserve the interactive REPL behavior for TTY inputs + // (e.g., "python3 /dev/stdin" when stdin is a terminal). + int run = _PyRun_InteractiveLoopObject(fp, filename, &cf); + fclose(fp); + return (run != 0); + } + /* Use _PyRun_SimpleFileObjectNoPrint which returns PyObject* without calling PyErr_Print(), so we can handle SystemExit properly via pymain_exit_err_print. */ - PyCompilerFlags cf = _PyCompilerFlags_INIT; PyObject *result = _PyRun_SimpleFileObjectNoPrint(fp, filename, 1, &cf); if (result == NULL) { return pymain_exit_err_print(); diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 74d0fe37ddaead..381ede833e08c2 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2057,6 +2057,61 @@ static int test_run_main_loop(void) } +static int test_run_main_system_exit_command(void) +{ + // gh-152132: Py_RunMain() must return the SystemExit code instead of + // calling exit() directly. + PyConfig config; + PyConfig_InitPythonConfig(&config); + + wchar_t *argv[] = {L"python3", L"-c", L"import sys; sys.exit(42)"}; + config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv); + config_set_string(&config, &config.program_name, L"./python3"); + init_from_config_clear(&config); + + int exitcode = Py_RunMain(); + if (exitcode != 42) { + fprintf(stderr, "expected exit code 42, got %d\n", exitcode); + return 1; + } + return 0; +} + + +static int test_run_main_system_exit_file(void) +{ + // gh-152132: Py_RunMain() must return the SystemExit code instead of + // calling exit() directly. + if (main_argc < 3) { + fprintf(stderr, "usage: %s test_run_main_system_exit_file SCRIPT\n", + PROGRAM); + return 1; + } + const char *filename = main_argv[2]; + wchar_t *wfilename = Py_DecodeLocale(filename, NULL); + if (wfilename == NULL) { + fprintf(stderr, "unable to decode filename\n"); + return 1; + } + + PyConfig config; + PyConfig_InitPythonConfig(&config); + + wchar_t *argv[] = {L"python3", wfilename}; + config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv); + config_set_string(&config, &config.program_name, L"./python3"); + init_from_config_clear(&config); + + int exitcode = Py_RunMain(); + PyMem_RawFree(wfilename); + if (exitcode != 42) { + fprintf(stderr, "expected exit code 42, got %d\n", exitcode); + return 1; + } + return 0; +} + + static int test_get_argc_argv(void) { PyConfig config; @@ -2951,6 +3006,8 @@ static struct TestCase TestCases[] = { {"test_initconfig_module", test_initconfig_module}, {"test_run_main", test_run_main}, {"test_run_main_loop", test_run_main_loop}, + {"test_run_main_system_exit_command", test_run_main_system_exit_command}, + {"test_run_main_system_exit_file", test_run_main_system_exit_file}, {"test_get_argc_argv", test_get_argc_argv}, {"test_init_use_frozen_modules", test_init_use_frozen_modules}, {"test_init_main_interpreter_settings", test_init_main_interpreter_settings}, From 7d7846e9d7ff26431a95bbcc4ece6a588b3efd9f Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Sat, 27 Jun 2026 15:11:53 +0800 Subject: [PATCH 6/6] gh-152132: Add embed tests for -m and message SystemExit paths Add test_run_main_system_exit_module and test_run_main_system_exit_command_message to _testembed.c and test_embed.py, covering the remaining sys.exit() paths mentioned in the review feedback (-m module and string message variants). --- Lib/test/test_embed.py | 19 +++++++++++++++++ Programs/_testembed.c | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 507b238cd4a1e9..1bd0558bf5fe19 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -439,6 +439,25 @@ def test_run_main_system_exit_file(self): finally: os.unlink(filename) + def test_run_main_system_exit_module(self): + # gh-152132: same as above, but for the run_module code path. + with tempfile.TemporaryDirectory() as tmpdir: + mod = os.path.join(tmpdir, "exit_mod.py") + with open(mod, "w", encoding="utf-8") as f: + f.write("import sys; sys.exit(42)\n") + env = dict(os.environ, PYTHONPATH=tmpdir) + out, err = self.run_embedded_interpreter( + "test_run_main_system_exit_module", env=env) + self.assertEqual(out, '') + self.assertEqual(err, '') + + def test_run_main_system_exit_command_message(self): + # gh-152132: same as above, but with a string exit message. + out, err = self.run_embedded_interpreter( + "test_run_main_system_exit_command_message") + self.assertEqual(out, '') + self.assertIn('error message', err) + def test_finalize_structseq(self): # bpo-46417: Py_Finalize() clears structseq static types. Check that # sys attributes using struct types still work when diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 381ede833e08c2..daaa0fdb0ccb4f 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2112,6 +2112,50 @@ static int test_run_main_system_exit_file(void) } +static int test_run_main_system_exit_module(void) +{ + // gh-152132: Py_RunMain() must return the SystemExit code instead of + // calling exit() directly. The module under -m is resolved from PYTHONPATH + // set by the test harness. + PyConfig config; + PyConfig_InitPythonConfig(&config); + + wchar_t *argv[] = {L"python3", L"-m", L"exit_mod"}; + config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv); + config_set_string(&config, &config.program_name, L"./python3"); + init_from_config_clear(&config); + + int exitcode = Py_RunMain(); + if (exitcode != 42) { + fprintf(stderr, "expected exit code 42, got %d\n", exitcode); + return 1; + } + return 0; +} + + +static int test_run_main_system_exit_command_message(void) +{ + // gh-152132: Py_RunMain() must return the SystemExit code instead of + // calling exit() directly. + PyConfig config; + PyConfig_InitPythonConfig(&config); + + wchar_t *argv[] = {L"python3", L"-c", + L"import sys; sys.exit('error message')"}; + config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv); + config_set_string(&config, &config.program_name, L"./python3"); + init_from_config_clear(&config); + + int exitcode = Py_RunMain(); + if (exitcode != 1) { + fprintf(stderr, "expected exit code 1, got %d\n", exitcode); + return 1; + } + return 0; +} + + static int test_get_argc_argv(void) { PyConfig config; @@ -3008,6 +3052,8 @@ static struct TestCase TestCases[] = { {"test_run_main_loop", test_run_main_loop}, {"test_run_main_system_exit_command", test_run_main_system_exit_command}, {"test_run_main_system_exit_file", test_run_main_system_exit_file}, + {"test_run_main_system_exit_module", test_run_main_system_exit_module}, + {"test_run_main_system_exit_command_message", test_run_main_system_exit_command_message}, {"test_get_argc_argv", test_get_argc_argv}, {"test_init_use_frozen_modules", test_init_use_frozen_modules}, {"test_init_main_interpreter_settings", test_init_main_interpreter_settings},