diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index 12e1e78526db35a..c4a5c9422c256bb 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_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 66dd7cd843b04fc..592605130650f9a 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_SimpleFileObjectNoPrint( + FILE *fp, + PyObject *filename, + int closeit, + PyCompilerFlags *flags); + extern int _PyRun_InteractiveLoopObject( FILE *fp, PyObject *filename, diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py index 55b9673ef6c91c0..e2b2e8e623beb21 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/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 000000000000000..45da4fbb83689cc --- /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. diff --git a/Modules/main.c b/Modules/main.c index a4dfddd98e257e2..177a17bb03bdc20 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_SimpleFileObjectNoPrint() #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,13 @@ pymain_run_command(wchar_t *command) PyCompilerFlags cf = _PyCompilerFlags_INIT; cf.cf_flags |= PyCF_IGNORE_COOKIE; - ret = _PyRun_SimpleStringFlagsWithName(PyBytes_AsString(bytes), "", &cf); + PyObject *result = _PyRun_SimpleStringFlagsNoPrint(PyBytes_AsString(bytes), "", &cf); Py_DECREF(bytes); - return (ret != 0); + if (result == NULL) { + return pymain_exit_err_print(); + } + Py_DECREF(result); + return 0; error: PySys_WriteStderr("Unable to decode the command from the command line:\n"); @@ -406,10 +409,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_SimpleFileObjectNoPrint 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 *result = _PyRun_SimpleFileObjectNoPrint(fp, filename, 1, &cf); + if (result == NULL) { + return pymain_exit_err_print(); + } + Py_DECREF(result); + return 0; } static int diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 971ab064777a418..9c8dbcacf8da48b 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -458,15 +458,19 @@ set_main_loader(PyObject *d, PyObject *filename, const char *loader_name) } -int -_PyRun_SimpleFileObject(FILE *fp, PyObject *filename, int closeit, - PyCompilerFlags *flags) +/* 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_SimpleFileObjectNoPrint(FILE *fp, PyObject *filename, int closeit, + PyCompilerFlags *flags) { - int ret = -1; + PyObject *result = NULL; PyObject *main_module = PyImport_AddModuleRef("__main__"); if (main_module == NULL) - return -1; + return NULL; PyObject *dict = PyModule_GetDict(main_module); // borrowed ref int set_file_name = 0; @@ -486,12 +490,12 @@ _PyRun_SimpleFileObject(FILE *fp, PyObject *filename, int closeit, goto done; } - PyObject *v; 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"); @@ -502,39 +506,58 @@ _PyRun_SimpleFileObject(FILE *fp, PyObject *filename, int closeit, 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); + 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, "")) && 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); + result = 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: +done: if (set_file_name) { - if (PyDict_PopString(dict, "__file__", NULL) < 0) { - PyErr_Print(); + if (result == 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); - return ret; + 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; } @@ -552,37 +575,52 @@ PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit, } -int -_PyRun_SimpleStringFlagsWithName(const char *command, const char* name, PyCompilerFlags *flags) { +/* 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_SimpleStringFlagsNoPrint(const char *command, const char* name, + PyCompilerFlags *flags) +{ PyObject *main_module = PyImport_AddModuleRef("__main__"); if (main_module == NULL) { - return -1; + return NULL; } 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) { - PyErr_Print(); Py_DECREF(main_module); - return -1; + 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); - if (res == NULL) { + 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(res); + Py_DECREF(result); return 0; } + int PyRun_SimpleStringFlags(const char *command, PyCompilerFlags *flags) {