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
37 changes: 37 additions & 0 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init-path-config>`;
* 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
-----------------

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.)

Expand Down
10 changes: 10 additions & 0 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);


Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
"[<class '_frozen_importlib.BuiltinImporter'>, "
"<class '_frozen_importlib.FrozenImporter'>]")
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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
107 changes: 95 additions & 12 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
}


Expand Down Expand Up @@ -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;
}


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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},
Expand Down
14 changes: 14 additions & 0 deletions Python/initconfig.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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*
Expand Down
7 changes: 7 additions & 0 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading