Skip to content

Commit 5aed27b

Browse files
[3.12] pythongh-109853: Fix sys.path[0] For Subinterpreters (pythongh-109994)
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 pythongh-109853. (cherry-picked from commit a040a32)
1 parent 1ea4cb1 commit 5aed27b

File tree

7 files changed

+217
-10
lines changed

7 files changed

+217
-10
lines changed

Include/cpython/initconfig.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ typedef struct PyConfig {
205205
wchar_t *run_module;
206206
wchar_t *run_filename;
207207

208+
/* --- Set by Py_Main() -------------------------- */
209+
wchar_t *sys_path_0;
210+
208211
/* --- Private fields ---------------------------- */
209212

210213
// Install importlib? If equals to 0, importlib is not initialized at all.

Lib/test/test_embed.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
500500
'run_command': None,
501501
'run_module': None,
502502
'run_filename': None,
503+
'sys_path_0': None,
503504

504505
'_install_importlib': 1,
505506
'check_hash_pycs_mode': 'default',
@@ -1119,6 +1120,7 @@ def test_init_run_main(self):
11191120
'program_name': './python3',
11201121
'run_command': code + '\n',
11211122
'parse_argv': 2,
1123+
'sys_path_0': '',
11221124
}
11231125
self.check_all_configs("test_init_run_main", config, api=API_PYTHON)
11241126

@@ -1134,6 +1136,7 @@ def test_init_main(self):
11341136
'run_command': code + '\n',
11351137
'parse_argv': 2,
11361138
'_init_main': 0,
1139+
'sys_path_0': '',
11371140
}
11381141
self.check_all_configs("test_init_main", config,
11391142
api=API_PYTHON,

Lib/test/test_interpreters.py

Lines changed: 152 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
@@ -488,6 +492,154 @@ def task():
488492
pass
489493

490494

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

493645
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: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,11 @@ pymain_run_python(int *exitcode)
559559
goto error;
560560
}
561561

562+
// XXX Calculate config->sys_path_0 in getpath.py.
563+
// The tricky part is that we can't check the path importers yet
564+
// at that point.
565+
assert(config->sys_path_0 == NULL);
566+
562567
if (config->run_filename != NULL) {
563568
/* If filename is a package (ex: directory or ZIP file) which contains
564569
__main__.py, main_importer_path is set to filename and will be
@@ -574,24 +579,37 @@ pymain_run_python(int *exitcode)
574579
// import readline and rlcompleter before script dir is added to sys.path
575580
pymain_import_readline(config);
576581

582+
PyObject *path0 = NULL;
577583
if (main_importer_path != NULL) {
578-
if (pymain_sys_path_add_path0(interp, main_importer_path) < 0) {
579-
goto error;
580-
}
584+
path0 = Py_NewRef(main_importer_path);
581585
}
582586
else if (!config->safe_path) {
583-
PyObject *path0 = NULL;
584587
int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0);
585588
if (res < 0) {
586589
goto error;
587590
}
588-
589-
if (res > 0) {
590-
if (pymain_sys_path_add_path0(interp, path0) < 0) {
591-
Py_DECREF(path0);
592-
goto error;
593-
}
591+
else if (res == 0) {
592+
Py_CLEAR(path0);
593+
}
594+
}
595+
// XXX Apply config->sys_path_0 in init_interp_main(). We have
596+
// to be sure to get readline/rlcompleter imported at the correct time.
597+
if (path0 != NULL) {
598+
wchar_t *wstr = PyUnicode_AsWideCharString(path0, NULL);
599+
if (wstr == NULL) {
594600
Py_DECREF(path0);
601+
goto error;
602+
}
603+
config->sys_path_0 = _PyMem_RawWcsdup(wstr);
604+
PyMem_Free(wstr);
605+
if (config->sys_path_0 == NULL) {
606+
Py_DECREF(path0);
607+
goto error;
608+
}
609+
int res = pymain_sys_path_add_path0(interp, path0);
610+
Py_DECREF(path0);
611+
if (res < 0) {
612+
goto error;
595613
}
596614
}
597615

Python/initconfig.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,7 @@ PyConfig_Clear(PyConfig *config)
739739
CLEAR(config->exec_prefix);
740740
CLEAR(config->base_exec_prefix);
741741
CLEAR(config->platlibdir);
742+
CLEAR(config->sys_path_0);
742743

743744
CLEAR(config->filesystem_encoding);
744745
CLEAR(config->filesystem_errors);
@@ -993,6 +994,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
993994
COPY_WSTR_ATTR(exec_prefix);
994995
COPY_WSTR_ATTR(base_exec_prefix);
995996
COPY_WSTR_ATTR(platlibdir);
997+
COPY_WSTR_ATTR(sys_path_0);
996998

997999
COPY_ATTR(site_import);
9981000
COPY_ATTR(bytes_warning);
@@ -1102,6 +1104,7 @@ _PyConfig_AsDict(const PyConfig *config)
11021104
SET_ITEM_WSTR(exec_prefix);
11031105
SET_ITEM_WSTR(base_exec_prefix);
11041106
SET_ITEM_WSTR(platlibdir);
1107+
SET_ITEM_WSTR(sys_path_0);
11051108
SET_ITEM_INT(site_import);
11061109
SET_ITEM_INT(bytes_warning);
11071110
SET_ITEM_INT(warn_default_encoding);
@@ -1403,6 +1406,7 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
14031406
GET_WSTR_OPT(pythonpath_env);
14041407
GET_WSTR_OPT(home);
14051408
GET_WSTR(platlibdir);
1409+
GET_WSTR(sys_path_0);
14061410

14071411
// Path configuration output
14081412
GET_UINT(module_search_paths_set);
@@ -3165,6 +3169,7 @@ _Py_DumpPathConfig(PyThreadState *tstate)
31653169
PySys_WriteStderr(" import site = %i\n", config->site_import);
31663170
PySys_WriteStderr(" is in build tree = %i\n", config->_is_python_build);
31673171
DUMP_CONFIG("stdlib dir", stdlib_dir);
3172+
DUMP_CONFIG("sys.path[0]", sys_path_0);
31683173
#undef DUMP_CONFIG
31693174

31703175
#define DUMP_SYS(NAME) \

Python/pylifecycle.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,31 @@ 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+
if (config->sys_path_0 != NULL) {
1206+
PyObject *path0 = PyUnicode_FromWideChar(config->sys_path_0, -1);
1207+
if (path0 == NULL) {
1208+
return _PyStatus_ERR("can't initialize sys.path[0]");
1209+
}
1210+
PyObject *sysdict = interp->sysdict;
1211+
if (sysdict == NULL) {
1212+
Py_DECREF(path0);
1213+
return _PyStatus_ERR("can't initialize sys.path[0]");
1214+
}
1215+
PyObject *sys_path = PyDict_GetItemWithError(sysdict, &_Py_ID(path));
1216+
if (sys_path == NULL) {
1217+
Py_DECREF(path0);
1218+
return _PyStatus_ERR("can't initialize sys.path[0]");
1219+
}
1220+
int res = PyList_Insert(sys_path, 0, path0);
1221+
Py_DECREF(path0);
1222+
if (res) {
1223+
return _PyStatus_ERR("can't initialize sys.path[0]");
1224+
}
1225+
}
1226+
}
1227+
12031228
assert(!_PyErr_Occurred(tstate));
12041229

12051230
return _PyStatus_OK();

0 commit comments

Comments
 (0)