diff options
author | Steve Dower <steve.dower@python.org> | 2021-12-03 00:08:42 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-03 00:08:42 (GMT) |
commit | 99fcf1505218464c489d419d4500f126b6d6dc28 (patch) | |
tree | a9d607d854e943b3651248eadbe2f31f8c410021 /Lib | |
parent | 9f2f7e42269db74a89fc8cd74d82a875787f01d7 (diff) | |
download | cpython-99fcf1505218464c489d419d4500f126b6d6dc28.zip cpython-99fcf1505218464c489d419d4500f126b6d6dc28.tar.gz cpython-99fcf1505218464c489d419d4500f126b6d6dc28.tar.bz2 |
bpo-45582: Port getpath[p].c to Python (GH-29041)
The getpath.py file is frozen at build time and executed as code over a namespace. It is never imported, nor is it meant to be importable or reusable. However, it should be easier to read, modify, and patch than the previous code.
This commit attempts to preserve every previously tested quirk, but these may be changed in the future to better align platforms.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/ntpath.py | 112 | ||||
-rw-r--r-- | Lib/posixpath.py | 86 | ||||
-rw-r--r-- | Lib/site.py | 13 | ||||
-rw-r--r-- | Lib/sysconfig.py | 5 | ||||
-rw-r--r-- | Lib/test/_test_embed_set_config.py | 1 | ||||
-rw-r--r-- | Lib/test/test_embed.py | 160 | ||||
-rw-r--r-- | Lib/test/test_getpath.py | 879 | ||||
-rw-r--r-- | Lib/test/test_ntpath.py | 5 | ||||
-rw-r--r-- | Lib/test/test_site.py | 22 | ||||
-rw-r--r-- | Lib/test/test_sysconfig.py | 4 | ||||
-rw-r--r-- | Lib/test/test_venv.py | 6 |
11 files changed, 1106 insertions, 187 deletions
diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 527c7ae..58483a0 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -70,7 +70,7 @@ def isabs(s): if s.replace('/', '\\').startswith('\\\\?\\'): return True s = splitdrive(s)[1] - return len(s) > 0 and s[0] in _get_bothseps(s) + return len(s) > 0 and s[0] and s[0] in _get_bothseps(s) # Join two (or more) paths. @@ -268,11 +268,13 @@ def ismount(path): root, rest = splitdrive(path) if root and root[0] in seps: return (not rest) or (rest in seps) - if rest in seps: + if rest and rest in seps: return True if _getvolumepathname: - return path.rstrip(seps) == _getvolumepathname(path).rstrip(seps) + x = path.rstrip(seps) + y =_getvolumepathname(path).rstrip(seps) + return x.casefold() == y.casefold() else: return False @@ -459,56 +461,68 @@ def expandvars(path): # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. # Previously, this function also truncated pathnames to 8+3 format, # but as this module is called "ntpath", that's obviously wrong! +try: + from nt import _path_normpath -def normpath(path): - """Normalize path, eliminating double slashes, etc.""" - path = os.fspath(path) - if isinstance(path, bytes): - sep = b'\\' - altsep = b'/' - curdir = b'.' - pardir = b'..' - special_prefixes = (b'\\\\.\\', b'\\\\?\\') - else: - sep = '\\' - altsep = '/' - curdir = '.' - pardir = '..' - special_prefixes = ('\\\\.\\', '\\\\?\\') - if path.startswith(special_prefixes): - # in the case of paths with these prefixes: - # \\.\ -> device names - # \\?\ -> literal paths - # do not do any normalization, but return the path - # unchanged apart from the call to os.fspath() - return path - path = path.replace(altsep, sep) - prefix, path = splitdrive(path) - - # collapse initial backslashes - if path.startswith(sep): - prefix += sep - path = path.lstrip(sep) - - comps = path.split(sep) - i = 0 - while i < len(comps): - if not comps[i] or comps[i] == curdir: - del comps[i] - elif comps[i] == pardir: - if i > 0 and comps[i-1] != pardir: - del comps[i-1:i+1] - i -= 1 - elif i == 0 and prefix.endswith(sep): +except ImportError: + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + sep = b'\\' + altsep = b'/' + curdir = b'.' + pardir = b'..' + special_prefixes = (b'\\\\.\\', b'\\\\?\\') + else: + sep = '\\' + altsep = '/' + curdir = '.' + pardir = '..' + special_prefixes = ('\\\\.\\', '\\\\?\\') + if path.startswith(special_prefixes): + # in the case of paths with these prefixes: + # \\.\ -> device names + # \\?\ -> literal paths + # do not do any normalization, but return the path + # unchanged apart from the call to os.fspath() + return path + path = path.replace(altsep, sep) + prefix, path = splitdrive(path) + + # collapse initial backslashes + if path.startswith(sep): + prefix += sep + path = path.lstrip(sep) + + comps = path.split(sep) + i = 0 + while i < len(comps): + if not comps[i] or comps[i] == curdir: del comps[i] + elif comps[i] == pardir: + if i > 0 and comps[i-1] != pardir: + del comps[i-1:i+1] + i -= 1 + elif i == 0 and prefix.endswith(sep): + del comps[i] + else: + i += 1 else: i += 1 - else: - i += 1 - # If the path is now empty, substitute '.' - if not prefix and not comps: - comps.append(curdir) - return prefix + sep.join(comps) + # If the path is now empty, substitute '.' + if not prefix and not comps: + comps.append(curdir) + return prefix + sep.join(comps) + +else: + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + return os.fsencode(_path_normpath(os.fsdecode(path))) or b"." + return _path_normpath(path) or "." + def _abspath_fallback(path): """Return the absolute version of a path as a fallback function in case diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 1953746..a46c667 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -334,43 +334,55 @@ def expandvars(path): # It should be understood that this may change the meaning of the path # if it contains symbolic links! -def normpath(path): - """Normalize path, eliminating double slashes, etc.""" - path = os.fspath(path) - if isinstance(path, bytes): - sep = b'/' - empty = b'' - dot = b'.' - dotdot = b'..' - else: - sep = '/' - empty = '' - dot = '.' - dotdot = '..' - if path == empty: - return dot - initial_slashes = path.startswith(sep) - # POSIX allows one or two initial slashes, but treats three or more - # as single slash. - # (see http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13) - if (initial_slashes and - path.startswith(sep*2) and not path.startswith(sep*3)): - initial_slashes = 2 - comps = path.split(sep) - new_comps = [] - for comp in comps: - if comp in (empty, dot): - continue - if (comp != dotdot or (not initial_slashes and not new_comps) or - (new_comps and new_comps[-1] == dotdot)): - new_comps.append(comp) - elif new_comps: - new_comps.pop() - comps = new_comps - path = sep.join(comps) - if initial_slashes: - path = sep*initial_slashes + path - return path or dot +try: + from posix import _path_normpath + +except ImportError: + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + sep = b'/' + empty = b'' + dot = b'.' + dotdot = b'..' + else: + sep = '/' + empty = '' + dot = '.' + dotdot = '..' + if path == empty: + return dot + initial_slashes = path.startswith(sep) + # POSIX allows one or two initial slashes, but treats three or more + # as single slash. + # (see http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13) + if (initial_slashes and + path.startswith(sep*2) and not path.startswith(sep*3)): + initial_slashes = 2 + comps = path.split(sep) + new_comps = [] + for comp in comps: + if comp in (empty, dot): + continue + if (comp != dotdot or (not initial_slashes and not new_comps) or + (new_comps and new_comps[-1] == dotdot)): + new_comps.append(comp) + elif new_comps: + new_comps.pop() + comps = new_comps + path = sep.join(comps) + if initial_slashes: + path = sep*initial_slashes + path + return path or dot + +else: + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + return os.fsencode(_path_normpath(os.fsdecode(path))) or b"." + return _path_normpath(path) or "." def abspath(path): diff --git a/Lib/site.py b/Lib/site.py index e129f3b..b11cd48 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -361,11 +361,11 @@ def getsitepackages(prefixes=None): continue seen.add(prefix) - libdirs = [sys.platlibdir] - if sys.platlibdir != "lib": - libdirs.append("lib") - if os.sep == '/': + libdirs = [sys.platlibdir] + if sys.platlibdir != "lib": + libdirs.append("lib") + for libdir in libdirs: path = os.path.join(prefix, libdir, "python%d.%d" % sys.version_info[:2], @@ -373,10 +373,7 @@ def getsitepackages(prefixes=None): sitepackages.append(path) else: sitepackages.append(prefix) - - for libdir in libdirs: - path = os.path.join(prefix, libdir, "site-packages") - sitepackages.append(path) + sitepackages.append(os.path.join(prefix, "Lib", "site-packages")) return sitepackages def addsitepackages(known_paths, prefixes=None): diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py index daf9f00..da918b7 100644 --- a/Lib/sysconfig.py +++ b/Lib/sysconfig.py @@ -216,6 +216,11 @@ def _expand_vars(scheme, vars): if vars is None: vars = {} _extend_dict(vars, get_config_vars()) + if os.name == 'nt': + # On Windows we want to substitute 'lib' for schemes rather + # than the native value (without modifying vars, in case it + # was passed in) + vars = vars | {'platlibdir': 'lib'} for key, value in _INSTALL_SCHEMES[scheme].items(): if os.name in ('posix', 'nt'): diff --git a/Lib/test/_test_embed_set_config.py b/Lib/test/_test_embed_set_config.py index d05907e..e7b7106 100644 --- a/Lib/test/_test_embed_set_config.py +++ b/Lib/test/_test_embed_set_config.py @@ -20,6 +20,7 @@ class SetConfigTests(unittest.TestCase): self.sys_copy = dict(sys.__dict__) def tearDown(self): + _testinternalcapi.reset_path_config() _testinternalcapi.set_config(self.old_config) sys.__dict__.clear() sys.__dict__.update(self.sys_copy) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 4781c7f..3620a76 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -13,6 +13,7 @@ import re import shutil import subprocess import sys +import sysconfig import tempfile import textwrap @@ -445,6 +446,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): '_init_main': 1, '_isolated_interpreter': 0, 'use_frozen_modules': 1, + '_is_python_build': IGNORE_CONFIG, } if MS_WINDOWS: CONFIG_COMPAT.update({ @@ -508,32 +510,6 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ('Py_LegacyWindowsStdioFlag', 'legacy_windows_stdio'), )) - # path config - if MS_WINDOWS: - PATH_CONFIG = { - 'isolated': -1, - 'site_import': -1, - 'python3_dll': GET_DEFAULT_CONFIG, - } - else: - PATH_CONFIG = {} - # other keys are copied by COPY_PATH_CONFIG - - COPY_PATH_CONFIG = [ - # Copy core config to global config for expected values - 'prefix', - 'exec_prefix', - 'program_name', - 'home', - 'stdlib_dir', - # program_full_path and module_search_path are copied indirectly from - # the core configuration in check_path_config(). - ] - if MS_WINDOWS: - COPY_PATH_CONFIG.extend(( - 'base_executable', - )) - EXPECTED_CONFIG = None @classmethod @@ -599,8 +575,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): return configs def get_expected_config(self, expected_preconfig, expected, - expected_pathconfig, env, api, - modify_path_cb=None): + env, api, modify_path_cb=None): configs = self._get_expected_config() pre_config = configs['pre_config'] @@ -608,11 +583,6 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): if value is self.GET_DEFAULT_CONFIG: expected_preconfig[key] = pre_config[key] - path_config = configs['path_config'] - for key, value in expected_pathconfig.items(): - if value is self.GET_DEFAULT_CONFIG: - expected_pathconfig[key] = path_config[key] - if not expected_preconfig['configure_locale'] or api == API_COMPAT: # there is no easy way to get the locale encoding before # setlocale(LC_CTYPE, "") is called: don't test encodings @@ -705,18 +675,8 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): self.assertEqual(configs['global_config'], expected) - def check_path_config(self, configs, expected): - config = configs['config'] - - for key in self.COPY_PATH_CONFIG: - expected[key] = config[key] - expected['module_search_path'] = os.path.pathsep.join(config['module_search_paths']) - expected['program_full_path'] = config['executable'] - - self.assertEqual(configs['path_config'], expected) - def check_all_configs(self, testname, expected_config=None, - expected_preconfig=None, expected_pathconfig=None, + expected_preconfig=None, modify_path_cb=None, stderr=None, *, api, preconfig_api=None, env=None, ignore_stderr=False, cwd=None): @@ -740,10 +700,6 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): if expected_config is None: expected_config = {} - if expected_pathconfig is None: - expected_pathconfig = {} - expected_pathconfig = dict(self.PATH_CONFIG, **expected_pathconfig) - if api == API_PYTHON: default_config = self.CONFIG_PYTHON elif api == API_ISOLATED: @@ -754,7 +710,6 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): self.get_expected_config(expected_preconfig, expected_config, - expected_pathconfig, env, api, modify_path_cb) @@ -772,7 +727,6 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): self.check_pre_config(configs, expected_preconfig) self.check_config(configs, expected_config) self.check_global_config(configs) - self.check_path_config(configs, expected_pathconfig) return configs def test_init_default_config(self): @@ -1039,10 +993,13 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): self.check_all_configs("test_init_dont_configure_locale", {}, preconfig, api=API_PYTHON) + @unittest.skip('as of 3.11 this test no longer works because ' + 'path calculations do not occur on read') def test_init_read_set(self): config = { 'program_name': './init_read_set', 'executable': 'my_executable', + 'base_executable': 'my_executable', } def modify_path(path): path.insert(1, "test_path_insert1") @@ -1156,7 +1113,6 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): # The current getpath.c doesn't determine the stdlib dir # in this case. 'stdlib_dir': '', - 'use_frozen_modules': -1, } self.default_program_name(config) env = {'TESTPATH': os.path.pathsep.join(paths)} @@ -1180,7 +1136,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): # The current getpath.c doesn't determine the stdlib dir # in this case. 'stdlib_dir': '', - 'use_frozen_modules': -1, + 'use_frozen_modules': 1, # overridden by PyConfig 'program_name': 'conf_program_name', 'base_executable': 'conf_executable', @@ -1210,13 +1166,16 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): ] @contextlib.contextmanager - def tmpdir_with_python(self): + def tmpdir_with_python(self, subdir=None): # Temporary directory with a copy of the Python program with tempfile.TemporaryDirectory() as tmpdir: # bpo-38234: On macOS and FreeBSD, the temporary directory # can be symbolic link. For example, /tmp can be a symbolic link # to /var/tmp. Call realpath() to resolve all symbolic links. tmpdir = os.path.realpath(tmpdir) + if subdir: + tmpdir = os.path.normpath(os.path.join(tmpdir, subdir)) + os.makedirs(tmpdir) if MS_WINDOWS: # Copy pythonXY.dll (or pythonXY_d.dll) @@ -1260,11 +1219,14 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): prefix = exec_prefix = home if MS_WINDOWS: - stdlib = os.path.join(home, sys.platlibdir) + stdlib = os.path.join(home, "Lib") + # Because we are specifying 'home', module search paths + # are fairly static + expected_paths = [paths[0], stdlib, os.path.join(home, 'DLLs')] else: version = f'{sys.version_info.major}.{sys.version_info.minor}' stdlib = os.path.join(home, sys.platlibdir, f'python{version}') - expected_paths = self.module_search_paths(prefix=home, exec_prefix=home) + expected_paths = self.module_search_paths(prefix=home, exec_prefix=home) config = { 'home': home, @@ -1291,7 +1253,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): env = {'PYTHONPATH': paths_str} return env - @unittest.skipIf(MS_WINDOWS, 'Windows does not use pybuilddir.txt') + @unittest.skipIf(MS_WINDOWS, 'See test_init_pybuilddir_win32') def test_init_pybuilddir(self): # Test path configuration with pybuilddir.txt configuration file @@ -1300,6 +1262,8 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): # directory (tmpdir) subdir = 'libdir' libdir = os.path.join(tmpdir, subdir) + # The stdlib dir is dirname(executable) + VPATH + 'Lib' + stdlibdir = os.path.join(tmpdir, 'Lib') os.mkdir(libdir) filename = os.path.join(tmpdir, 'pybuilddir.txt') @@ -1307,21 +1271,58 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): fp.write(subdir) module_search_paths = self.module_search_paths() + module_search_paths[-2] = stdlibdir module_search_paths[-1] = libdir executable = self.test_exe config = { + 'base_exec_prefix': sysconfig.get_config_var("exec_prefix"), + 'base_prefix': sysconfig.get_config_var("prefix"), 'base_executable': executable, 'executable': executable, 'module_search_paths': module_search_paths, - 'stdlib_dir': STDLIB_INSTALL, - 'use_frozen_modules': 1 if STDLIB_INSTALL else -1, + 'stdlib_dir': stdlibdir, } env = self.copy_paths_by_env(config) self.check_all_configs("test_init_compat_config", config, api=API_COMPAT, env=env, ignore_stderr=True, cwd=tmpdir) + @unittest.skipUnless(MS_WINDOWS, 'See test_init_pybuilddir') + def test_init_pybuilddir_win32(self): + # Test path configuration with pybuilddir.txt configuration file + + with self.tmpdir_with_python(r'PCbuild\arch') as tmpdir: + # The prefix is dirname(executable) + VPATH + prefix = os.path.normpath(os.path.join(tmpdir, r'..\..')) + # The stdlib dir is dirname(executable) + VPATH + 'Lib' + stdlibdir = os.path.normpath(os.path.join(tmpdir, r'..\..\Lib')) + + filename = os.path.join(tmpdir, 'pybuilddir.txt') + with open(filename, "w", encoding="utf8") as fp: + fp.write(tmpdir) + + module_search_paths = self.module_search_paths() + module_search_paths[-3] = os.path.join(tmpdir, os.path.basename(module_search_paths[-3])) + module_search_paths[-2] = stdlibdir + module_search_paths[-1] = tmpdir + + executable = self.test_exe + config = { + 'base_exec_prefix': prefix, + 'base_prefix': prefix, + 'base_executable': executable, + 'executable': executable, + 'prefix': prefix, + 'exec_prefix': prefix, + 'module_search_paths': module_search_paths, + 'stdlib_dir': stdlibdir, + } + env = self.copy_paths_by_env(config) + self.check_all_configs("test_init_compat_config", config, + api=API_COMPAT, env=env, + ignore_stderr=False, cwd=tmpdir) + def test_init_pyvenv_cfg(self): # Test path configuration with pyvenv.cfg configuration file @@ -1336,12 +1337,12 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'lib-dynload') os.makedirs(lib_dynload) else: - lib_dynload = os.path.join(pyvenv_home, 'lib') - os.makedirs(lib_dynload) - # getpathp.c uses Lib\os.py as the LANDMARK + lib_folder = os.path.join(pyvenv_home, 'Lib') + os.makedirs(lib_folder) + # getpath.py uses Lib\os.py as the LANDMARK shutil.copyfile( os.path.join(support.STDLIB_DIR, 'os.py'), - os.path.join(lib_dynload, 'os.py'), + os.path.join(lib_folder, 'os.py'), ) filename = os.path.join(tmpdir, 'pyvenv.cfg') @@ -1355,43 +1356,38 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): else: for index, path in enumerate(paths): if index == 0: + # Because we copy the DLLs into tmpdir as well, the zip file + # entry in sys.path will be there. For a regular venv, it will + # usually be in the home directory. paths[index] = os.path.join(tmpdir, os.path.basename(path)) else: paths[index] = os.path.join(pyvenv_home, os.path.basename(path)) paths[-1] = pyvenv_home executable = self.test_exe + base_executable = os.path.join(pyvenv_home, os.path.basename(executable)) exec_prefix = pyvenv_home config = { + 'base_prefix': sysconfig.get_config_var("prefix"), 'base_exec_prefix': exec_prefix, 'exec_prefix': exec_prefix, - 'base_executable': executable, + 'base_executable': base_executable, 'executable': executable, 'module_search_paths': paths, } - path_config = {} if MS_WINDOWS: config['base_prefix'] = pyvenv_home config['prefix'] = pyvenv_home - config['stdlib_dir'] = os.path.join(pyvenv_home, 'lib') + config['stdlib_dir'] = os.path.join(pyvenv_home, 'Lib') config['use_frozen_modules'] = 1 - - ver = sys.version_info - dll = f'python{ver.major}' - if debug_build(executable): - dll += '_d' - dll += '.DLL' - dll = os.path.join(os.path.dirname(executable), dll) - path_config['python3_dll'] = dll else: - # The current getpath.c doesn't determine the stdlib dir - # in this case. - config['stdlib_dir'] = STDLIB_INSTALL - config['use_frozen_modules'] = 1 if STDLIB_INSTALL else -1 + # cannot reliably assume stdlib_dir here because it + # depends too much on our build. But it ought to be found + config['stdlib_dir'] = self.IGNORE_CONFIG + config['use_frozen_modules'] = 1 env = self.copy_paths_by_env(config) self.check_all_configs("test_init_compat_config", config, - expected_pathconfig=path_config, api=API_COMPAT, env=env, ignore_stderr=True, cwd=tmpdir) @@ -1502,10 +1498,14 @@ class SetConfigTests(unittest.TestCase): def test_set_config(self): # bpo-42260: Test _PyInterpreterState_SetConfig() import_helper.import_module('_testcapi') - cmd = [sys.executable, '-I', '-m', 'test._test_embed_set_config'] + cmd = [sys.executable, '-X', 'utf8', '-I', '-m', 'test._test_embed_set_config'] proc = subprocess.run(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE, + encoding='utf-8', errors='backslashreplace') + if proc.returncode and support.verbose: + print(proc.stdout) + print(proc.stderr) self.assertEqual(proc.returncode, 0, (proc.returncode, proc.stdout, proc.stderr)) diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py new file mode 100644 index 0000000..c18689c --- /dev/null +++ b/Lib/test/test_getpath.py @@ -0,0 +1,879 @@ +import copy +import ntpath +import pathlib +import posixpath +import sys +import unittest + +from test.support import verbose + +try: + # If we are in a source tree, use the original source file for tests + SOURCE = (pathlib.Path(__file__).absolute().parent.parent.parent / "Modules/getpath.py").read_bytes() +except FileNotFoundError: + # Try from _testcapimodule instead + from _testinternalcapi import get_getpath_codeobject + SOURCE = get_getpath_codeobject() + + +class MockGetPathTests(unittest.TestCase): + def __init__(self, *a, **kw): + super().__init__(*a, **kw) + self.maxDiff = None + + def test_normal_win32(self): + "Test a 'standard' install layout on Windows." + ns = MockNTNamespace( + argv0=r"C:\Python\python.exe", + real_executable=r"C:\Python\python.exe", + ) + ns.add_known_xfile(r"C:\Python\python.exe") + ns.add_known_file(r"C:\Python\Lib\os.py") + ns.add_known_dir(r"C:\Python\DLLs") + expected = dict( + executable=r"C:\Python\python.exe", + base_executable=r"C:\Python\python.exe", + prefix=r"C:\Python", + exec_prefix=r"C:\Python", + module_search_paths_set=1, + module_search_paths=[ + r"C:\Python\python98.zip", + r"C:\Python\Lib", + r"C:\Python\DLLs", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_buildtree_win32(self): + "Test an in-build-tree layout on Windows." + ns = MockNTNamespace( + argv0=r"C:\CPython\PCbuild\amd64\python.exe", + real_executable=r"C:\CPython\PCbuild\amd64\python.exe", + ) + ns.add_known_xfile(r"C:\CPython\PCbuild\amd64\python.exe") + ns.add_known_file(r"C:\CPython\Lib\os.py") + ns.add_known_file(r"C:\CPython\PCbuild\amd64\pybuilddir.txt", [""]) + expected = dict( + executable=r"C:\CPython\PCbuild\amd64\python.exe", + base_executable=r"C:\CPython\PCbuild\amd64\python.exe", + prefix=r"C:\CPython", + exec_prefix=r"C:\CPython", + build_prefix=r"C:\CPython", + _is_python_build=1, + module_search_paths_set=1, + module_search_paths=[ + r"C:\CPython\PCbuild\amd64\python98.zip", + r"C:\CPython\Lib", + r"C:\CPython\PCbuild\amd64", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_venv_win32(self): + """Test a venv layout on Windows. + + This layout is discovered by the presence of %__PYVENV_LAUNCHER__%, + specifying the original launcher executable. site.py is responsible + for updating prefix and exec_prefix. + """ + ns = MockNTNamespace( + argv0=r"C:\Python\python.exe", + ENV___PYVENV_LAUNCHER__=r"C:\venv\Scripts\python.exe", + real_executable=r"C:\Python\python.exe", + ) + ns.add_known_xfile(r"C:\Python\python.exe") + ns.add_known_xfile(r"C:\venv\Scripts\python.exe") + ns.add_known_file(r"C:\Python\Lib\os.py") + ns.add_known_dir(r"C:\Python\DLLs") + ns.add_known_file(r"C:\venv\pyvenv.cfg", [ + r"home = C:\Python" + ]) + expected = dict( + executable=r"C:\venv\Scripts\python.exe", + prefix=r"C:\Python", + exec_prefix=r"C:\Python", + base_executable=r"C:\Python\python.exe", + base_prefix=r"C:\Python", + base_exec_prefix=r"C:\Python", + module_search_paths_set=1, + module_search_paths=[ + r"C:\Python\python98.zip", + r"C:\Python\Lib", + r"C:\Python", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_registry_win32(self): + """Test registry lookup on Windows. + + On Windows there are registry entries that are intended for other + applications to register search paths. + """ + hkey = rf"HKLM\Software\Python\PythonCore\9.8-XY\PythonPath" + winreg = MockWinreg({ + hkey: None, + f"{hkey}\\Path1": "path1-dir", + f"{hkey}\\Path1\\Subdir": "not-subdirs", + }) + ns = MockNTNamespace( + argv0=r"C:\Python\python.exe", + real_executable=r"C:\Python\python.exe", + winreg=winreg, + ) + ns.add_known_xfile(r"C:\Python\python.exe") + ns.add_known_file(r"C:\Python\Lib\os.py") + ns.add_known_dir(r"C:\Python\DLLs") + expected = dict( + module_search_paths_set=1, + module_search_paths=[ + r"C:\Python\python98.zip", + "path1-dir", + # should not contain not-subdirs + r"C:\Python\Lib", + r"C:\Python\DLLs", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + ns["config"]["use_environment"] = 0 + ns["config"]["module_search_paths_set"] = 0 + ns["config"]["module_search_paths"] = None + expected = dict( + module_search_paths_set=1, + module_search_paths=[ + r"C:\Python\python98.zip", + r"C:\Python\Lib", + r"C:\Python\DLLs", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_symlink_normal_win32(self): + "Test a 'standard' install layout via symlink on Windows." + ns = MockNTNamespace( + argv0=r"C:\LinkedFrom\python.exe", + real_executable=r"C:\Python\python.exe", + ) + ns.add_known_xfile(r"C:\LinkedFrom\python.exe") + ns.add_known_xfile(r"C:\Python\python.exe") + ns.add_known_link(r"C:\LinkedFrom\python.exe", r"C:\Python\python.exe") + ns.add_known_file(r"C:\Python\Lib\os.py") + ns.add_known_dir(r"C:\Python\DLLs") + expected = dict( + executable=r"C:\LinkedFrom\python.exe", + base_executable=r"C:\LinkedFrom\python.exe", + prefix=r"C:\Python", + exec_prefix=r"C:\Python", + module_search_paths_set=1, + module_search_paths=[ + r"C:\Python\python98.zip", + r"C:\Python\Lib", + r"C:\Python\DLLs", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_symlink_buildtree_win32(self): + "Test an in-build-tree layout via symlink on Windows." + ns = MockNTNamespace( + argv0=r"C:\LinkedFrom\python.exe", + real_executable=r"C:\CPython\PCbuild\amd64\python.exe", + ) + ns.add_known_xfile(r"C:\LinkedFrom\python.exe") + ns.add_known_xfile(r"C:\CPython\PCbuild\amd64\python.exe") + ns.add_known_link(r"C:\LinkedFrom\python.exe", r"C:\CPython\PCbuild\amd64\python.exe") + ns.add_known_file(r"C:\CPython\Lib\os.py") + ns.add_known_file(r"C:\CPython\PCbuild\amd64\pybuilddir.txt", [""]) + expected = dict( + executable=r"C:\LinkedFrom\python.exe", + base_executable=r"C:\LinkedFrom\python.exe", + prefix=r"C:\CPython", + exec_prefix=r"C:\CPython", + build_prefix=r"C:\CPython", + _is_python_build=1, + module_search_paths_set=1, + module_search_paths=[ + r"C:\CPython\PCbuild\amd64\python98.zip", + r"C:\CPython\Lib", + r"C:\CPython\PCbuild\amd64", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_normal_posix(self): + "Test a 'standard' install layout on *nix" + ns = MockPosixNamespace( + PREFIX="/usr", + argv0="python", + ENV_PATH="/usr/bin", + ) + ns.add_known_xfile("/usr/bin/python") + ns.add_known_file("/usr/lib/python9.8/os.py") + ns.add_known_dir("/usr/lib/python9.8/lib-dynload") + expected = dict( + executable="/usr/bin/python", + base_executable="/usr/bin/python", + prefix="/usr", + exec_prefix="/usr", + module_search_paths_set=1, + module_search_paths=[ + "/usr/lib/python98.zip", + "/usr/lib/python9.8", + "/usr/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_buildpath_posix(self): + """Test an in-build-tree layout on POSIX. + + This layout is discovered from the presence of pybuilddir.txt, which + contains the relative path from the executable's directory to the + platstdlib path. + """ + ns = MockPosixNamespace( + argv0=r"/home/cpython/python", + PREFIX="/usr/local", + ) + ns.add_known_xfile("/home/cpython/python") + ns.add_known_xfile("/usr/local/bin/python") + ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.linux-x86_64-9.8"]) + ns.add_known_file("/home/cpython/Lib/os.py") + ns.add_known_dir("/home/cpython/lib-dynload") + expected = dict( + executable="/home/cpython/python", + prefix="/usr/local", + exec_prefix="/usr/local", + base_executable="/home/cpython/python", + build_prefix="/home/cpython", + _is_python_build=1, + module_search_paths_set=1, + module_search_paths=[ + "/usr/local/lib/python98.zip", + "/home/cpython/Lib", + "/home/cpython/build/lib.linux-x86_64-9.8", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_venv_posix(self): + "Test a venv layout on *nix." + ns = MockPosixNamespace( + argv0="python", + PREFIX="/usr", + ENV_PATH="/venv/bin:/usr/bin", + ) + ns.add_known_xfile("/usr/bin/python") + ns.add_known_xfile("/venv/bin/python") + ns.add_known_file("/usr/lib/python9.8/os.py") + ns.add_known_dir("/usr/lib/python9.8/lib-dynload") + ns.add_known_file("/venv/pyvenv.cfg", [ + r"home = /usr/bin" + ]) + expected = dict( + executable="/venv/bin/python", + prefix="/usr", + exec_prefix="/usr", + base_executable="/usr/bin/python", + base_prefix="/usr", + base_exec_prefix="/usr", + module_search_paths_set=1, + module_search_paths=[ + "/usr/lib/python98.zip", + "/usr/lib/python9.8", + "/usr/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_symlink_normal_posix(self): + "Test a 'standard' install layout via symlink on *nix" + ns = MockPosixNamespace( + PREFIX="/usr", + argv0="/linkfrom/python", + ) + ns.add_known_xfile("/linkfrom/python") + ns.add_known_xfile("/usr/bin/python") + ns.add_known_link("/linkfrom/python", "/usr/bin/python") + ns.add_known_file("/usr/lib/python9.8/os.py") + ns.add_known_dir("/usr/lib/python9.8/lib-dynload") + expected = dict( + executable="/linkfrom/python", + base_executable="/linkfrom/python", + prefix="/usr", + exec_prefix="/usr", + module_search_paths_set=1, + module_search_paths=[ + "/usr/lib/python98.zip", + "/usr/lib/python9.8", + "/usr/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_symlink_buildpath_posix(self): + """Test an in-build-tree layout on POSIX. + + This layout is discovered from the presence of pybuilddir.txt, which + contains the relative path from the executable's directory to the + platstdlib path. + """ + ns = MockPosixNamespace( + argv0=r"/linkfrom/python", + PREFIX="/usr/local", + ) + ns.add_known_xfile("/linkfrom/python") + ns.add_known_xfile("/home/cpython/python") + ns.add_known_link("/linkfrom/python", "/home/cpython/python") + ns.add_known_xfile("/usr/local/bin/python") + ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.linux-x86_64-9.8"]) + ns.add_known_file("/home/cpython/Lib/os.py") + ns.add_known_dir("/home/cpython/lib-dynload") + expected = dict( + executable="/linkfrom/python", + prefix="/usr/local", + exec_prefix="/usr/local", + base_executable="/linkfrom/python", + build_prefix="/home/cpython", + _is_python_build=1, + module_search_paths_set=1, + module_search_paths=[ + "/usr/local/lib/python98.zip", + "/home/cpython/Lib", + "/home/cpython/build/lib.linux-x86_64-9.8", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_custom_platlibdir_posix(self): + "Test an install with custom platlibdir on *nix" + ns = MockPosixNamespace( + PREFIX="/usr", + argv0="/linkfrom/python", + PLATLIBDIR="lib64", + ) + ns.add_known_xfile("/usr/bin/python") + ns.add_known_file("/usr/lib64/python9.8/os.py") + ns.add_known_dir("/usr/lib64/python9.8/lib-dynload") + expected = dict( + executable="/linkfrom/python", + base_executable="/linkfrom/python", + prefix="/usr", + exec_prefix="/usr", + module_search_paths_set=1, + module_search_paths=[ + "/usr/lib64/python98.zip", + "/usr/lib64/python9.8", + "/usr/lib64/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_venv_macos(self): + """Test a venv layout on macOS. + + This layout is discovered when 'executable' and 'real_executable' match, + but $__PYVENV_LAUNCHER__ has been set to the original process. + """ + ns = MockPosixNamespace( + os_name="darwin", + argv0="/usr/bin/python", + PREFIX="/usr", + ENV___PYVENV_LAUNCHER__="/framework/Python9.8/python", + real_executable="/usr/bin/python", + ) + ns.add_known_xfile("/usr/bin/python") + ns.add_known_xfile("/framework/Python9.8/python") + ns.add_known_file("/usr/lib/python9.8/os.py") + ns.add_known_dir("/usr/lib/python9.8/lib-dynload") + ns.add_known_file("/framework/Python9.8/pyvenv.cfg", [ + "home = /usr/bin" + ]) + expected = dict( + executable="/framework/Python9.8/python", + prefix="/usr", + exec_prefix="/usr", + base_executable="/usr/bin/python", + base_prefix="/usr", + base_exec_prefix="/usr", + module_search_paths_set=1, + module_search_paths=[ + "/usr/lib/python98.zip", + "/usr/lib/python9.8", + "/usr/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_symlink_normal_macos(self): + "Test a 'standard' install layout via symlink on macOS" + ns = MockPosixNamespace( + os_name="darwin", + PREFIX="/usr", + argv0="python", + ENV_PATH="/linkfrom:/usr/bin", + # real_executable on macOS matches the invocation path + real_executable="/linkfrom/python", + ) + ns.add_known_xfile("/linkfrom/python") + ns.add_known_xfile("/usr/bin/python") + ns.add_known_link("/linkfrom/python", "/usr/bin/python") + ns.add_known_file("/usr/lib/python9.8/os.py") + ns.add_known_dir("/usr/lib/python9.8/lib-dynload") + expected = dict( + executable="/linkfrom/python", + base_executable="/linkfrom/python", + prefix="/usr", + exec_prefix="/usr", + module_search_paths_set=1, + module_search_paths=[ + "/usr/lib/python98.zip", + "/usr/lib/python9.8", + "/usr/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_symlink_buildpath_macos(self): + """Test an in-build-tree layout via symlink on macOS. + + This layout is discovered from the presence of pybuilddir.txt, which + contains the relative path from the executable's directory to the + platstdlib path. + """ + ns = MockPosixNamespace( + os_name="darwin", + argv0=r"python", + ENV_PATH="/linkfrom:/usr/bin", + PREFIX="/usr/local", + # real_executable on macOS matches the invocation path + real_executable="/linkfrom/python", + ) + ns.add_known_xfile("/linkfrom/python") + ns.add_known_xfile("/home/cpython/python") + ns.add_known_link("/linkfrom/python", "/home/cpython/python") + ns.add_known_xfile("/usr/local/bin/python") + ns.add_known_file("/home/cpython/pybuilddir.txt", ["build/lib.macos-9.8"]) + ns.add_known_file("/home/cpython/Lib/os.py") + ns.add_known_dir("/home/cpython/lib-dynload") + expected = dict( + executable="/linkfrom/python", + prefix="/usr/local", + exec_prefix="/usr/local", + base_executable="/linkfrom/python", + build_prefix="/home/cpython", + _is_python_build=1, + module_search_paths_set=1, + module_search_paths=[ + "/usr/local/lib/python98.zip", + "/home/cpython/Lib", + "/home/cpython/build/lib.macos-9.8", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + +# ****************************************************************************** + +DEFAULT_NAMESPACE = dict( + PREFIX="", + EXEC_PREFIX="", + PYTHONPATH="", + VPATH="", + PLATLIBDIR="", + PYDEBUGEXT="", + VERSION_MAJOR=9, # fixed version number for ease + VERSION_MINOR=8, # of testing + PYWINVER=None, + EXE_SUFFIX=None, + + ENV_PATH="", + ENV_PYTHONHOME="", + ENV_PYTHONEXECUTABLE="", + ENV___PYVENV_LAUNCHER__="", + argv0="", + py_setpath="", + real_executable="", + executable_dir="", + library="", + winreg=None, + build_prefix=None, + venv_prefix=None, +) + +DEFAULT_CONFIG = dict( + home=None, + platlibdir=None, + pythonpath=None, + program_name=None, + prefix=None, + exec_prefix=None, + base_prefix=None, + base_exec_prefix=None, + executable=None, + base_executable="", + stdlib_dir=None, + platstdlib_dir=None, + module_search_paths=None, + module_search_paths_set=0, + pythonpath_env=None, + argv=None, + orig_argv=None, + + isolated=0, + use_environment=1, + use_site=1, +) + +class MockNTNamespace(dict): + def __init__(self, *a, argv0=None, config=None, **kw): + self.update(DEFAULT_NAMESPACE) + self["config"] = DEFAULT_CONFIG.copy() + self["os_name"] = "nt" + self["PLATLIBDIR"] = "DLLs" + self["PYWINVER"] = "9.8-XY" + self["VPATH"] = r"..\.." + super().__init__(*a, **kw) + if argv0: + self["config"]["orig_argv"] = [argv0] + if config: + self["config"].update(config) + self._files = {} + self._links = {} + self._dirs = set() + self._warnings = [] + + def add_known_file(self, path, lines=None): + self._files[path.casefold()] = list(lines or ()) + self.add_known_dir(path.rpartition("\\")[0]) + + def add_known_xfile(self, path): + self.add_known_file(path) + + def add_known_link(self, path, target): + self._links[path.casefold()] = target + + def add_known_dir(self, path): + p = path.rstrip("\\").casefold() + while p: + self._dirs.add(p) + p = p.rpartition("\\")[0] + + def __missing__(self, key): + try: + return getattr(self, key) + except AttributeError: + raise KeyError(key) from None + + def abspath(self, path): + if self.isabs(path): + return path + return self.joinpath("C:\\Absolute", path) + + def basename(self, path): + return path.rpartition("\\")[2] + + def dirname(self, path): + name = path.rstrip("\\").rpartition("\\")[0] + if name[1:] == ":": + return name + "\\" + return name + + def hassuffix(self, path, suffix): + return path.casefold().endswith(suffix.casefold()) + + def isabs(self, path): + return path[1:3] == ":\\" + + def isdir(self, path): + if verbose: + print("Check if", path, "is a dir") + return path.casefold() in self._dirs + + def isfile(self, path): + if verbose: + print("Check if", path, "is a file") + return path.casefold() in self._files + + def ismodule(self, path): + if verbose: + print("Check if", path, "is a module") + path = path.casefold() + return path in self._files and path.rpartition(".")[2] == "py".casefold() + + def isxfile(self, path): + if verbose: + print("Check if", path, "is a executable") + path = path.casefold() + return path in self._files and path.rpartition(".")[2] == "exe".casefold() + + def joinpath(self, *path): + return ntpath.normpath(ntpath.join(*path)) + + def readlines(self, path): + try: + return self._files[path.casefold()] + except KeyError: + raise FileNotFoundError(path) from None + + def realpath(self, path, _trail=None): + if verbose: + print("Read link from", path) + try: + link = self._links[path.casefold()] + except KeyError: + return path + if _trail is None: + _trail = set() + elif link.casefold() in _trail: + raise OSError("circular link") + _trail.add(link.casefold()) + return self.realpath(link, _trail) + + def warn(self, message): + self._warnings.append(message) + if verbose: + print(message) + + +class MockWinreg: + HKEY_LOCAL_MACHINE = "HKLM" + HKEY_CURRENT_USER = "HKCU" + + def __init__(self, keys): + self.keys = {k.casefold(): v for k, v in keys.items()} + self.open = {} + + def __repr__(self): + return "<MockWinreg>" + + def __eq__(self, other): + return isinstance(other, type(self)) + + def open_keys(self): + return list(self.open) + + def OpenKeyEx(self, hkey, subkey): + if verbose: + print(f"OpenKeyEx({hkey}, {subkey})") + key = f"{hkey}\\{subkey}".casefold() + if key in self.keys: + self.open[key] = self.open.get(key, 0) + 1 + return key + raise FileNotFoundError() + + def CloseKey(self, hkey): + if verbose: + print(f"CloseKey({hkey})") + hkey = hkey.casefold() + if hkey not in self.open: + raise RuntimeError("key is not open") + self.open[hkey] -= 1 + if not self.open[hkey]: + del self.open[hkey] + + def EnumKey(self, hkey, i): + if verbose: + print(f"EnumKey({hkey}, {i})") + hkey = hkey.casefold() + if hkey not in self.open: + raise RuntimeError("key is not open") + prefix = f'{hkey}\\' + subkeys = [k[len(prefix):] for k in sorted(self.keys) if k.startswith(prefix)] + subkeys[:] = [k for k in subkeys if '\\' not in k] + for j, n in enumerate(subkeys): + if j == i: + return n.removeprefix(prefix) + raise OSError("end of enumeration") + + def QueryValue(self, hkey): + if verbose: + print(f"QueryValue({hkey})") + hkey = hkey.casefold() + if hkey not in self.open: + raise RuntimeError("key is not open") + try: + return self.keys[hkey] + except KeyError: + raise OSError() + + +class MockPosixNamespace(dict): + def __init__(self, *a, argv0=None, config=None, **kw): + self.update(DEFAULT_NAMESPACE) + self["config"] = DEFAULT_CONFIG.copy() + self["os_name"] = "posix" + self["PLATLIBDIR"] = "lib" + super().__init__(*a, **kw) + if argv0: + self["config"]["orig_argv"] = [argv0] + if config: + self["config"].update(config) + self._files = {} + self._xfiles = set() + self._links = {} + self._dirs = set() + self._warnings = [] + + def add_known_file(self, path, lines=None): + self._files[path] = list(lines or ()) + self.add_known_dir(path.rpartition("/")[0]) + + def add_known_xfile(self, path): + self.add_known_file(path) + self._xfiles.add(path) + + def add_known_link(self, path, target): + self._links[path] = target + + def add_known_dir(self, path): + p = path.rstrip("/") + while p: + self._dirs.add(p) + p = p.rpartition("/")[0] + + def __missing__(self, key): + try: + return getattr(self, key) + except AttributeError: + raise KeyError(key) from None + + def abspath(self, path): + if self.isabs(path): + return path + return self.joinpath("/Absolute", path) + + def basename(self, path): + return path.rpartition("/")[2] + + def dirname(self, path): + return path.rstrip("/").rpartition("/")[0] + + def hassuffix(self, path, suffix): + return path.endswith(suffix) + + def isabs(self, path): + return path[0:1] == "/" + + def isdir(self, path): + if verbose: + print("Check if", path, "is a dir") + return path in self._dirs + + def isfile(self, path): + if verbose: + print("Check if", path, "is a file") + return path in self._files + + def ismodule(self, path): + if verbose: + print("Check if", path, "is a module") + return path in self._files and path.rpartition(".")[2] == "py" + + def isxfile(self, path): + if verbose: + print("Check if", path, "is an xfile") + return path in self._xfiles + + def joinpath(self, *path): + return posixpath.normpath(posixpath.join(*path)) + + def readlines(self, path): + try: + return self._files[path] + except KeyError: + raise FileNotFoundError(path) from None + + def realpath(self, path, _trail=None): + if verbose: + print("Read link from", path) + try: + link = self._links[path] + except KeyError: + return path + if _trail is None: + _trail = set() + elif link in _trail: + raise OSError("circular link") + _trail.add(link) + return self.realpath(link, _trail) + + def warn(self, message): + self._warnings.append(message) + if verbose: + print(message) + + +def diff_dict(before, after, prefix="global"): + diff = [] + for k in sorted(before): + if k[:2] == "__": + continue + if k == "config": + diff_dict(before[k], after[k], prefix="config") + continue + if k in after and after[k] != before[k]: + diff.append((k, before[k], after[k])) + if not diff: + return + max_k = max(len(k) for k, _, _ in diff) + indent = " " * (len(prefix) + 1 + max_k) + if verbose: + for k, b, a in diff: + if b: + print("{}.{} -{!r}\n{} +{!r}".format(prefix, k.ljust(max_k), b, indent, a)) + else: + print("{}.{} +{!r}".format(prefix, k.ljust(max_k), a)) + + +def dump_dict(before, after, prefix="global"): + if not verbose or not after: + return + max_k = max(len(k) for k in after) + for k, v in sorted(after.items(), key=lambda i: i[0]): + if k[:2] == "__": + continue + if k == "config": + dump_dict(before[k], after[k], prefix="config") + continue + try: + if v != before[k]: + print("{}.{} {!r} (was {!r})".format(prefix, k.ljust(max_k), v, before[k])) + continue + except KeyError: + pass + print("{}.{} {!r}".format(prefix, k.ljust(max_k), v)) + + +def getpath(ns, keys): + before = copy.deepcopy(ns) + failed = True + try: + exec(SOURCE, ns) + failed = False + finally: + if failed: + dump_dict(before, ns) + else: + diff_dict(before, ns) + return { + k: ns['config'].get(k, ns.get(k, ...)) + for k in keys + } diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 661c59d..a8d87e5 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -741,8 +741,9 @@ class TestNtpath(NtpathTestCase): # (or any other volume root). The drive-relative # locations below cannot then refer to mount points # - drive, path = ntpath.splitdrive(sys.executable) - with os_helper.change_cwd(ntpath.dirname(sys.executable)): + test_cwd = os.getenv("SystemRoot") + drive, path = ntpath.splitdrive(test_cwd) + with os_helper.change_cwd(test_cwd): self.assertFalse(ntpath.ismount(drive.lower())) self.assertFalse(ntpath.ismount(drive.upper())) diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index 5f06a0d..76d35da 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -11,6 +11,7 @@ from test.support import os_helper from test.support import socket_helper from test.support import captured_stderr from test.support.os_helper import TESTFN, EnvironmentVarGuard, change_cwd +import ast import builtins import encodings import glob @@ -300,7 +301,8 @@ class HelperFunctionsTests(unittest.TestCase): self.assertEqual(len(dirs), 2) self.assertEqual(dirs[0], 'xoxo') wanted = os.path.join('xoxo', 'lib', 'site-packages') - self.assertEqual(dirs[1], wanted) + self.assertEqual(os.path.normcase(dirs[1]), + os.path.normcase(wanted)) @unittest.skipUnless(HAS_USER_SITE, 'need user site') def test_no_home_directory(self): @@ -497,13 +499,14 @@ class StartupImportTests(unittest.TestCase): def test_startup_imports(self): # Get sys.path in isolated mode (python3 -I) - popen = subprocess.Popen([sys.executable, '-I', '-c', - 'import sys; print(repr(sys.path))'], + popen = subprocess.Popen([sys.executable, '-X', 'utf8', '-I', + '-c', 'import sys; print(repr(sys.path))'], stdout=subprocess.PIPE, - encoding='utf-8') + encoding='utf-8', + errors='surrogateescape') stdout = popen.communicate()[0] self.assertEqual(popen.returncode, 0, repr(stdout)) - isolated_paths = eval(stdout) + isolated_paths = ast.literal_eval(stdout) # bpo-27807: Even with -I, the site module executes all .pth files # found in sys.path (see site.addpackage()). Skip the test if at least @@ -515,14 +518,15 @@ class StartupImportTests(unittest.TestCase): # This tests checks which modules are loaded by Python when it # initially starts upon startup. - popen = subprocess.Popen([sys.executable, '-I', '-v', '-c', - 'import sys; print(set(sys.modules))'], + popen = subprocess.Popen([sys.executable, '-X', 'utf8', '-I', '-v', + '-c', 'import sys; print(set(sys.modules))'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - encoding='utf-8') + encoding='utf-8', + errors='surrogateescape') stdout, stderr = popen.communicate() self.assertEqual(popen.returncode, 0, (stdout, stderr)) - modules = eval(stdout) + modules = ast.literal_eval(stdout) self.assertIn('site', modules) diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index 9408657..506266d 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -102,6 +102,10 @@ class TestSysConfig(unittest.TestCase): def test_get_path(self): config_vars = get_config_vars() + if os.name == 'nt': + # On Windows, we replace the native platlibdir name with the + # default so that POSIX schemes resolve correctly + config_vars = config_vars | {'platlibdir': 'lib'} for scheme in _INSTALL_SCHEMES: for name in _INSTALL_SCHEMES[scheme]: expected = _INSTALL_SCHEMES[scheme][name].format(**config_vars) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 94d6265..de714de 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -15,7 +15,7 @@ import subprocess import sys import tempfile from test.support import (captured_stdout, captured_stderr, requires_zlib, - skip_if_broken_multiprocessing_synchronize) + skip_if_broken_multiprocessing_synchronize, verbose) from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree) import unittest import venv @@ -40,6 +40,8 @@ def check_output(cmd, encoding=None): encoding=encoding) out, err = p.communicate() if p.returncode: + if verbose and err: + print(err.decode('utf-8', 'backslashreplace')) raise subprocess.CalledProcessError( p.returncode, cmd, out, err) return out, err @@ -194,7 +196,7 @@ class BasicTest(BaseTest): ('base_exec_prefix', sys.base_exec_prefix)): cmd[2] = 'import sys; print(sys.%s)' % prefix out, err = check_output(cmd) - self.assertEqual(out.strip(), expected.encode()) + self.assertEqual(out.strip(), expected.encode(), prefix) if sys.platform == 'win32': ENV_SUBDIRS = ( |