diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index c345029e4acd49..7d6e50f959c434 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -238,6 +238,43 @@ Module Similar to the :c:func:`PyImport_AppendInittab` function. +Initialization Callback +----------------------- + +.. c:function:: int PyInitConfig_SetInitCallback(PyInitConfig *config, PyStatus (*callback)(void *arg), void *arg) + + Set an initialization callback. It allows executing code as soon as the + Python interpreter is initialized, before the first import. For example, it + can be used to add a meta path importer into :data:`sys.meta_path`. + + When the callback is called, Python is only partially initialized. What's + available at this point: + + * Builtin types; + * Builtin exceptions; + * Builtin and frozen modules (can be imported); + * The :mod:`sys` module is only partially initialized + (ex: :data:`sys.path` and :data:`sys.stdout` don't exist yet). + + After the callback, the Python initialization is completed: + + * Install and configure :mod:`importlib`; + * Apply the :ref:`Path Configuration `; + * Install signal handlers; + * Finish :mod:`sys` module initialization (ex: create :data:`sys.stdout` + and :data:`sys.path`); + * Enable optional features like :mod:`faulthandler` and :mod:`tracemalloc`; + * Import the :mod:`site` module; + * etc. + + A single callback can be registered. If this function is called more than + once, the previous callback is overridden. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + .. versionadded:: next + Initialize Python ----------------- diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 1bd82545e588fa..6184ef496e115b 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1079,6 +1079,11 @@ New features a module from a *spec* and *initfunc*. (Contributed by Itamar Oren in :gh:`116146`.) +* Add :c:func:`PyInitConfig_SetInitCallback` to execute code as soon as the + Python interpreter is initialized, before the first import. For example, it + can be used to add a meta path importer into :data:`sys.meta_path`. + (Contributed by Victor Stinner in :gh:`142417`.) + * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. (Contributed by Victor Stinner in :gh:`111489`.) diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 1c979d91a40850..cc38c938edaf0a 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -215,6 +215,12 @@ typedef struct PyConfig { wchar_t *run_module; wchar_t *run_filename; + /* --- Initialization callback ------------------- */ + + // See PyInitConfig_SetInitCallback() function. + PyStatus (*init_callback)(void *arg); + void *init_callback_arg; + /* --- Set by Py_Main() -------------------------- */ wchar_t *sys_path_0; @@ -323,6 +329,10 @@ PyAPI_FUNC(int) PyInitConfig_AddModule(PyInitConfig *config, const char *name, PyObject* (*initfunc)(void)); +PyAPI_FUNC(int) PyInitConfig_SetInitCallback(PyInitConfig *config, + PyStatus (*callback)(void *arg), + void *arg); + PyAPI_FUNC(int) Py_InitializeFromInitConfig(PyInitConfig *config); diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index b536794122787d..7137c9c666a005 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1885,6 +1885,20 @@ def test_init_in_background_thread(self): out, err = self.run_embedded_interpreter("test_init_in_background_thread") self.assertEqual(err, "") + def test_init_callback(self): + out, err = self.run_embedded_interpreter("test_init_callback") + modules = [ + '_frozen_importlib', '_imp', '_thread', '_warnings', '_weakref', + 'builtins', 'sys'] + meta_path = ( + "[, " + "]") + self.assertEqual(err.splitlines(), + ["Hello Callback!", + f"sys.modules: {modules}", + f"sys.meta_path: {meta_path}"]) + self.assertEqual(out, "") + class AuditingTests(EmbeddingTestsMixin, unittest.TestCase): def test_open_code_hook(self): diff --git a/Misc/NEWS.d/next/C_API/2025-12-08-16-12-34.gh-issue-142417.tYamiX.rst b/Misc/NEWS.d/next/C_API/2025-12-08-16-12-34.gh-issue-142417.tYamiX.rst new file mode 100644 index 00000000000000..0c7ff2a88bfeb1 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-12-08-16-12-34.gh-issue-142417.tYamiX.rst @@ -0,0 +1,4 @@ +Add :c:func:`PyInitConfig_SetInitCallback` to execute code as soon as the +Python interpreter is initialized, before the first import. For example, it can +be used to add a meta path importer into :data:`sys.meta_path`. Patch by Victor +Stinner. diff --git a/Programs/_testembed.c b/Programs/_testembed.c index c6a18249e3ccdd..4ae3b6f583af32 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -1741,6 +1741,16 @@ static int initconfig_getint(PyInitConfig *config, const char *name) return (int)value; } +static void initconfig_error(PyInitConfig *config) +{ + const char *err_msg; + int res = PyInitConfig_GetError(config, &err_msg); + assert(res == 1); + + printf("Python init failed: %s\n", err_msg); + PyInitConfig_Free(config); +} + static int test_initconfig_api(void) { @@ -1795,12 +1805,8 @@ static int test_initconfig_api(void) return 0; error: - { - const char *err_msg; - (void)PyInitConfig_GetError(config, &err_msg); - printf("Python init failed: %s\n", err_msg); - exit(1); - } + initconfig_error(config); + return 1; } @@ -1953,12 +1959,8 @@ static int test_initconfig_module(void) return 0; error: - { - const char *err_msg; - (void)PyInitConfig_GetError(config, &err_msg); - printf("Python init failed: %s\n", err_msg); - exit(1); - } + initconfig_error(config); + return 1; } @@ -2170,6 +2172,86 @@ static int test_init_in_background_thread(void) } +static PyStatus init_callback(void *arg) +{ + const char *msg = (const char*)arg; + fprintf(stderr, "%s\n", msg); + + // Write sorted(sys.modules) to sys.stderr + PyObject *modules = PySys_GetAttrString("modules"); + if (modules == NULL) { + return PyStatus_Error("failed to get sys.modules"); + } + + PyObject *builtins = PyEval_GetBuiltins(); // borrowed ref + if (builtins == NULL) { + Py_DECREF(modules); + return PyStatus_Error("failed to get builtins"); + } + + PyObject *sorted; + if (PyDict_GetItemStringRef(builtins, "sorted", &sorted) <= 0) { + Py_DECREF(modules); + return PyStatus_Error("failed to get sorted"); + } + + PyObject *names = PyObject_CallOneArg(sorted, modules); + Py_DECREF(modules); + if (names == NULL) { + return PyStatus_Error("sorted failed"); + } + + PySys_FormatStderr("sys.modules: %R\n", names); + Py_DECREF(names); + + // Write sys.meta_path to sys.stderr + const char *code = ( + "import sys; " + "print(f\"sys.meta_path: {sys.meta_path}\", file=sys.stderr)"); + if (PyRun_SimpleString(code) < 0) { + return PyStatus_Error("PyRun_SimpleString failed"); + } + + return PyStatus_Ok(); +} + + +static int test_init_callback(void) +{ + PyInitConfig *config = PyInitConfig_Create(); + if (config == NULL) { + printf("Init allocation error\n"); + return 1; + } + + if (PyInitConfig_SetStr(config, "program_name", PROGRAM_NAME_UTF8) < 0) { + goto error; + } + + const char *ignored_msg = "ignored_msg"; + if (PyInitConfig_SetInitCallback(config, init_callback, (void*)ignored_msg) < 0) { + goto error; + } + + // PyInitConfig_SetInitCallback() can be called more than once, but the + // previous callback and callback argument are overridden. + const char *msg = "Hello Callback!"; + if (PyInitConfig_SetInitCallback(config, init_callback, (void*)msg) < 0) { + goto error; + } + + if (Py_InitializeFromInitConfig(config) < 0) { + goto error; + } + PyInitConfig_Free(config); + return 0; + +error: + initconfig_error(config); + return 1; +} + + #ifndef MS_WINDOWS #include "test_frozenmain.h" // M_test_frozenmain @@ -2658,6 +2740,7 @@ static struct TestCase TestCases[] = { {"test_init_use_frozen_modules", test_init_use_frozen_modules}, {"test_init_main_interpreter_settings", test_init_main_interpreter_settings}, {"test_init_in_background_thread", test_init_in_background_thread}, + {"test_init_callback", test_init_callback}, // Audit {"test_open_code_hook", test_open_code_hook}, diff --git a/Python/initconfig.c b/Python/initconfig.c index 7176670c110d69..ff938c24cc1d34 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -1274,6 +1274,10 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2) Py_UNREACHABLE(); } } + + config->init_callback = config2->init_callback; + config->init_callback_arg = config2->init_callback_arg; + return _PyStatus_OK(); } @@ -4240,6 +4244,16 @@ Py_InitializeFromInitConfig(PyInitConfig *config) } +int +PyInitConfig_SetInitCallback(PyInitConfig *config, + PyStatus (*callback)(void *arg), void *arg) +{ + config->config.init_callback = callback; + config->config.init_callback_arg = arg; + return 0; +} + + // --- PyConfig_Get() ------------------------------------------------------- static const PyConfigSpec* diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 2527dca71d774e..9b6fa7dd39c93d 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -1457,6 +1457,13 @@ Py_InitializeFromConfig(const PyConfig *config) } config = _PyInterpreterState_GetConfig(tstate->interp); + if (config->init_callback != NULL) { + status = config->init_callback(config->init_callback_arg); + if (_PyStatus_EXCEPTION(status)) { + return status; + } + } + if (config->_init_main) { status = pyinit_main(tstate); if (_PyStatus_EXCEPTION(status)) {