Skip to content

Commit 3135544

Browse files
[3.12] gh-109853: Fix sys.path[0] For Subinterpreters (gh-109994) (gh-110701)
This change makes sure sys.path[0] is set properly for subinterpreters. Before, it wasn't getting set at all. This change does not address the broader concerns from gh-109853. (cherry-picked from commit a040a32)
1 parent 592a849 commit 3135544

File tree

7 files changed

+26702
-26487
lines changed

7 files changed

+26702
-26487
lines changed

Doc/data/python3.12.abi

Lines changed: 26489 additions & 26477 deletions
Large diffs are not rendered by default.

Include/internal/pycore_runtime.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ typedef struct pyruntimestate {
162162
/* All the objects that are shared by the runtime's interpreters. */
163163
struct _Py_static_objects static_objects;
164164

165+
/* The value to use for sys.path[0] in new subinterpreters.
166+
Normally this would be part of the PyConfig struct. However,
167+
we cannot add it there in 3.12 since that's an ABI change. */
168+
wchar_t *sys_path_0;
169+
165170
/* The following fields are here to avoid allocation during init.
166171
The data is exposed through _PyRuntimeState pointer fields.
167172
These fields should not be accessed directly outside of init.

Lib/test/test_interpreters.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import contextlib
2+
import json
23
import os
4+
import os.path
5+
import sys
36
import threading
47
from textwrap import dedent
58
import unittest
@@ -8,6 +11,7 @@
811
from test import support
912
from test.support import import_helper
1013
from test.support import threading_helper
14+
from test.support import os_helper
1115
_interpreters = import_helper.import_module('_xxsubinterpreters')
1216
_channels = import_helper.import_module('_xxinterpchannels')
1317
from test.support import interpreters
@@ -487,6 +491,153 @@ def task():
487491
pass
488492

489493

494+
class StartupTests(TestBase):
495+
496+
# We want to ensure the initial state of subinterpreters
497+
# matches expectations.
498+
499+
_subtest_count = 0
500+
501+
@contextlib.contextmanager
502+
def subTest(self, *args):
503+
with super().subTest(*args) as ctx:
504+
self._subtest_count += 1
505+
try:
506+
yield ctx
507+
finally:
508+
if self._debugged_in_subtest:
509+
if self._subtest_count == 1:
510+
# The first subtest adds a leading newline, so we
511+
# compensate here by not printing a trailing newline.
512+
print('### end subtest debug ###', end='')
513+
else:
514+
print('### end subtest debug ###')
515+
self._debugged_in_subtest = False
516+
517+
def debug(self, msg, *, header=None):
518+
if header:
519+
self._debug(f'--- {header} ---')
520+
if msg:
521+
if msg.endswith(os.linesep):
522+
self._debug(msg[:-len(os.linesep)])
523+
else:
524+
self._debug(msg)
525+
self._debug('<no newline>')
526+
self._debug('------')
527+
else:
528+
self._debug(msg)
529+
530+
_debugged = False
531+
_debugged_in_subtest = False
532+
def _debug(self, msg):
533+
if not self._debugged:
534+
print()
535+
self._debugged = True
536+
if self._subtest is not None:
537+
if True:
538+
if not self._debugged_in_subtest:
539+
self._debugged_in_subtest = True
540+
print('### start subtest debug ###')
541+
print(msg)
542+
else:
543+
print(msg)
544+
545+
def create_temp_dir(self):
546+
import tempfile
547+
tmp = tempfile.mkdtemp(prefix='test_interpreters_')
548+
tmp = os.path.realpath(tmp)
549+
self.addCleanup(os_helper.rmtree, tmp)
550+
return tmp
551+
552+
def write_script(self, *path, text):
553+
filename = os.path.join(*path)
554+
dirname = os.path.dirname(filename)
555+
if dirname:
556+
os.makedirs(dirname, exist_ok=True)
557+
with open(filename, 'w', encoding='utf-8') as outfile:
558+
outfile.write(dedent(text))
559+
return filename
560+
561+
@support.requires_subprocess()
562+
def run_python(self, argv, *, cwd=None):
563+
# This method is inspired by
564+
# EmbeddingTestsMixin.run_embedded_interpreter() in test_embed.py.
565+
import shlex
566+
import subprocess
567+
if isinstance(argv, str):
568+
argv = shlex.split(argv)
569+
argv = [sys.executable, *argv]
570+
try:
571+
proc = subprocess.run(
572+
argv,
573+
cwd=cwd,
574+
capture_output=True,
575+
text=True,
576+
)
577+
except Exception as exc:
578+
self.debug(f'# cmd: {shlex.join(argv)}')
579+
if isinstance(exc, FileNotFoundError) and not exc.filename:
580+
if os.path.exists(argv[0]):
581+
exists = 'exists'
582+
else:
583+
exists = 'does not exist'
584+
self.debug(f'{argv[0]} {exists}')
585+
raise # re-raise
586+
assert proc.stderr == '' or proc.returncode != 0, proc.stderr
587+
if proc.returncode != 0 and support.verbose:
588+
self.debug(f'# python3 {shlex.join(argv[1:])} failed:')
589+
self.debug(proc.stdout, header='stdout')
590+
self.debug(proc.stderr, header='stderr')
591+
self.assertEqual(proc.returncode, 0)
592+
self.assertEqual(proc.stderr, '')
593+
return proc.stdout
594+
595+
def test_sys_path_0(self):
596+
# The main interpreter's sys.path[0] should be used by subinterpreters.
597+
script = '''
598+
import sys
599+
from test.support import interpreters
600+
601+
orig = sys.path[0]
602+
603+
interp = interpreters.create()
604+
interp.run(f"""if True:
605+
import json
606+
import sys
607+
print(json.dumps({{
608+
'main': {orig!r},
609+
'sub': sys.path[0],
610+
}}, indent=4), flush=True)
611+
""")
612+
'''
613+
# <tmp>/
614+
# pkg/
615+
# __init__.py
616+
# __main__.py
617+
# script.py
618+
# script.py
619+
cwd = self.create_temp_dir()
620+
self.write_script(cwd, 'pkg', '__init__.py', text='')
621+
self.write_script(cwd, 'pkg', '__main__.py', text=script)
622+
self.write_script(cwd, 'pkg', 'script.py', text=script)
623+
self.write_script(cwd, 'script.py', text=script)
624+
625+
cases = [
626+
('script.py', cwd),
627+
('-m script', cwd),
628+
('-m pkg', cwd),
629+
('-m pkg.script', cwd),
630+
('-c "import script"', ''),
631+
]
632+
for argv, expected in cases:
633+
with self.subTest(f'python3 {argv}'):
634+
out = self.run_python(argv, cwd=cwd)
635+
data = json.loads(out)
636+
sp0_main, sp0_sub = data['main'], data['sub']
637+
self.assertEqual(sp0_sub, sp0_main)
638+
self.assertEqual(sp0_sub, expected)
639+
640+
490641
class TestIsShareable(TestBase):
491642

492643
def test_default_shareables(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``sys.path[0]`` is now set correctly for subinterpreters.

Modules/main.c

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,8 @@ pymain_run_python(int *exitcode)
559559
goto error;
560560
}
561561

562+
assert(interp->runtime->sys_path_0 == NULL);
563+
562564
if (config->run_filename != NULL) {
563565
/* If filename is a package (ex: directory or ZIP file) which contains
564566
__main__.py, main_importer_path is set to filename and will be
@@ -574,24 +576,38 @@ pymain_run_python(int *exitcode)
574576
// import readline and rlcompleter before script dir is added to sys.path
575577
pymain_import_readline(config);
576578

579+
PyObject *path0 = NULL;
577580
if (main_importer_path != NULL) {
578-
if (pymain_sys_path_add_path0(interp, main_importer_path) < 0) {
579-
goto error;
580-
}
581+
path0 = Py_NewRef(main_importer_path);
581582
}
582583
else if (!config->safe_path) {
583-
PyObject *path0 = NULL;
584584
int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0);
585585
if (res < 0) {
586586
goto error;
587587
}
588-
589-
if (res > 0) {
590-
if (pymain_sys_path_add_path0(interp, path0) < 0) {
591-
Py_DECREF(path0);
592-
goto error;
593-
}
588+
else if (res == 0) {
589+
Py_CLEAR(path0);
590+
}
591+
}
592+
if (path0 != NULL) {
593+
wchar_t *wstr = PyUnicode_AsWideCharString(path0, NULL);
594+
if (wstr == NULL) {
594595
Py_DECREF(path0);
596+
goto error;
597+
}
598+
PyMemAllocatorEx old_alloc;
599+
_PyMem_SetDefaultAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
600+
interp->runtime->sys_path_0 = _PyMem_RawWcsdup(wstr);
601+
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
602+
PyMem_Free(wstr);
603+
if (interp->runtime->sys_path_0 == NULL) {
604+
Py_DECREF(path0);
605+
goto error;
606+
}
607+
int res = pymain_sys_path_add_path0(interp, path0);
608+
Py_DECREF(path0);
609+
if (res < 0) {
610+
goto error;
595611
}
596612
}
597613

Python/pylifecycle.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,32 @@ init_interp_main(PyThreadState *tstate)
12001200
#endif
12011201
}
12021202

1203+
if (!is_main_interp) {
1204+
// The main interpreter is handled in Py_Main(), for now.
1205+
wchar_t *sys_path_0 = interp->runtime->sys_path_0;
1206+
if (sys_path_0 != NULL) {
1207+
PyObject *path0 = PyUnicode_FromWideChar(sys_path_0, -1);
1208+
if (path0 == NULL) {
1209+
return _PyStatus_ERR("can't initialize sys.path[0]");
1210+
}
1211+
PyObject *sysdict = interp->sysdict;
1212+
if (sysdict == NULL) {
1213+
Py_DECREF(path0);
1214+
return _PyStatus_ERR("can't initialize sys.path[0]");
1215+
}
1216+
PyObject *sys_path = PyDict_GetItemWithError(sysdict, &_Py_ID(path));
1217+
if (sys_path == NULL) {
1218+
Py_DECREF(path0);
1219+
return _PyStatus_ERR("can't initialize sys.path[0]");
1220+
}
1221+
int res = PyList_Insert(sys_path, 0, path0);
1222+
Py_DECREF(path0);
1223+
if (res) {
1224+
return _PyStatus_ERR("can't initialize sys.path[0]");
1225+
}
1226+
}
1227+
}
1228+
12031229
assert(!_PyErr_Occurred(tstate));
12041230

12051231
return _PyStatus_OK();

Python/pystate.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,10 @@ _PyRuntimeState_Fini(_PyRuntimeState *runtime)
524524
}
525525

526526
#undef FREE_LOCK
527+
if (runtime->sys_path_0 != NULL) {
528+
PyMem_RawFree(runtime->sys_path_0);
529+
runtime->sys_path_0 = NULL;
530+
}
527531
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
528532
}
529533

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy