Skip to content

Commit da936a6

Browse files
Keep a cache of loaded module namespaces.
1 parent ccc135c commit da936a6

File tree

2 files changed

+95
-13
lines changed

2 files changed

+95
-13
lines changed

Lib/test/test_concurrent_futures/test_interpreter_pool.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
)
1111
import _interpreters
1212
from test import support
13+
from test.support import os_helper
14+
from test.support import script_helper
1315
import test.test_asyncio.utils as testasyncio_utils
1416
from test.support.interpreters import queues
1517

@@ -155,6 +157,38 @@ def test_init_func(self):
155157
self.assertEqual(before, b'\0')
156158
self.assertEqual(after, msg)
157159

160+
def test_init_with___main___global(self):
161+
# See https://github.com/python/cpython/pull/133957#issuecomment-2927415311.
162+
text = """if True:
163+
from concurrent.futures import InterpreterPoolExecutor
164+
165+
INITIALIZER_STATUS = 'uninitialized'
166+
167+
def init(x):
168+
global INITIALIZER_STATUS
169+
INITIALIZER_STATUS = x
170+
INITIALIZER_STATUS
171+
172+
def get_init_status():
173+
return INITIALIZER_STATUS
174+
175+
if __name__ == "__main__":
176+
exe = InterpreterPoolExecutor(initializer=init,
177+
initargs=('initialized',))
178+
fut = exe.submit(get_init_status)
179+
print(fut.result()) # 'initialized'
180+
exe.shutdown(wait=True)
181+
print(INITIALIZER_STATUS) # 'uninitialized'
182+
"""
183+
with os_helper.temp_dir() as tempdir:
184+
filename = script_helper.make_script(tempdir, 'my-script', text)
185+
res = script_helper.assert_python_ok(filename)
186+
stdout = res.out.decode('utf-8').strip()
187+
self.assertEqual(stdout.splitlines(), [
188+
'initialized',
189+
'uninitialized',
190+
])
191+
158192
def test_init_closure(self):
159193
count = 0
160194
def init1():

Python/crossinterp.c

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,50 @@ sync_module_clear(struct sync_module *data)
540540
}
541541

542542

543+
static PyObject *
544+
get_cached_module_ns(PyThreadState *tstate,
545+
const char *modname, const char *filename)
546+
{
547+
// Load the module from the original file.
548+
assert(filename != NULL);
549+
PyObject *loaded = NULL;
550+
551+
const char *run_modname = modname;
552+
if (strcmp(modname, "__main__") == 0) {
553+
// We don't want to trigger "if __name__ == '__main__':".
554+
run_modname = "<fake __main__>";
555+
}
556+
557+
// First try the per-interpreter cache.
558+
PyObject *interpns = PyInterpreterState_GetDict(tstate->interp);
559+
assert(interpns != NULL);
560+
PyObject *key = PyUnicode_FromFormat("CACHED_MODULE_NS_%s", modname);
561+
if (key == NULL) {
562+
return NULL;
563+
}
564+
if (PyDict_GetItemRef(interpns, key, &loaded) < 0) {
565+
goto finally;
566+
}
567+
if (loaded != NULL) {
568+
goto finally;
569+
}
570+
571+
// It wasn't already loaded from file.
572+
loaded = runpy_run_path(filename, run_modname);
573+
if (loaded == NULL) {
574+
goto finally;
575+
}
576+
if (PyDict_SetItem(interpns, key, loaded) < 0) {
577+
Py_CLEAR(loaded);
578+
goto finally;
579+
}
580+
581+
finally:
582+
Py_DECREF(key);
583+
return loaded;
584+
}
585+
586+
543587
struct _unpickle_context {
544588
PyThreadState *tstate;
545589
// We only special-case the __main__ module,
@@ -574,37 +618,40 @@ _unpickle_context_set_module(struct _unpickle_context *ctx,
574618
struct sync_module_result res = {0};
575619
struct sync_module_result *cached = NULL;
576620
const char *filename = NULL;
577-
const char *run_modname = modname;
578621
if (strcmp(modname, "__main__") == 0) {
579622
cached = &ctx->main.cached;
580623
filename = ctx->main.filename;
581-
// We don't want to trigger "if __name__ == '__main__':".
582-
run_modname = "<fake __main__>";
583624
}
584625
else {
585626
res.failed = PyExc_NotImplementedError;
586-
goto finally;
627+
goto error;
587628
}
588629

589630
res.module = import_get_module(ctx->tstate, modname);
590631
if (res.module == NULL) {
591-
res.failed = _PyErr_GetRaisedException(ctx->tstate);
592-
assert(res.failed != NULL);
593-
goto finally;
632+
goto error;
594633
}
595634

635+
// Load the module ns from the original file and cache it.
636+
// Note that functions will use the cached ns for __globals__,
637+
// not res.module.
596638
if (filename == NULL) {
597-
Py_CLEAR(res.module);
598639
res.failed = PyExc_NotImplementedError;
599-
goto finally;
640+
goto error;
600641
}
601-
res.loaded = runpy_run_path(filename, run_modname);
642+
res.loaded = get_cached_module_ns(ctx->tstate, modname, filename);
602643
if (res.loaded == NULL) {
603-
Py_CLEAR(res.module);
644+
goto error;
645+
}
646+
goto finally;
647+
648+
error:
649+
Py_CLEAR(res.module);
650+
if (res.failed == NULL) {
604651
res.failed = _PyErr_GetRaisedException(ctx->tstate);
605652
assert(res.failed != NULL);
606-
goto finally;
607653
}
654+
assert(!_PyErr_Occurred(ctx->tstate));
608655

609656
finally:
610657
if (cached != NULL) {
@@ -629,7 +676,8 @@ _handle_unpickle_missing_attr(struct _unpickle_context *ctx, PyObject *exc)
629676
}
630677

631678
// Get the module.
632-
struct sync_module_result mod = _unpickle_context_get_module(ctx, info.modname);
679+
struct sync_module_result mod =
680+
_unpickle_context_get_module(ctx, info.modname);
633681
if (mod.failed != NULL) {
634682
// It must have failed previously.
635683
return -1;

0 commit comments

Comments
 (0)