Skip to content

Commit 5ff278c

Browse files
committed
gh-142417: Add PyInitConfig_SetInitCallback() function
Add init_callback and init_callback_arg members to PyConfig.
1 parent 0b8c348 commit 5ff278c

File tree

8 files changed

+169
-12
lines changed

8 files changed

+169
-12
lines changed

Doc/c-api/init_config.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,26 @@ Module
238238
Similar to the :c:func:`PyImport_AppendInittab` function.
239239
240240
241+
Initialization Callback
242+
-----------------------
243+
244+
.. c:function:: int PyInitConfig_SetInitCallback(PyInitConfig *config, PyStatus (*callback)(void *arg), void *arg)
245+
246+
Set an initialization callback. It allows executing code as soon as the
247+
Python interpreter is initialized, before the first import. For example, it
248+
can be used to add a meta path importer into :data:`sys.meta_path`.
249+
250+
Python is not fully initialized yet when the callback is called. For
251+
example, :data:`sys.stdout` may not exist yet.
252+
253+
A single callback can be registered. If this function is called more than
254+
once, the previous callback is overridden.
255+
256+
* Return ``0`` on success.
257+
* Set an error in *config* and return ``-1`` on error.
258+
259+
.. versionadded:: next
260+
241261
Initialize Python
242262
-----------------
243263

Doc/whatsnew/3.15.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,11 @@ New features
10791079
a module from a *spec* and *initfunc*.
10801080
(Contributed by Itamar Oren in :gh:`116146`.)
10811081

1082+
* Add :c:func:`PyInitConfig_SetInitCallback` to execute code as soon as the
1083+
Python interpreter is initialized, before the first import. For example, it
1084+
can be used to add a meta path importer into :data:`sys.meta_path`.
1085+
(Contributed by Victor Stinner in :gh:`142417`.)
1086+
10821087
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
10831088
(Contributed by Victor Stinner in :gh:`111489`.)
10841089

Include/cpython/initconfig.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ typedef struct PyConfig {
215215
wchar_t *run_module;
216216
wchar_t *run_filename;
217217

218+
/* --- Initialization callback ------------------- */
219+
220+
// See PyInitConfig_SetInitCallback() function.
221+
PyStatus (*init_callback)(void *arg);
222+
void *init_callback_arg;
223+
218224
/* --- Set by Py_Main() -------------------------- */
219225
wchar_t *sys_path_0;
220226

@@ -323,6 +329,10 @@ PyAPI_FUNC(int) PyInitConfig_AddModule(PyInitConfig *config,
323329
const char *name,
324330
PyObject* (*initfunc)(void));
325331

332+
PyAPI_FUNC(int) PyInitConfig_SetInitCallback(PyInitConfig *config,
333+
PyStatus (*callback)(void *arg),
334+
void *arg);
335+
326336
PyAPI_FUNC(int) Py_InitializeFromInitConfig(PyInitConfig *config);
327337

328338

Lib/test/test_embed.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,6 +1885,20 @@ def test_init_in_background_thread(self):
18851885
out, err = self.run_embedded_interpreter("test_init_in_background_thread")
18861886
self.assertEqual(err, "")
18871887

1888+
def test_init_callback(self):
1889+
out, err = self.run_embedded_interpreter("test_init_callback")
1890+
modules = [
1891+
'_frozen_importlib', '_imp', '_thread', '_warnings', '_weakref',
1892+
'builtins', 'sys']
1893+
meta_path = (
1894+
"[<class '_frozen_importlib.BuiltinImporter'>, "
1895+
"<class '_frozen_importlib.FrozenImporter'>]")
1896+
self.assertEqual(err.splitlines(),
1897+
["Hello Callback!",
1898+
f"sys.modules: {modules}",
1899+
f"sys.meta_path: {meta_path}"])
1900+
self.assertEqual(out, "")
1901+
18881902

18891903
class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
18901904
def test_open_code_hook(self):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add :c:func:`PyInitConfig_SetInitCallback` to execute code as soon as the
2+
Python interpreter is initialized, before the first import. For example, it can
3+
be used to add a meta path importer into :data:`sys.meta_path`. Patch by Victor
4+
Stinner.

Programs/_testembed.c

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,6 +1741,16 @@ static int initconfig_getint(PyInitConfig *config, const char *name)
17411741
return (int)value;
17421742
}
17431743

1744+
static void initconfig_error(PyInitConfig *config)
1745+
{
1746+
const char *err_msg;
1747+
int res = PyInitConfig_GetError(config, &err_msg);
1748+
assert(res == 1);
1749+
1750+
printf("Python init failed: %s\n", err_msg);
1751+
PyInitConfig_Free(config);
1752+
}
1753+
17441754

17451755
static int test_initconfig_api(void)
17461756
{
@@ -1795,12 +1805,8 @@ static int test_initconfig_api(void)
17951805
return 0;
17961806

17971807
error:
1798-
{
1799-
const char *err_msg;
1800-
(void)PyInitConfig_GetError(config, &err_msg);
1801-
printf("Python init failed: %s\n", err_msg);
1802-
exit(1);
1803-
}
1808+
initconfig_error(config);
1809+
return 1;
18041810
}
18051811

18061812

@@ -1953,12 +1959,8 @@ static int test_initconfig_module(void)
19531959
return 0;
19541960

19551961
error:
1956-
{
1957-
const char *err_msg;
1958-
(void)PyInitConfig_GetError(config, &err_msg);
1959-
printf("Python init failed: %s\n", err_msg);
1960-
exit(1);
1961-
}
1962+
initconfig_error(config);
1963+
return 1;
19621964
}
19631965

19641966

@@ -2170,6 +2172,86 @@ static int test_init_in_background_thread(void)
21702172
}
21712173

21722174

2175+
static PyStatus init_callback(void *arg)
2176+
{
2177+
const char *msg = (const char*)arg;
2178+
fprintf(stderr, "%s\n", msg);
2179+
2180+
// Write sorted(sys.modules) to sys.stderr
2181+
PyObject *modules = PySys_GetAttrString("modules");
2182+
if (modules == NULL) {
2183+
return PyStatus_Error("failed to get sys.modules");
2184+
}
2185+
2186+
PyObject *builtins = PyEval_GetBuiltins(); // borrowed ref
2187+
if (builtins == NULL) {
2188+
Py_DECREF(modules);
2189+
return PyStatus_Error("failed to get builtins");
2190+
}
2191+
2192+
PyObject *sorted;
2193+
if (PyDict_GetItemStringRef(builtins, "sorted", &sorted) <= 0) {
2194+
Py_DECREF(modules);
2195+
return PyStatus_Error("failed to get sorted");
2196+
}
2197+
2198+
PyObject *names = PyObject_CallOneArg(sorted, modules);
2199+
Py_DECREF(modules);
2200+
if (names == NULL) {
2201+
return PyStatus_Error("sorted failed");
2202+
}
2203+
2204+
PySys_FormatStderr("sys.modules: %R\n", names);
2205+
Py_DECREF(names);
2206+
2207+
// Write sys.meta_path to sys.stderr
2208+
const char *code = (
2209+
"import sys; "
2210+
"print(f\"sys.meta_path: {sys.meta_path}\", file=sys.stderr)");
2211+
if (PyRun_SimpleString(code) < 0) {
2212+
return PyStatus_Error("PyRun_SimpleString failed");
2213+
}
2214+
2215+
return PyStatus_Ok();
2216+
}
2217+
2218+
2219+
static int test_init_callback(void)
2220+
{
2221+
PyInitConfig *config = PyInitConfig_Create();
2222+
if (config == NULL) {
2223+
printf("Init allocation error\n");
2224+
return 1;
2225+
}
2226+
2227+
if (PyInitConfig_SetStr(config, "program_name", PROGRAM_NAME_UTF8) < 0) {
2228+
goto error;
2229+
}
2230+
2231+
const char *ignored_msg = "ignored_msg";
2232+
if (PyInitConfig_SetInitCallback(config, init_callback, (void*)ignored_msg) < 0) {
2233+
goto error;
2234+
}
2235+
2236+
// PyInitConfig_SetInitCallback() can be called more than once, but the
2237+
// previous callback and callback argument are overridden.
2238+
const char *msg = "Hello Callback!";
2239+
if (PyInitConfig_SetInitCallback(config, init_callback, (void*)msg) < 0) {
2240+
goto error;
2241+
}
2242+
2243+
if (Py_InitializeFromInitConfig(config) < 0) {
2244+
goto error;
2245+
}
2246+
PyInitConfig_Free(config);
2247+
return 0;
2248+
2249+
error:
2250+
initconfig_error(config);
2251+
return 1;
2252+
}
2253+
2254+
21732255
#ifndef MS_WINDOWS
21742256
#include "test_frozenmain.h" // M_test_frozenmain
21752257

@@ -2658,6 +2740,7 @@ static struct TestCase TestCases[] = {
26582740
{"test_init_use_frozen_modules", test_init_use_frozen_modules},
26592741
{"test_init_main_interpreter_settings", test_init_main_interpreter_settings},
26602742
{"test_init_in_background_thread", test_init_in_background_thread},
2743+
{"test_init_callback", test_init_callback},
26612744

26622745
// Audit
26632746
{"test_open_code_hook", test_open_code_hook},

Python/initconfig.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,10 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
12741274
Py_UNREACHABLE();
12751275
}
12761276
}
1277+
1278+
config->init_callback = config2->init_callback;
1279+
config->init_callback_arg = config2->init_callback_arg;
1280+
12771281
return _PyStatus_OK();
12781282
}
12791283

@@ -4240,6 +4244,16 @@ Py_InitializeFromInitConfig(PyInitConfig *config)
42404244
}
42414245

42424246

4247+
int
4248+
PyInitConfig_SetInitCallback(PyInitConfig *config,
4249+
PyStatus (*callback)(void *arg), void *arg)
4250+
{
4251+
config->config.init_callback = callback;
4252+
config->config.init_callback_arg = arg;
4253+
return 0;
4254+
}
4255+
4256+
42434257
// --- PyConfig_Get() -------------------------------------------------------
42444258

42454259
static const PyConfigSpec*

Python/pylifecycle.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,13 @@ Py_InitializeFromConfig(const PyConfig *config)
14571457
}
14581458
config = _PyInterpreterState_GetConfig(tstate->interp);
14591459

1460+
if (config->init_callback != NULL) {
1461+
status = config->init_callback(config->init_callback_arg);
1462+
if (_PyStatus_EXCEPTION(status)) {
1463+
return status;
1464+
}
1465+
}
1466+
14601467
if (config->_init_main) {
14611468
status = pyinit_main(tstate);
14621469
if (_PyStatus_EXCEPTION(status)) {

0 commit comments

Comments
 (0)