Skip to content

Commit d84f18e

Browse files
committed
gh-152132: Fix Py_RunMain() to return an exit code
Fix Py_RunMain() to return an exit code, rather than calling Py_Exit(), when running a script, a command, or the REPL. Changes: * Add E_EXITCODE return value to <errcode.h> header. * Add exitcode parameter to internal functions: _PyRun_SimpleFile(), _PyRun_AnyFile() and _PyRun_InteractiveLoop(). * Add unit tests. * pymain_repl() now displays the error if PySys_Audit() or import _pyrepl failed.
1 parent ce96dd6 commit d84f18e

8 files changed

Lines changed: 343 additions & 157 deletions

File tree

Include/errcode.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ extern "C" {
3838
#define E_BADSINGLE 27 /* Ill-formed single statement input */
3939
#define E_INTERACT_STOP 28 /* Interactive mode stopped tokenization */
4040
#define E_COLUMNOVERFLOW 29 /* Column offset overflow */
41+
#define E_EXITCODE 30 /* (internal) got SystemExit, use exitcode */
4142

4243
#ifdef __cplusplus
4344
}

Include/internal/pycore_pylifecycle.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ extern int _Py_LegacyLocaleDetected(int warn);
109109
PyAPI_FUNC(char*) _Py_SetLocaleFromEnv(int category);
110110

111111
// Export for special main.c string compiling with source tracebacks
112-
int _PyRun_SimpleStringFlagsWithName(const char *command, const char* name, PyCompilerFlags *flags);
112+
extern int _PyRun_SimpleString(
113+
const char *command,
114+
const char* name,
115+
PyCompilerFlags *flags,
116+
int *exitcode);
113117

114118

115119
/* interpreter config */

Include/internal/pycore_pythonrun.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,25 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11-
extern int _PyRun_SimpleFileObject(
11+
extern int _PyRun_SimpleFile(
1212
FILE *fp,
1313
PyObject *filename,
1414
int closeit,
15-
PyCompilerFlags *flags);
15+
PyCompilerFlags *flags,
16+
int *exitcode);
1617

17-
extern int _PyRun_AnyFileObject(
18+
extern int _PyRun_AnyFile(
1819
FILE *fp,
1920
PyObject *filename,
2021
int closeit,
21-
PyCompilerFlags *flags);
22+
PyCompilerFlags *flags,
23+
int *exitcode);
2224

23-
extern int _PyRun_InteractiveLoopObject(
25+
extern int _PyRun_InteractiveLoop(
2426
FILE *fp,
2527
PyObject *filename,
26-
PyCompilerFlags *flags);
28+
PyCompilerFlags *flags,
29+
int *exitcode);
2730

2831
extern int _PyObject_SupportedAsScript(PyObject *);
2932
extern const char* _Py_SourceAsString(

Lib/test/test_embed.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
if not os.path.isfile(os.path.join(STDLIB_INSTALL, 'os.py')):
6969
STDLIB_INSTALL = None
7070

71+
CODE_EXITCODE_123 = 'raise SystemExit(123)'
72+
73+
7174
def debug_build(program):
7275
program = os.path.basename(program)
7376
name = os.path.splitext(program)[0]
@@ -140,12 +143,16 @@ def run_embedded_interpreter(self, *args, env=None,
140143
env = env.copy()
141144
env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
142145

143-
p = subprocess.Popen(cmd,
144-
stdout=subprocess.PIPE,
145-
stderr=subprocess.PIPE,
146-
universal_newlines=True,
147-
env=env,
148-
cwd=cwd)
146+
kwargs = dict(
147+
stdout=subprocess.PIPE,
148+
stderr=subprocess.PIPE,
149+
universal_newlines=True,
150+
env=env,
151+
cwd=cwd,
152+
)
153+
if input is not None:
154+
kwargs['stdin'] = subprocess.PIPE
155+
p = subprocess.Popen(cmd, **kwargs)
149156
try:
150157
(out, err) = p.communicate(input=input, timeout=timeout)
151158
except:
@@ -590,6 +597,40 @@ def _nogil_filtered_err(err: str, mod_name: str) -> str:
590597
]
591598
return "\n".join(filtered_err_lines)
592599

600+
def test_init_run_main_code_exitcode(self):
601+
env = dict(os.environ)
602+
env['TEST_CODE'] = CODE_EXITCODE_123
603+
self.run_embedded_interpreter("test_init_run_main_code_exitcode",
604+
env=env)
605+
606+
def test_init_run_main_script_exitcode(self):
607+
filename = os.path.abspath(os_helper.TESTFN + '.py')
608+
with open(filename, 'x', encoding='utf8') as fp:
609+
fp.write(CODE_EXITCODE_123)
610+
self.addCleanup(os_helper.unlink, filename)
611+
612+
env = dict(os.environ)
613+
env['TEST_SCRIPT'] = filename
614+
self.run_embedded_interpreter("test_init_run_main_script_exitcode",
615+
env=env)
616+
617+
def test_init_run_main_interactive_exitcode(self):
618+
code = CODE_EXITCODE_123
619+
self.run_embedded_interpreter("test_init_run_main_interactive_exitcode",
620+
input=code)
621+
622+
def test_init_run_main_module_exitcode(self):
623+
modname = '_testembed_testmodule'
624+
filename = os.path.abspath(modname + '.py')
625+
with open(filename, 'x', encoding='utf8') as fp:
626+
fp.write(CODE_EXITCODE_123)
627+
self.addCleanup(os_helper.unlink, filename)
628+
629+
env = dict(os.environ)
630+
env['TEST_MODULE'] = modname
631+
self.run_embedded_interpreter("test_init_run_main_module_exitcode",
632+
env=env)
633+
593634

594635
def config_dev_mode(preconfig, config):
595636
preconfig['allocator'] = PYMEM_ALLOCATOR_DEBUG
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :c:func:`Py_RunMain` to return an exit code, rather than calling
2+
:c:func:`Py_Exit`, when running a script, a command, or the REPL. Patch by
3+
Victor Stinner.

0 commit comments

Comments
 (0)