Bug report
Bug description:
TL;DR - Py_RunMain correctly handles sys.exit() when running a module (control is returned to embedding application and return value is the exit code), but not when running a command or a file path (process exits and control is never returned to embedding application).
Specifically, I'm attempting to embed Python in another application using Py_BytesMain to call into Python, passing it the path to a script and the command line arguments. According to the documentation, Py_BytesMain, Py_Main, and Py_RunMain all share the following behavior when Python calls sys.exit():
If PyConfig.inspect is not set (the default), the return value will be 0 if the interpreter exits normally (that is, without raising an exception), the exit status of an unhandled SystemExit, or 1 for any other unhandled exception.
However, this isn't the behavior I'm seeing. When calling Py_BytesMain, sys.exit() in the Python script results in the process exiting altogether, and control is never returned to the host application. Here's a minimal repro:
repro.c:
#include <Python.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("[C] Calling Py_BytesMain...\n");
int ret = Py_BytesMain(argc, argv);
printf("[C] Py_BytesMain returned: %d\n", ret);
return 0;
}
script.py:
import sys
print(f"[Python] Script argv: {sys.argv}")
print("[Python] Raising SystemExit(42) from script file")
sys.exit(42)
Compiling (gcc -o repro repro.c $(python3-config --cflags --ldflags --embed)) and running this results in:
[C] Calling Py_BytesMain...
[Python] Script argv: ['script.py', 'extra_arg1', 'extra_arg2']
[Python] Raising SystemExit(42) from script file
Note we never get to the "Py_BytesMain returned..." line, since the process exits upon the sys.exit call.
This definitely seems to be a bug, because Py_Main/Py_BytesMain/Py_RunMain are intended for embedding scenarios where it's desirable to return control to the host application with the Python exit code; indeed this was one of the motivations of PEP-587. The correct behavior also seems to be confirmed by e.g. GH-133264. @vstinner writes:
You're correct that Py_RunMain() is designed to embed Python in an application. Calling exit(code) would be bad in this case. Py_RunMain() return value is an exit code. SystemExit(code) becomes an exit code, it doesn't call exit(code).
Based on the code, it looks like Py_Main and Py_BytesMain both call into Py_RunMain. FWIW, when running a module with Py_RunMain, I do see the expected behavior, so PyConfig.run_module works, but PyConfig.run_command and PyConfig.run_filename do not. I believe this is because pymain_run_module correctly ensures that pymain_exit_err_print is called (this was added back in 1208328), but pymain_run_command calls _PyRun_SimpleStringFlagsWithName, which ends up calling PyErr_Print; similarly, pymain_run_file calls pymain_run_file_obj, which calls _PyRun_AnyFileObject (also, it doesn't seem to return the actual exit code, although that's immaterial atm since we never get back here:
|
int run = _PyRun_AnyFileObject(fp, filename, 1, &cf); |
|
return (run != 0); |
), and the latter calls _PyRun_SimpleFileObject which ends up calling Py_ErrPrint.
It's worth nothing that some of the underlying C-API functions mentioned above are also shared with the "Very High Level Layer" C-APIs, for which it is expected behavior / documented that SystemExit exits the process e.g. https://docs.python.org/3/c-api/veryhigh.html#c.PyRun_SimpleStringFlags says "Note that if an otherwise unhandled SystemExit is raised, this function will not return -1, but exit the process, as long as PyConfig.inspect is zero." And that's probably why this bug exists. However, this should not be the case for Py_Main / Py_BytesMain / Py_RunMain; presumably we'll need to ensure pymain_exit_err_print is called instead of PyErr_Print when going through these entry-points. (I do sort of wonder whether it would be better to do this unconditionally but the current behavior is at least documented for the Very High Level Layer APIs and it would be a breaking change to e.g. PyRun_SimpleFileExFlags).
I can certainly attempt a patch, though it might take me a bit longer since I'm not very familiar with the C-API (other than the code I read through last night while looking into this...). One fix would just be adding e.g. a print_errors parameter to toggle the behavior in the underlying functions, so e.g. pymain_run_file_obj could call _PyRun_AnyFileObject with that set to 0, whereas PyRun_SimpleFileExFlags could call it with 1. Though that strikes me as a bit ugly; might be better to introduce new functions that just return the PyObject* and then the existing ones can wrap it and call PyErr_Print or pymain_exit_err_print as appropriate. Also, first wanted to confirm my understanding that this is indeed a bug. Thanks!
CPython versions tested on:
3.14
Operating systems tested on:
Linux
Bug report
Bug description:
TL;DR - Py_RunMain correctly handles sys.exit() when running a module (control is returned to embedding application and return value is the exit code), but not when running a command or a file path (process exits and control is never returned to embedding application).
Specifically, I'm attempting to embed Python in another application using Py_BytesMain to call into Python, passing it the path to a script and the command line arguments. According to the documentation, Py_BytesMain, Py_Main, and Py_RunMain all share the following behavior when Python calls sys.exit():
However, this isn't the behavior I'm seeing. When calling Py_BytesMain, sys.exit() in the Python script results in the process exiting altogether, and control is never returned to the host application. Here's a minimal repro:
repro.c:
script.py:
Compiling (
gcc -o repro repro.c $(python3-config --cflags --ldflags --embed)) and running this results in:Note we never get to the "Py_BytesMain returned..." line, since the process exits upon the sys.exit call.
This definitely seems to be a bug, because Py_Main/Py_BytesMain/Py_RunMain are intended for embedding scenarios where it's desirable to return control to the host application with the Python exit code; indeed this was one of the motivations of PEP-587. The correct behavior also seems to be confirmed by e.g. GH-133264. @vstinner writes:
Based on the code, it looks like Py_Main and Py_BytesMain both call into Py_RunMain. FWIW, when running a module with Py_RunMain, I do see the expected behavior, so PyConfig.run_module works, but PyConfig.run_command and PyConfig.run_filename do not. I believe this is because pymain_run_module correctly ensures that pymain_exit_err_print is called (this was added back in 1208328), but pymain_run_command calls _PyRun_SimpleStringFlagsWithName, which ends up calling PyErr_Print; similarly, pymain_run_file calls pymain_run_file_obj, which calls _PyRun_AnyFileObject (also, it doesn't seem to return the actual exit code, although that's immaterial atm since we never get back here:
cpython/Modules/main.c
Lines 411 to 412 in b41dc4a
It's worth nothing that some of the underlying C-API functions mentioned above are also shared with the "Very High Level Layer" C-APIs, for which it is expected behavior / documented that SystemExit exits the process e.g. https://docs.python.org/3/c-api/veryhigh.html#c.PyRun_SimpleStringFlags says "Note that if an otherwise unhandled SystemExit is raised, this function will not return -1, but exit the process, as long as PyConfig.inspect is zero." And that's probably why this bug exists. However, this should not be the case for Py_Main / Py_BytesMain / Py_RunMain; presumably we'll need to ensure pymain_exit_err_print is called instead of PyErr_Print when going through these entry-points. (I do sort of wonder whether it would be better to do this unconditionally but the current behavior is at least documented for the Very High Level Layer APIs and it would be a breaking change to e.g. PyRun_SimpleFileExFlags).
I can certainly attempt a patch, though it might take me a bit longer since I'm not very familiar with the C-API (other than the code I read through last night while looking into this...). One fix would just be adding e.g. a print_errors parameter to toggle the behavior in the underlying functions, so e.g. pymain_run_file_obj could call _PyRun_AnyFileObject with that set to 0, whereas PyRun_SimpleFileExFlags could call it with 1. Though that strikes me as a bit ugly; might be better to introduce new functions that just return the PyObject* and then the existing ones can wrap it and call PyErr_Print or pymain_exit_err_print as appropriate. Also, first wanted to confirm my understanding that this is indeed a bug. Thanks!
CPython versions tested on:
3.14
Operating systems tested on:
Linux