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
41 changes: 41 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,47 @@ 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_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
Expand Down
41 changes: 41 additions & 0 deletions Lib/test/test_runpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,47 @@ def test_pymain_run_module(self):
ham = self.ham
self.assertSigInt(["-m", ham.stem], cwd=ham.parent)

# Tests for sys.exit() handling (gh-152132)
Comment thread
tangyuan0821 marked this conversation as resolved.
# 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)']
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.
31 changes: 24 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,24 @@ pymain_run_file_obj(PyObject *program_name, PyObject *filename,
return pymain_exit_err_print();
}

/* PyRun_AnyFileExFlags(closeit=1) calls fclose(fp) before running code */
PyCompilerFlags cf = _PyCompilerFlags_INIT;
int run = _PyRun_AnyFileObject(fp, filename, 1, &cf);
Comment thread
tangyuan0821 marked this conversation as resolved.
return (run != 0);

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. */
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
103 changes: 103 additions & 0 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -2057,6 +2057,105 @@ 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_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;
Expand Down Expand Up @@ -2951,6 +3050,10 @@ 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_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},
Expand Down
Loading
Loading