diff options
author | Steve Dower <steve.dower@python.org> | 2021-04-07 00:02:07 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-07 00:02:07 (GMT) |
commit | 04732ca993fa077a8b9640cc77fb2f152339585a (patch) | |
tree | e5aec9caf5770fd1aff72efc13640712155b8cbe /Lib | |
parent | b57e045320d1d2a70eab236b7d31a3ebb75037c3 (diff) | |
download | cpython-04732ca993fa077a8b9640cc77fb2f152339585a.zip cpython-04732ca993fa077a8b9640cc77fb2f152339585a.tar.gz cpython-04732ca993fa077a8b9640cc77fb2f152339585a.tar.bz2 |
bpo-43105: Importlib now resolves relative paths when creating module spec objects from file locations (GH-25121)
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/importlib/_bootstrap_external.py | 81 | ||||
-rw-r--r-- | Lib/test/test_import/__init__.py | 14 | ||||
-rw-r--r-- | Lib/test/test_importlib/test_spec.py | 19 | ||||
-rw-r--r-- | Lib/test/test_importlib/test_windows.py | 45 | ||||
-rw-r--r-- | Lib/test/test_site.py | 50 |
5 files changed, 130 insertions, 79 deletions
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index be11cfe..0b6cc8a 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -45,6 +45,7 @@ else: # Assumption made in _path_join() assert all(len(sep) == 1 for sep in path_separators) path_sep = path_separators[0] +path_sep_tuple = tuple(path_separators) path_separators = ''.join(path_separators) _pathseps_with_colon = {f':{s}' for s in path_separators} @@ -91,22 +92,49 @@ def _unpack_uint16(data): return int.from_bytes(data, 'little') -def _path_join(*path_parts): - """Replacement for os.path.join().""" - return path_sep.join([part.rstrip(path_separators) - for part in path_parts if part]) +if _MS_WINDOWS: + def _path_join(*path_parts): + """Replacement for os.path.join().""" + if not path_parts: + return "" + if len(path_parts) == 1: + return path_parts[0] + root = "" + path = [] + for new_root, tail in map(_os._path_splitroot, path_parts): + if new_root.startswith(path_sep_tuple) or new_root.endswith(path_sep_tuple): + root = new_root.rstrip(path_separators) or root + path = [path_sep + tail] + elif new_root.endswith(':'): + if root.casefold() != new_root.casefold(): + # Drive relative paths have to be resolved by the OS, so we reset the + # tail but do not add a path_sep prefix. + root = new_root + path = [tail] + else: + path.append(tail) + else: + root = new_root or root + path.append(tail) + path = [p.rstrip(path_separators) for p in path if p] + if len(path) == 1 and not path[0]: + # Avoid losing the root's trailing separator when joining with nothing + return root + path_sep + return root + path_sep.join(path) + +else: + def _path_join(*path_parts): + """Replacement for os.path.join().""" + return path_sep.join([part.rstrip(path_separators) + for part in path_parts if part]) def _path_split(path): """Replacement for os.path.split().""" - if len(path_separators) == 1: - front, _, tail = path.rpartition(path_sep) - return front, tail - for x in reversed(path): - if x in path_separators: - front, tail = path.rsplit(x, maxsplit=1) - return front, tail - return '', path + i = max(path.rfind(p) for p in path_separators) + if i < 0: + return '', path + return path[:i], path[i + 1:] def _path_stat(path): @@ -140,13 +168,18 @@ def _path_isdir(path): return _path_is_mode_type(path, 0o040000) -def _path_isabs(path): - """Replacement for os.path.isabs. +if _MS_WINDOWS: + def _path_isabs(path): + """Replacement for os.path.isabs.""" + if not path: + return False + root = _os._path_splitroot(path)[0].replace('/', '\\') + return len(root) > 1 and (root.startswith('\\\\') or root.endswith('\\')) - Considers a Windows drive-relative path (no drive, but starts with slash) to - still be "absolute". - """ - return path.startswith(path_separators) or path[1:3] in _pathseps_with_colon +else: + def _path_isabs(path): + """Replacement for os.path.isabs.""" + return path.startswith(path_separators) def _write_atomic(path, data, mode=0o666): @@ -707,6 +740,11 @@ def spec_from_file_location(name, location=None, *, loader=None, pass else: location = _os.fspath(location) + if not _path_isabs(location): + try: + location = _path_join(_os.getcwd(), location) + except OSError: + pass # If the location is on the filesystem, but doesn't actually exist, # we could return None here, indicating that the location is not @@ -1451,6 +1489,8 @@ class FileFinder: self._loaders = loaders # Base (directory) path self.path = path or '.' + if not _path_isabs(self.path): + self.path = _path_join(_os.getcwd(), self.path) self._path_mtime = -1 self._path_cache = set() self._relaxed_path_cache = set() @@ -1516,7 +1556,10 @@ class FileFinder: is_namespace = _path_isdir(base_path) # Check for a file w/ a proper suffix exists. for suffix, loader_class in self._loaders: - full_path = _path_join(self.path, tail_module + suffix) + try: + full_path = _path_join(self.path, tail_module + suffix) + except ValueError: + return None _bootstrap._verbose_message('trying {}', full_path, verbosity=2) if cache_module + suffix in cache: if _path_isfile(full_path): diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index dd832a1..8fe3e1d 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -906,7 +906,7 @@ class PycacheTests(unittest.TestCase): m = __import__(TESTFN) try: self.assertEqual(m.__file__, - os.path.join(os.curdir, os.path.relpath(pyc_file))) + os.path.join(os.getcwd(), os.curdir, os.path.relpath(pyc_file))) finally: os.remove(pyc_file) @@ -914,7 +914,7 @@ class PycacheTests(unittest.TestCase): # Modules now also have an __cached__ that points to the pyc file. m = __import__(TESTFN) pyc_file = importlib.util.cache_from_source(TESTFN + '.py') - self.assertEqual(m.__cached__, os.path.join(os.curdir, pyc_file)) + self.assertEqual(m.__cached__, os.path.join(os.getcwd(), os.curdir, pyc_file)) @skip_if_dont_write_bytecode def test___cached___legacy_pyc(self): @@ -930,7 +930,7 @@ class PycacheTests(unittest.TestCase): importlib.invalidate_caches() m = __import__(TESTFN) self.assertEqual(m.__cached__, - os.path.join(os.curdir, os.path.relpath(pyc_file))) + os.path.join(os.getcwd(), os.curdir, os.path.relpath(pyc_file))) @skip_if_dont_write_bytecode def test_package___cached__(self): @@ -950,10 +950,10 @@ class PycacheTests(unittest.TestCase): m = __import__('pep3147.foo') init_pyc = importlib.util.cache_from_source( os.path.join('pep3147', '__init__.py')) - self.assertEqual(m.__cached__, os.path.join(os.curdir, init_pyc)) + self.assertEqual(m.__cached__, os.path.join(os.getcwd(), os.curdir, init_pyc)) foo_pyc = importlib.util.cache_from_source(os.path.join('pep3147', 'foo.py')) self.assertEqual(sys.modules['pep3147.foo'].__cached__, - os.path.join(os.curdir, foo_pyc)) + os.path.join(os.getcwd(), os.curdir, foo_pyc)) def test_package___cached___from_pyc(self): # Like test___cached__ but ensuring __cached__ when imported from a @@ -977,10 +977,10 @@ class PycacheTests(unittest.TestCase): m = __import__('pep3147.foo') init_pyc = importlib.util.cache_from_source( os.path.join('pep3147', '__init__.py')) - self.assertEqual(m.__cached__, os.path.join(os.curdir, init_pyc)) + self.assertEqual(m.__cached__, os.path.join(os.getcwd(), os.curdir, init_pyc)) foo_pyc = importlib.util.cache_from_source(os.path.join('pep3147', 'foo.py')) self.assertEqual(sys.modules['pep3147.foo'].__cached__, - os.path.join(os.curdir, foo_pyc)) + os.path.join(os.getcwd(), os.curdir, foo_pyc)) def test_recompute_pyc_same_second(self): # Even when the source file doesn't change timestamp, a change in diff --git a/Lib/test/test_importlib/test_spec.py b/Lib/test/test_importlib/test_spec.py index b57eb6c..dcb0527 100644 --- a/Lib/test/test_importlib/test_spec.py +++ b/Lib/test/test_importlib/test_spec.py @@ -506,7 +506,7 @@ class FactoryTests: def setUp(self): self.name = 'spam' - self.path = 'spam.py' + self.path = os.path.abspath('spam.py') self.cached = self.util.cache_from_source(self.path) self.loader = TestLoader() self.fileloader = TestLoader(self.path) @@ -645,7 +645,7 @@ class FactoryTests: self.assertEqual(spec.loader, self.fileloader) self.assertEqual(spec.origin, self.path) self.assertIs(spec.loader_state, None) - self.assertEqual(spec.submodule_search_locations, ['']) + self.assertEqual(spec.submodule_search_locations, [os.getcwd()]) self.assertEqual(spec.cached, self.cached) self.assertTrue(spec.has_location) @@ -744,7 +744,7 @@ class FactoryTests: self.assertEqual(spec.loader, self.fileloader) self.assertEqual(spec.origin, self.path) self.assertIs(spec.loader_state, None) - self.assertEqual(spec.submodule_search_locations, ['']) + self.assertEqual(spec.submodule_search_locations, [os.getcwd()]) self.assertEqual(spec.cached, self.cached) self.assertTrue(spec.has_location) @@ -769,7 +769,7 @@ class FactoryTests: self.assertEqual(spec.loader, self.pkgloader) self.assertEqual(spec.origin, self.path) self.assertIs(spec.loader_state, None) - self.assertEqual(spec.submodule_search_locations, ['']) + self.assertEqual(spec.submodule_search_locations, [os.getcwd()]) self.assertEqual(spec.cached, self.cached) self.assertTrue(spec.has_location) @@ -817,6 +817,17 @@ class FactoryTests: self.assertEqual(spec.cached, self.cached) self.assertTrue(spec.has_location) + def test_spec_from_file_location_relative_path(self): + spec = self.util.spec_from_file_location(self.name, + os.path.basename(self.path), loader=self.fileloader) + + self.assertEqual(spec.name, self.name) + self.assertEqual(spec.loader, self.fileloader) + self.assertEqual(spec.origin, self.path) + self.assertIs(spec.loader_state, None) + self.assertIs(spec.submodule_search_locations, None) + self.assertEqual(spec.cached, self.cached) + self.assertTrue(spec.has_location) (Frozen_FactoryTests, Source_FactoryTests diff --git a/Lib/test/test_importlib/test_windows.py b/Lib/test/test_importlib/test_windows.py index 64ffe10..802fb34 100644 --- a/Lib/test/test_importlib/test_windows.py +++ b/Lib/test/test_importlib/test_windows.py @@ -126,3 +126,48 @@ class WindowsExtensionSuffixTests: (Frozen_WindowsExtensionSuffixTests, Source_WindowsExtensionSuffixTests ) = test_util.test_both(WindowsExtensionSuffixTests, machinery=machinery) + + +@unittest.skipUnless(sys.platform.startswith('win'), 'requires Windows') +class WindowsBootstrapPathTests(unittest.TestCase): + def check_join(self, expected, *inputs): + from importlib._bootstrap_external import _path_join + actual = _path_join(*inputs) + if expected.casefold() == actual.casefold(): + return + self.assertEqual(expected, actual) + + def test_path_join(self): + self.check_join(r"C:\A\B", "C:\\", "A", "B") + self.check_join(r"C:\A\B", "D:\\", "D", "C:\\", "A", "B") + self.check_join(r"C:\A\B", "C:\\", "A", "C:B") + self.check_join(r"C:\A\B", "C:\\", "A\\B") + self.check_join(r"C:\A\B", r"C:\A\B") + + self.check_join("D:A", r"D:", "A") + self.check_join("D:A", r"C:\B\C", "D:", "A") + self.check_join("D:A", r"C:\B\C", r"D:A") + + self.check_join(r"A\B\C", "A", "B", "C") + self.check_join(r"A\B\C", "A", r"B\C") + self.check_join(r"A\B/C", "A", "B/C") + self.check_join(r"A\B\C", "A/", "B\\", "C") + + # Dots are not normalised by this function + self.check_join(r"A\../C", "A", "../C") + self.check_join(r"A.\.\B", "A.", ".", "B") + + self.check_join(r"\\Server\Share\A\B\C", r"\\Server\Share", "A", "B", "C") + self.check_join(r"\\Server\Share\A\B\C", r"\\Server\Share", "D", r"\A", "B", "C") + self.check_join(r"\\Server\Share\A\B\C", r"\\Server2\Share2", "D", + r"\\Server\Share", "A", "B", "C") + self.check_join(r"\\Server\Share\A\B\C", r"\\Server", r"\Share", "A", "B", "C") + self.check_join(r"\\Server\Share", r"\\Server\Share") + self.check_join(r"\\Server\Share\\", r"\\Server\Share\\") + + # Handle edge cases with empty segments + self.check_join("C:\\A", "C:/A", "") + self.check_join("C:\\", "C:/", "") + self.check_join("C:", "C:", "") + self.check_join("//Server/Share\\", "//Server/Share/", "") + self.check_join("//Server/Share\\", "//Server/Share", "") diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index 6060288..9b4ab42 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -173,6 +173,7 @@ class HelperFunctionsTests(unittest.TestCase): pth_dir, pth_fn = self.make_pth("abc\x00def\n") with captured_stderr() as err_out: self.assertFalse(site.addpackage(pth_dir, pth_fn, set())) + self.maxDiff = None self.assertEqual(err_out.getvalue(), "") for path in sys.path: if isinstance(path, str): @@ -408,55 +409,6 @@ class ImportSideEffectTests(unittest.TestCase): """Restore sys.path""" sys.path[:] = self.sys_path - def test_abs_paths(self): - # Make sure all imported modules have their __file__ and __cached__ - # attributes as absolute paths. Arranging to put the Lib directory on - # PYTHONPATH would cause the os module to have a relative path for - # __file__ if abs_paths() does not get run. sys and builtins (the - # only other modules imported before site.py runs) do not have - # __file__ or __cached__ because they are built-in. - try: - parent = os.path.relpath(os.path.dirname(os.__file__)) - cwd = os.getcwd() - except ValueError: - # Failure to get relpath probably means we need to chdir - # to the same drive. - cwd, parent = os.path.split(os.path.dirname(os.__file__)) - with change_cwd(cwd): - env = os.environ.copy() - env['PYTHONPATH'] = parent - code = ('import os, sys', - # use ASCII to avoid locale issues with non-ASCII directories - 'os_file = os.__file__.encode("ascii", "backslashreplace")', - r'sys.stdout.buffer.write(os_file + b"\n")', - 'os_cached = os.__cached__.encode("ascii", "backslashreplace")', - r'sys.stdout.buffer.write(os_cached + b"\n")') - command = '\n'.join(code) - # First, prove that with -S (no 'import site'), the paths are - # relative. - proc = subprocess.Popen([sys.executable, '-S', '-c', command], - env=env, - stdout=subprocess.PIPE) - stdout, stderr = proc.communicate() - - self.assertEqual(proc.returncode, 0) - os__file__, os__cached__ = stdout.splitlines()[:2] - self.assertFalse(os.path.isabs(os__file__)) - self.assertFalse(os.path.isabs(os__cached__)) - # Now, with 'import site', it works. - proc = subprocess.Popen([sys.executable, '-c', command], - env=env, - stdout=subprocess.PIPE) - stdout, stderr = proc.communicate() - self.assertEqual(proc.returncode, 0) - os__file__, os__cached__ = stdout.splitlines()[:2] - self.assertTrue(os.path.isabs(os__file__), - "expected absolute path, got {}" - .format(os__file__.decode('ascii'))) - self.assertTrue(os.path.isabs(os__cached__), - "expected absolute path, got {}" - .format(os__cached__.decode('ascii'))) - def test_abs_paths_cached_None(self): """Test for __cached__ is None. |