33import unittest
44
55from collections import namedtuple
6+ import contextlib
67import json
78import os
89import re
10+ import shutil
911import subprocess
1012import sys
13+ import tempfile
1114import textwrap
1215
1316
2528API_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+
2837def 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
10371187class AuditingTests (EmbeddingTestsMixin , unittest .TestCase ):
10381188 def test_open_code_hook (self ):
0 commit comments