diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index ef09639189a6c2..6fb09a8b01a25d 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -116,15 +116,14 @@ Error Handling Get the *config* exit code. - * Set *\*exitcode* and return ``1`` if *config* has an exit code set. - * Return ``0`` if *config* has no exit code set. + Return ``0``. - Only the ``Py_InitializeFromInitConfig()`` function can set an exit - code if the ``parse_argv`` option is non-zero. + In Python 3.15, :c:func:`Py_InitializeFromInitConfig` sets an exit code if a + command line option wants to exit Python. This is no longer the case in + Python 3.16. Instead, the option is processed in :c:func:`Py_RunMain`. This + function became useless. - An exit code can be set when parsing the command line failed (exit - code ``2``) or when a command line option asks to display the command - line help (exit code ``0``). + .. deprecated:: next Get Options @@ -247,10 +246,10 @@ Initialize Python * Return ``0`` on success. * Set an error in *config* and return ``-1`` on error. - * Set an exit code in *config* and return ``-1`` if Python wants to - exit. - See ``PyInitConfig_GetExitcode()`` for the exit code case. + .. versionchanged:: next + The function no longer sets an exit code if a command line option wants + to exit Python. Instead, the option is processed in :c:func:`Py_RunMain`. .. _pyinitconfig-opts: @@ -690,9 +689,6 @@ Example of customized Python always running in isolated mode:: exception: PyConfig_Clear(&config); - if (PyStatus_IsExit(status)) { - return status.exitcode; - } /* Display the error message and exit the process with non-zero exit code */ Py_ExitStatusException(status); @@ -758,6 +754,8 @@ PyStatus Exit code. Argument passed to ``exit()``. + .. deprecated:: next + .. c:member:: const char *err_msg Error message. @@ -788,6 +786,11 @@ PyStatus Exit Python with the specified exit code. + .. deprecated:: next + :c:func:`Py_InitializeFromInitConfig` no longer sets an exit code if a + command line option wants to exit Python. Instead, the option is + processed in :c:func:`Py_RunMain`. + Functions to handle a status: .. c:function:: int PyStatus_Exception(PyStatus status) @@ -803,6 +806,11 @@ PyStatus Is the result an exit? + .. deprecated:: next + :c:func:`Py_InitializeFromInitConfig` no longer sets an exit code if a + command line option wants to exit Python. Instead, the option is + processed in :c:func:`Py_RunMain`. + .. c:function:: void Py_ExitStatusException(PyStatus status) Call ``exit(exitcode)`` if *status* is an exit. Print the error diff --git a/Doc/c-api/interp-lifecycle.rst b/Doc/c-api/interp-lifecycle.rst index bd4125cacdd0e6..efd208263bdf33 100644 --- a/Doc/c-api/interp-lifecycle.rst +++ b/Doc/c-api/interp-lifecycle.rst @@ -127,6 +127,11 @@ Initializing and finalizing the interpreter interpreter, populating the runtime configuration structure, and querying the returned status structure. + .. versionchanged:: next + The function no longer returns an exit code if a command line option + wants to exit Python. Instead, the option is processed in + :c:func:`Py_RunMain`. + .. c:function:: int Py_IsInitialized() diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 3d3125ad17b126..2ed01e390f784d 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1128,6 +1128,14 @@ Deprecated C APIs and :c:func:`PyAsyncGen_New` are deprecated. They are scheduled for removal in 3.18. +* Deprecate :c:func:`PyInitConfig_GetExitCode`, :c:func:`PyStatus_Exit`, + and :c:func:`PyStatus_IsExit` functions, and :c:member:`PyStatus.exitcode` + member. :c:func:`Py_InitializeFromInitConfig` and + :c:func:`Py_InitializeFromConfig` can no longer return an exit code. + Instead, if a command line option wants to exit Python, the option is + processed in :c:func:`Py_RunMain`. + (Contributed by Victor Stinner in :gh:`158080`.) + * :c:func:`PyModule_GetFilename` is no longer deprecated, but using :c:func:`PyModule_GetFilenameObject` instead is still recommended. (Contributed by Victor Stinner in :gh:`154757`.) diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 1ccc496c63ac78..cd4a4c765e9eaf 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -11,19 +11,19 @@ typedef struct { enum { _PyStatus_TYPE_OK=0, _PyStatus_TYPE_ERROR=1, - _PyStatus_TYPE_EXIT=2 + _PyStatus_TYPE_EXIT=2 // deprecated } _type; const char *func; const char *err_msg; - int exitcode; + Py_DEPRECATED(3.16) int exitcode; } PyStatus; PyAPI_FUNC(PyStatus) PyStatus_Ok(void); PyAPI_FUNC(PyStatus) PyStatus_Error(const char *err_msg); PyAPI_FUNC(PyStatus) PyStatus_NoMemory(void); -PyAPI_FUNC(PyStatus) PyStatus_Exit(int exitcode); +Py_DEPRECATED(3.16) PyAPI_FUNC(PyStatus) PyStatus_Exit(int exitcode); PyAPI_FUNC(int) PyStatus_IsError(PyStatus err); -PyAPI_FUNC(int) PyStatus_IsExit(PyStatus err); +Py_DEPRECATED(3.16) PyAPI_FUNC(int) PyStatus_IsExit(PyStatus err); PyAPI_FUNC(int) PyStatus_Exception(PyStatus err); /* --- PyWideStringList ------------------------------------------------ */ @@ -242,6 +242,11 @@ typedef struct PyConfig { // PYTHON_PRESITE=package.module or -X presite=package.module wchar_t *run_presite; #endif + + // If a command line option wants to exit Python, store it in this member + // and only process the option in Py_RunMain() instead of PyConfig_Read(). + // If equals to -1, there is no option. + int _deferred_cmdline_option; } PyConfig; PyAPI_FUNC(void) PyConfig_InitPythonConfig(PyConfig *config); @@ -293,7 +298,7 @@ PyAPI_FUNC(void) PyInitConfig_Free(PyInitConfig *config); PyAPI_FUNC(int) PyInitConfig_GetError(PyInitConfig* config, const char **err_msg); -PyAPI_FUNC(int) PyInitConfig_GetExitCode(PyInitConfig* config, +Py_DEPRECATED(3.16) PyAPI_FUNC(int) PyInitConfig_GetExitCode(PyInitConfig* config, int *exitcode); PyAPI_FUNC(int) PyInitConfig_HasOption(PyInitConfig *config, diff --git a/Include/internal/pycore_initconfig.h b/Include/internal/pycore_initconfig.h index 183b2d45c5ede1..50df2efdd4e526 100644 --- a/Include/internal/pycore_initconfig.h +++ b/Include/internal/pycore_initconfig.h @@ -182,6 +182,8 @@ extern PyObject* _PyConfig_CreateXOptionsDict(const PyConfig *config); extern void _Py_DumpPathConfig(PyThreadState *tstate); +extern int _PyConfig_ProcessDeferredCmdlineOption(PyConfig *config); + /* --- Function used for testing ---------------------------------- */ diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 77b231323cc275..39485afcdb0f57 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -783,6 +783,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'use_frozen_modules': not support.Py_DEBUG, 'safe_path': False, '_is_python_build': IGNORE_CONFIG, + '_deferred_cmdline_option': 0, } if Py_STATS: CONFIG_COMPAT['_pystats'] = False diff --git a/Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst b/Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst new file mode 100644 index 00000000000000..5a5fc2317b67f4 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst @@ -0,0 +1,6 @@ +Deprecate :c:func:`PyInitConfig_GetExitCode`, :c:func:`PyStatus_Exit`, and +:c:func:`PyStatus_IsExit` functions, and :c:member:`PyStatus.exitcode` +member. :c:func:`Py_InitializeFromInitConfig` and +:c:func:`Py_InitializeFromConfig` can no longer return an exit code. +Instead, if a command line option wants to exit Python, the option is +processed in :c:func:`Py_RunMain`. Patch by Victor Stinner. diff --git a/Modules/main.c b/Modules/main.c index ef22331760906c..44a025d500e35e 100644 --- a/Modules/main.c +++ b/Modules/main.c @@ -749,13 +749,21 @@ pymain_set_path0(PyObject *main_importer_path) static void pymain_run_python(int *exitcode) { - int set_running_main = 0; - - PyObject *main_importer_path = NULL; PyInterpreterState *interp = _PyInterpreterState_GET(); /* pymain_repl() and pymain_run_stdin() modify the config */ PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp); + // Process command line options which want to exit Python + int cmdline_exitcode = _PyConfig_ProcessDeferredCmdlineOption(config); + if (cmdline_exitcode >= 0) { + *exitcode = cmdline_exitcode; + return; + } + + int set_running_main = 0; + + PyObject *main_importer_path = NULL; + /* ensure path config is written into global variables */ PyStatus status = _PyPathConfig_UpdateGlobal(config); if (_PyStatus_EXCEPTION(status)) { @@ -910,10 +918,7 @@ static int pymain_main(_PyArgv *args) { PyStatus status = pymain_init(args); - if (_PyStatus_IS_EXIT(status)) { - pymain_free(); - return status.exitcode; - } + assert(!_PyStatus_IS_EXIT(status)); if (_PyStatus_EXCEPTION(status)) { pymain_exit_error(status); } diff --git a/Programs/_bootstrap_python.c b/Programs/_bootstrap_python.c index d30ef8c879d815..c65d616afbd501 100644 --- a/Programs/_bootstrap_python.c +++ b/Programs/_bootstrap_python.c @@ -104,9 +104,6 @@ main(int argc, char **argv) error: PyConfig_Clear(&config); - if (PyStatus_IsExit(status)) { - return status.exitcode; - } Py_ExitStatusException(status); } diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 79e817829c1594..592c23756201f0 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -1722,7 +1722,7 @@ static int test_initconfig_api(void) goto error; } - // Set a list of UTF-8 strings (argv) + // Set a list of UTF-8 strings (xoptions) char* xoptions[] = {"faulthandler"}; if (PyInitConfig_SetStrList(config, "xoptions", Py_ARRAY_LENGTH(xoptions), xoptions) < 0) { @@ -1814,29 +1814,25 @@ static int test_initconfig_get_api(void) static int test_initconfig_exit(void) { + // -h command line option is stored as PyConfig._deferred_cmdline_option PyInitConfig *config = PyInitConfig_Create(); if (config == NULL) { printf("Init allocation error\n"); return 1; } - char *argv[] = {PROGRAM_NAME_UTF8, "--help"}; + char *argv[] = {PROGRAM_NAME_UTF8, "-h"}; assert(PyInitConfig_SetStrList(config, "argv", Py_ARRAY_LENGTH(argv), argv) == 0); - assert(PyInitConfig_SetInt(config, "parse_argv", 1) == 0); - assert(Py_InitializeFromInitConfig(config) < 0); - - int exitcode; - assert(PyInitConfig_GetExitCode(config, &exitcode) == 1); - assert(exitcode == 0); + assert(Py_InitializeFromInitConfig(config) == 0); + PyInitConfig_Free(config); - const char *err_msg; - assert(PyInitConfig_GetError(config, &err_msg) == 1); - assert(strcmp(err_msg, "exit code 0") == 0); + const PyConfig *rt_config = _Py_GetConfig(); + assert(rt_config->_deferred_cmdline_option == 'h'); - PyInitConfig_Free(config); + Py_Finalize(); return 0; } diff --git a/Python/getopt.c b/Python/getopt.c index 79bea2359ffffc..7e918189c716a9 100644 --- a/Python/getopt.c +++ b/Python/getopt.c @@ -41,10 +41,10 @@ static const wchar_t *opt_ptr = L""; static const _PyOS_LongOption longopts[] = { /* name, has_arg, val (used in switch in initconfig.c) */ - {L"check-hash-based-pycs", 1, 0}, - {L"help-all", 0, 1}, - {L"help-env", 0, 2}, - {L"help-xoptions", 0, 3}, + {L"check-hash-based-pycs", 1, 1}, + {L"help-all", 0, 2}, + {L"help-env", 0, 3}, + {L"help-xoptions", 0, 4}, {NULL, 0, -1}, /* sentinel */ }; diff --git a/Python/initconfig.c b/Python/initconfig.c index d683fdd6abc6e1..98f75acc04e996 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -204,6 +204,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = { SPEC(module_search_paths_set, BOOL, INIT_ONLY, NO_SYS), SPEC(pythonpath_env, WSTR_OPT, INIT_ONLY, NO_SYS), SPEC(sys_path_0, WSTR_OPT, INIT_ONLY, NO_SYS), + SPEC(_deferred_cmdline_option, INT, INIT_ONLY, NO_SYS), // Array terminator {NULL, 0, 0, 0, NO_SYS}, @@ -2996,13 +2997,17 @@ static PyStatus config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, Py_ssize_t *opt_index) { + // DEFER_OPTION() only stores the first parsed deferred option +#define DEFER_OPTION(OPTION) \ + do { \ + if (config->_deferred_cmdline_option == 0) { \ + config->_deferred_cmdline_option = (OPTION); \ + } \ + } while (0) + PyStatus status; const PyWideStringList *argv = &config->argv; int print_version = 0; - const wchar_t* program = config->program_name; - if (!program && argv->length >= 1) { - program = argv->items[0]; - } _PyOS_ResetGetOpt(); do { @@ -3045,7 +3050,7 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, switch (c) { // Integers represent long options, see Python/getopt.c - case 0: + case 1: // check-hash-based-pycs if (wcscmp(_PyOS_optarg, L"always") == 0 || wcscmp(_PyOS_optarg, L"never") == 0 @@ -3057,27 +3062,24 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, return status; } } else { - fprintf(stderr, "--check-hash-based-pycs must be one of " - "'default', 'always', or 'never'\n"); - config_usage(1, program); - return _PyStatus_EXIT(2); + DEFER_OPTION(c); } break; - case 1: + case 2: // help-all - config_complete_usage(program); - return _PyStatus_EXIT(0); + DEFER_OPTION(c); + break; - case 2: + case 3: // help-env - config_envvars_usage(); - return _PyStatus_EXIT(0); + DEFER_OPTION(c); + break; - case 3: + case 4: // help-xoptions - config_xoptions_usage(); - return _PyStatus_EXIT(0); + DEFER_OPTION(c); + break; case 'b': config->bytes_warning++; @@ -3136,11 +3138,17 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, case 'h': case '?': - config_usage(0, program); - return _PyStatus_EXIT(0); + DEFER_OPTION(c); + break; case 'V': print_version++; + if (print_version >= 2) { + DEFER_OPTION('W'); + } + else { + DEFER_OPTION(c); + } break; case 'W': @@ -3162,16 +3170,10 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, default: /* unknown argument: parsing failed */ - config_usage(1, program); - return _PyStatus_EXIT(2); + DEFER_OPTION(c); + break; } - } while (1); - - if (print_version) { - printf("Python %s\n", - (print_version >= 2) ? Py_GetVersion() : PY_VERSION); - return _PyStatus_EXIT(0); - } + } while (config->_deferred_cmdline_option == 0); if (config->run_command == NULL && config->run_module == NULL && _PyOS_optind < argv->length @@ -3192,6 +3194,72 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, *opt_index = _PyOS_optind; return _PyStatus_OK(); + +#undef DEFER_OPTION +} + + +int +_PyConfig_ProcessDeferredCmdlineOption(PyConfig *config) +{ + int c = config->_deferred_cmdline_option; + config->_deferred_cmdline_option = 0; + if (c == 0) { + // There is no deferred option + return -1; + } + + // Select the program name + const PyWideStringList *argv = &config->argv; + const wchar_t* program = config->program_name; + if (!program && argv->length >= 1) { + program = argv->items[0]; + } + if (!program) { + program = L"python"; + } + + switch (c) { + case 'h': + case '?': + config_usage(0, program); + return 0; + + case 'V': + printf("Python %s\n", PY_VERSION); + return 0; + + case 'W': // -VV or more -V options + printf("Python %s\n", Py_GetVersion()); + return 0; + + // Integers represent long options, see Python/getopt.c + case 1: + // check-hash-based-pycs + fprintf(stderr, "--check-hash-based-pycs must be one of " + "'default', 'always', or 'never'\n"); + config_usage(1, program); + return 2; + + case 2: + // help-all + config_complete_usage(program); + return 0; + + case 3: + // help-env + config_envvars_usage(); + return 0; + + case 4: + // help-xoptions + config_xoptions_usage(); + return 0; + + default: + config_usage(1, program); + return 2; + } } @@ -3634,7 +3702,6 @@ config_argv0_add_exe(PyConfig *config) * Command line arguments * Environment variables - * Py_xxx global configuration variables The only side effects are to modify config and to call _Py_SetArgcArgv(). */ PyStatus @@ -3966,22 +4033,7 @@ PyInitConfig_Free(PyInitConfig *config) int PyInitConfig_GetError(PyInitConfig* config, const char **perr_msg) { - if (_PyStatus_IS_EXIT(config->status)) { - char buffer[22]; // len("exit code -2147483648\0") - PyOS_snprintf(buffer, sizeof(buffer), - "exit code %i", - config->status.exitcode); - - if (config->err_msg != NULL) { - free(config->err_msg); - } - config->err_msg = strdup(buffer); - if (config->err_msg != NULL) { - *perr_msg = config->err_msg; - return 1; - } - config->status = _PyStatus_NO_MEMORY(); - } + assert(!_PyStatus_IS_EXIT(config->status)); if (_PyStatus_IS_ERROR(config->status) && config->status.err_msg != NULL) { *perr_msg = config->status.err_msg; @@ -3997,13 +4049,8 @@ PyInitConfig_GetError(PyInitConfig* config, const char **perr_msg) int PyInitConfig_GetExitCode(PyInitConfig* config, int *exitcode) { - if (_PyStatus_IS_EXIT(config->status)) { - *exitcode = config->status.exitcode; - return 1; - } - else { - return 0; - } + assert(!_PyStatus_IS_EXIT(config->status)); + return 0; } diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index a9c98d73fd0f72..8888f523559123 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3830,10 +3830,9 @@ _Py_FatalRefcountErrorFunc(const char *func, const char *msg) void _Py_NO_RETURN Py_ExitStatusException(PyStatus status) { - if (_PyStatus_IS_EXIT(status)) { - exit(status.exitcode); - } - else if (_PyStatus_IS_ERROR(status)) { + assert(!_PyStatus_IS_EXIT(status)); + + if (_PyStatus_IS_ERROR(status)) { fatal_error(fileno(stderr), 1, status.func, status.err_msg, 1); } else {