Skip to content

Commit 52ad33a

Browse files
authored
bpo-38234: test_embed: test pyvenv.cfg and pybuilddir.txt (GH-16366)
Add test_init_pybuilddir() and test_init_pyvenv_cfg() to test_embed to test pyvenv.cfg and pybuilddir.txt configuration files. Fix sysconfig._generate_posix_vars(): pybuilddir.txt uses UTF-8 encoding, not ASCII.
1 parent 2180f6b commit 52ad33a

2 files changed

Lines changed: 166 additions & 16 deletions

File tree

Lib/sysconfig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ def _generate_posix_vars():
412412
pprint.pprint(vars, stream=f)
413413

414414
# Create file used for sys.path fixup -- see Modules/getpath.c
415-
with open('pybuilddir.txt', 'w', encoding='ascii') as f:
415+
with open('pybuilddir.txt', 'w', encoding='utf8') as f:
416416
f.write(pybuilddir)
417417

418418
def _init_posix(vars):

Lib/test/test_embed.py

Lines changed: 165 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import unittest
44

55
from collections import namedtuple
6+
import contextlib
67
import json
78
import os
89
import re
10+
import shutil
911
import subprocess
1012
import sys
13+
import tempfile
1114
import textwrap
1215

1316

@@ -25,6 +28,12 @@
2528
API_ISOLATED = 3
2629

2730

31+
def debug_build(program):
32+
program = os.path.basename(program)
33+
name = os.path.splitext(program)[0]
34+
return name.endswith("_d")
35+
36+
2837
def remove_python_envvars():
2938
env = dict(os.environ)
3039
# Remove PYTHON* environment variables to get deterministic environment
@@ -40,7 +49,7 @@ def setUp(self):
4049
basepath = os.path.dirname(os.path.dirname(os.path.dirname(here)))
4150
exename = "_testembed"
4251
if MS_WINDOWS:
43-
ext = ("_d" if "_d" in sys.executable else "") + ".exe"
52+
ext = ("_d" if debug_build(sys.executable) else "") + ".exe"
4453
exename += ext
4554
exepath = os.path.dirname(sys.executable)
4655
else:
@@ -58,7 +67,8 @@ def tearDown(self):
5867
os.chdir(self.oldcwd)
5968

6069
def run_embedded_interpreter(self, *args, env=None,
61-
timeout=None, returncode=0, input=None):
70+
timeout=None, returncode=0, input=None,
71+
cwd=None):
6272
"""Runs a test in the embedded interpreter"""
6373
cmd = [self.test_exe]
6474
cmd.extend(args)
@@ -72,7 +82,8 @@ def run_embedded_interpreter(self, *args, env=None,
7282
stdout=subprocess.PIPE,
7383
stderr=subprocess.PIPE,
7484
universal_newlines=True,
75-
env=env)
85+
env=env,
86+
cwd=cwd)
7687
try:
7788
(out, err) = p.communicate(input=input, timeout=timeout)
7889
except:
@@ -460,6 +471,11 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
460471

461472
EXPECTED_CONFIG = None
462473

474+
@classmethod
475+
def tearDownClass(cls):
476+
# clear cache
477+
cls.EXPECTED_CONFIG = None
478+
463479
def main_xoptions(self, xoptions_list):
464480
xoptions = {}
465481
for opt in xoptions_list:
@@ -490,11 +506,12 @@ def _get_expected_config_impl(self):
490506
args = [sys.executable, '-S', '-c', code]
491507
proc = subprocess.run(args, env=env,
492508
stdout=subprocess.PIPE,
493-
stderr=subprocess.STDOUT)
509+
stderr=subprocess.PIPE)
494510
if proc.returncode:
495511
raise Exception(f"failed to get the default config: "
496512
f"stdout={proc.stdout!r} stderr={proc.stderr!r}")
497513
stdout = proc.stdout.decode('utf-8')
514+
# ignore stderr
498515
try:
499516
return json.loads(stdout)
500517
except json.JSONDecodeError:
@@ -506,8 +523,15 @@ def _get_expected_config(self):
506523
cls.EXPECTED_CONFIG = self._get_expected_config_impl()
507524

508525
# get a copy
509-
return {key: dict(value)
510-
for key, value in cls.EXPECTED_CONFIG.items()}
526+
configs = {}
527+
for config_key, config_value in cls.EXPECTED_CONFIG.items():
528+
config = {}
529+
for key, value in config_value.items():
530+
if isinstance(value, list):
531+
value = value.copy()
532+
config[key] = value
533+
configs[config_key] = config
534+
return configs
511535

512536
def get_expected_config(self, expected_preconfig, expected, env, api,
513537
modify_path_cb=None):
@@ -612,7 +636,7 @@ def check_global_config(self, configs):
612636

613637
def check_all_configs(self, testname, expected_config=None,
614638
expected_preconfig=None, modify_path_cb=None, stderr=None,
615-
*, api, env=None, ignore_stderr=False):
639+
*, api, env=None, ignore_stderr=False, cwd=None):
616640
new_env = remove_python_envvars()
617641
if env is not None:
618642
new_env.update(env)
@@ -642,7 +666,8 @@ def check_all_configs(self, testname, expected_config=None,
642666
expected_config, env,
643667
api, modify_path_cb)
644668

645-
out, err = self.run_embedded_interpreter(testname, env=env)
669+
out, err = self.run_embedded_interpreter(testname,
670+
env=env, cwd=cwd)
646671
if stderr is None and not expected_config['verbose']:
647672
stderr = ""
648673
if stderr is not None and not ignore_stderr:
@@ -994,6 +1019,48 @@ def test_init_setpath(self):
9941019
api=API_COMPAT, env=env,
9951020
ignore_stderr=True)
9961021

1022+
def module_search_paths(self, prefix=None, exec_prefix=None):
1023+
config = self._get_expected_config()
1024+
if prefix is None:
1025+
prefix = config['config']['prefix']
1026+
if exec_prefix is None:
1027+
exec_prefix = config['config']['prefix']
1028+
if MS_WINDOWS:
1029+
return config['config']['module_search_paths']
1030+
else:
1031+
ver = sys.version_info
1032+
return [
1033+
os.path.join(prefix, 'lib',
1034+
f'python{ver.major}{ver.minor}.zip'),
1035+
os.path.join(prefix, 'lib',
1036+
f'python{ver.major}.{ver.minor}'),
1037+
os.path.join(exec_prefix, 'lib',
1038+
f'python{ver.major}.{ver.minor}', 'lib-dynload'),
1039+
]
1040+
1041+
@contextlib.contextmanager
1042+
def tmpdir_with_python(self):
1043+
# Temporary directory with a copy of the Python program
1044+
with tempfile.TemporaryDirectory() as tmpdir:
1045+
if MS_WINDOWS:
1046+
# Copy pythonXY.dll (or pythonXY_d.dll)
1047+
ver = sys.version_info
1048+
dll = f'python{ver.major}{ver.minor}'
1049+
if debug_build(sys.executable):
1050+
dll += '_d'
1051+
dll += '.dll'
1052+
dll = os.path.join(os.path.dirname(self.test_exe), dll)
1053+
dll_copy = os.path.join(tmpdir, os.path.basename(dll))
1054+
shutil.copyfile(dll, dll_copy)
1055+
1056+
# Copy Python program
1057+
exec_copy = os.path.join(tmpdir, os.path.basename(self.test_exe))
1058+
shutil.copyfile(self.test_exe, exec_copy)
1059+
shutil.copystat(self.test_exe, exec_copy)
1060+
self.test_exe = exec_copy
1061+
1062+
yield tmpdir
1063+
9971064
def test_init_setpythonhome(self):
9981065
# Test Py_SetPythonHome(home) + PYTHONPATH env var
9991066
# + Py_SetProgramName()
@@ -1012,13 +1079,7 @@ def test_init_setpythonhome(self):
10121079

10131080
prefix = exec_prefix = home
10141081
ver = sys.version_info
1015-
if MS_WINDOWS:
1016-
expected_paths = paths
1017-
else:
1018-
expected_paths = [
1019-
os.path.join(prefix, 'lib', f'python{ver.major}{ver.minor}.zip'),
1020-
os.path.join(home, 'lib', f'python{ver.major}.{ver.minor}'),
1021-
os.path.join(home, 'lib', f'python{ver.major}.{ver.minor}/lib-dynload')]
1082+
expected_paths = self.module_search_paths(prefix=home, exec_prefix=home)
10221083

10231084
config = {
10241085
'home': home,
@@ -1033,6 +1094,95 @@ def test_init_setpythonhome(self):
10331094
self.check_all_configs("test_init_setpythonhome", config,
10341095
api=API_COMPAT, env=env)
10351096

1097+
def copy_paths_by_env(self, config):
1098+
all_configs = self._get_expected_config()
1099+
paths = all_configs['config']['module_search_paths']
1100+
paths_str = os.path.pathsep.join(paths)
1101+
config['pythonpath_env'] = paths_str
1102+
env = {'PYTHONPATH': paths_str}
1103+
return env
1104+
1105+
@unittest.skipIf(MS_WINDOWS, 'Windows does not use pybuilddir.txt')
1106+
def test_init_pybuilddir(self):
1107+
# Test path configuration with pybuilddir.txt configuration file
1108+
1109+
with self.tmpdir_with_python() as tmpdir:
1110+
# pybuilddir.txt is a sub-directory relative to the current
1111+
# directory (tmpdir)
1112+
subdir = 'libdir'
1113+
libdir = os.path.join(tmpdir, subdir)
1114+
os.mkdir(libdir)
1115+
1116+
filename = os.path.join(tmpdir, 'pybuilddir.txt')
1117+
with open(filename, "w", encoding="utf8") as fp:
1118+
fp.write(subdir)
1119+
1120+
module_search_paths = self.module_search_paths()
1121+
module_search_paths[-1] = libdir
1122+
1123+
executable = self.test_exe
1124+
config = {
1125+
'base_executable': executable,
1126+
'executable': executable,
1127+
'module_search_paths': module_search_paths,
1128+
}
1129+
env = self.copy_paths_by_env(config)
1130+
self.check_all_configs("test_init_compat_config", config,
1131+
api=API_COMPAT, env=env,
1132+
ignore_stderr=True, cwd=tmpdir)
1133+
1134+
def test_init_pyvenv_cfg(self):
1135+
# Test path configuration with pyvenv.cfg configuration file
1136+
1137+
with self.tmpdir_with_python() as tmpdir, \
1138+
tempfile.TemporaryDirectory() as pyvenv_home:
1139+
ver = sys.version_info
1140+
1141+
if not MS_WINDOWS:
1142+
lib_dynload = os.path.join(pyvenv_home,
1143+
'lib',
1144+
f'python{ver.major}.{ver.minor}',
1145+
'lib-dynload')
1146+
os.makedirs(lib_dynload)
1147+
else:
1148+
lib_dynload = os.path.join(pyvenv_home, 'lib')
1149+
os.makedirs(lib_dynload)
1150+
# getpathp.c uses Lib\os.py as the LANDMARK
1151+
shutil.copyfile(os.__file__, os.path.join(lib_dynload, 'os.py'))
1152+
1153+
filename = os.path.join(tmpdir, 'pyvenv.cfg')
1154+
with open(filename, "w", encoding="utf8") as fp:
1155+
print("home = %s" % pyvenv_home, file=fp)
1156+
print("include-system-site-packages = false", file=fp)
1157+
1158+
paths = self.module_search_paths()
1159+
if not MS_WINDOWS:
1160+
paths[-1] = lib_dynload
1161+
else:
1162+
for index, path in enumerate(paths):
1163+
if index == 0:
1164+
paths[index] = os.path.join(tmpdir, os.path.basename(path))
1165+
else:
1166+
paths[index] = os.path.join(pyvenv_home, os.path.basename(path))
1167+
paths[-1] = pyvenv_home
1168+
1169+
executable = self.test_exe
1170+
exec_prefix = pyvenv_home
1171+
config = {
1172+
'base_exec_prefix': exec_prefix,
1173+
'exec_prefix': exec_prefix,
1174+
'base_executable': executable,
1175+
'executable': executable,
1176+
'module_search_paths': paths,
1177+
}
1178+
if MS_WINDOWS:
1179+
config['base_prefix'] = pyvenv_home
1180+
config['prefix'] = pyvenv_home
1181+
env = self.copy_paths_by_env(config)
1182+
self.check_all_configs("test_init_compat_config", config,
1183+
api=API_COMPAT, env=env,
1184+
ignore_stderr=True, cwd=tmpdir)
1185+
10361186

10371187
class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
10381188
def test_open_code_hook(self):

0 commit comments

Comments
 (0)