Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Include/internal/pycore_pylifecycle.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down
6 changes: 6 additions & 0 deletions Include/internal/pycore_pythonrun.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_runpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 15 additions & 7 deletions Modules/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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) {
Expand All @@ -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), "<string>", &cf);
PyObject *result = _PyRun_SimpleStringFlagsNoPrint(PyBytes_AsString(bytes), "<string>", &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");
Expand Down Expand Up @@ -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
Expand Down
104 changes: 71 additions & 33 deletions Python/pythonrun.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
Expand All @@ -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, "<stdin>")) &&
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;
}


Expand All @@ -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)
{
Expand Down
Loading