Skip to content

Commit 7acbaf9

Browse files
committed
Revert "bpo-43105: Importlib now resolves relative paths when creating module spec objects from file locations (pythonGH-25121)"
This reverts commit 04732ca.
1 parent a47e7fe commit 7acbaf9

File tree

8 files changed

+186
-304
lines changed

8 files changed

+186
-304
lines changed

Lib/importlib/_bootstrap_external.py

Lines changed: 19 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
# Assumption made in _path_join()
4646
assert all(len(sep) == 1 for sep in path_separators)
4747
path_sep = path_separators[0]
48-
path_sep_tuple = tuple(path_separators)
4948
path_separators = ''.join(path_separators)
5049
_pathseps_with_colon = {f':{s}' for s in path_separators}
5150

@@ -92,49 +91,22 @@ def _unpack_uint16(data):
9291
return int.from_bytes(data, 'little')
9392

9493

95-
if _MS_WINDOWS:
96-
def _path_join(*path_parts):
97-
"""Replacement for os.path.join()."""
98-
if not path_parts:
99-
return ""
100-
if len(path_parts) == 1:
101-
return path_parts[0]
102-
root = ""
103-
path = []
104-
for new_root, tail in map(_os._path_splitroot, path_parts):
105-
if new_root.startswith(path_sep_tuple) or new_root.endswith(path_sep_tuple):
106-
root = new_root.rstrip(path_separators) or root
107-
path = [path_sep + tail]
108-
elif new_root.endswith(':'):
109-
if root.casefold() != new_root.casefold():
110-
# Drive relative paths have to be resolved by the OS, so we reset the
111-
# tail but do not add a path_sep prefix.
112-
root = new_root
113-
path = [tail]
114-
else:
115-
path.append(tail)
116-
else:
117-
root = new_root or root
118-
path.append(tail)
119-
path = [p.rstrip(path_separators) for p in path if p]
120-
if len(path) == 1 and not path[0]:
121-
# Avoid losing the root's trailing separator when joining with nothing
122-
return root + path_sep
123-
return root + path_sep.join(path)
124-
125-
else:
126-
def _path_join(*path_parts):
127-
"""Replacement for os.path.join()."""
128-
return path_sep.join([part.rstrip(path_separators)
129-
for part in path_parts if part])
94+
def _path_join(*path_parts):
95+
"""Replacement for os.path.join()."""
96+
return path_sep.join([part.rstrip(path_separators)
97+
for part in path_parts if part])
13098

13199

132100
def _path_split(path):
133101
"""Replacement for os.path.split()."""
134-
i = max(path.rfind(p) for p in path_separators)
135-
if i < 0:
136-
return '', path
137-
return path[:i], path[i + 1:]
102+
if len(path_separators) == 1:
103+
front, _, tail = path.rpartition(path_sep)
104+
return front, tail
105+
for x in reversed(path):
106+
if x in path_separators:
107+
front, tail = path.rsplit(x, maxsplit=1)
108+
return front, tail
109+
return '', path
138110

139111

140112
def _path_stat(path):
@@ -168,18 +140,13 @@ def _path_isdir(path):
168140
return _path_is_mode_type(path, 0o040000)
169141

170142

171-
if _MS_WINDOWS:
172-
def _path_isabs(path):
173-
"""Replacement for os.path.isabs."""
174-
if not path:
175-
return False
176-
root = _os._path_splitroot(path)[0].replace('/', '\\')
177-
return len(root) > 1 and (root.startswith('\\\\') or root.endswith('\\'))
143+
def _path_isabs(path):
144+
"""Replacement for os.path.isabs.
178145
179-
else:
180-
def _path_isabs(path):
181-
"""Replacement for os.path.isabs."""
182-
return path.startswith(path_separators)
146+
Considers a Windows drive-relative path (no drive, but starts with slash) to
147+
still be "absolute".
148+
"""
149+
return path.startswith(path_separators) or path[1:3] in _pathseps_with_colon
183150

184151

185152
def _write_atomic(path, data, mode=0o666):
@@ -743,11 +710,6 @@ def spec_from_file_location(name, location=None, *, loader=None,
743710
pass
744711
else:
745712
location = _os.fspath(location)
746-
if not _path_isabs(location):
747-
try:
748-
location = _path_join(_os.getcwd(), location)
749-
except OSError:
750-
pass
751713

752714
# If the location is on the filesystem, but doesn't actually exist,
753715
# we could return None here, indicating that the location is not
@@ -1501,8 +1463,6 @@ def __init__(self, path, *loader_details):
15011463
self._loaders = loaders
15021464
# Base (directory) path
15031465
self.path = path or '.'
1504-
if not _path_isabs(self.path):
1505-
self.path = _path_join(_os.getcwd(), self.path)
15061466
self._path_mtime = -1
15071467
self._path_cache = set()
15081468
self._relaxed_path_cache = set()
@@ -1568,10 +1528,7 @@ def find_spec(self, fullname, target=None):
15681528
is_namespace = _path_isdir(base_path)
15691529
# Check for a file w/ a proper suffix exists.
15701530
for suffix, loader_class in self._loaders:
1571-
try:
1572-
full_path = _path_join(self.path, tail_module + suffix)
1573-
except ValueError:
1574-
return None
1531+
full_path = _path_join(self.path, tail_module + suffix)
15751532
_bootstrap._verbose_message('trying {}', full_path, verbosity=2)
15761533
if cache_module + suffix in cache:
15771534
if _path_isfile(full_path):

Lib/test/test_import/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -906,15 +906,15 @@ def test_missing_source_legacy(self):
906906
m = __import__(TESTFN)
907907
try:
908908
self.assertEqual(m.__file__,
909-
os.path.join(os.getcwd(), os.curdir, os.path.relpath(pyc_file)))
909+
os.path.join(os.curdir, os.path.relpath(pyc_file)))
910910
finally:
911911
os.remove(pyc_file)
912912

913913
def test___cached__(self):
914914
# Modules now also have an __cached__ that points to the pyc file.
915915
m = __import__(TESTFN)
916916
pyc_file = importlib.util.cache_from_source(TESTFN + '.py')
917-
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), os.curdir, pyc_file))
917+
self.assertEqual(m.__cached__, os.path.join(os.curdir, pyc_file))
918918

919919
@skip_if_dont_write_bytecode
920920
def test___cached___legacy_pyc(self):
@@ -930,7 +930,7 @@ def test___cached___legacy_pyc(self):
930930
importlib.invalidate_caches()
931931
m = __import__(TESTFN)
932932
self.assertEqual(m.__cached__,
933-
os.path.join(os.getcwd(), os.curdir, os.path.relpath(pyc_file)))
933+
os.path.join(os.curdir, os.path.relpath(pyc_file)))
934934

935935
@skip_if_dont_write_bytecode
936936
def test_package___cached__(self):
@@ -950,10 +950,10 @@ def cleanup():
950950
m = __import__('pep3147.foo')
951951
init_pyc = importlib.util.cache_from_source(
952952
os.path.join('pep3147', '__init__.py'))
953-
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), os.curdir, init_pyc))
953+
self.assertEqual(m.__cached__, os.path.join(os.curdir, init_pyc))
954954
foo_pyc = importlib.util.cache_from_source(os.path.join('pep3147', 'foo.py'))
955955
self.assertEqual(sys.modules['pep3147.foo'].__cached__,
956-
os.path.join(os.getcwd(), os.curdir, foo_pyc))
956+
os.path.join(os.curdir, foo_pyc))
957957

958958
def test_package___cached___from_pyc(self):
959959
# Like test___cached__ but ensuring __cached__ when imported from a
@@ -977,10 +977,10 @@ def cleanup():
977977
m = __import__('pep3147.foo')
978978
init_pyc = importlib.util.cache_from_source(
979979
os.path.join('pep3147', '__init__.py'))
980-
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), os.curdir, init_pyc))
980+
self.assertEqual(m.__cached__, os.path.join(os.curdir, init_pyc))
981981
foo_pyc = importlib.util.cache_from_source(os.path.join('pep3147', 'foo.py'))
982982
self.assertEqual(sys.modules['pep3147.foo'].__cached__,
983-
os.path.join(os.getcwd(), os.curdir, foo_pyc))
983+
os.path.join(os.curdir, foo_pyc))
984984

985985
def test_recompute_pyc_same_second(self):
986986
# Even when the source file doesn't change timestamp, a change in

Lib/test/test_importlib/test_spec.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ class FactoryTests:
506506

507507
def setUp(self):
508508
self.name = 'spam'
509-
self.path = os.path.abspath('spam.py')
509+
self.path = 'spam.py'
510510
self.cached = self.util.cache_from_source(self.path)
511511
self.loader = TestLoader()
512512
self.fileloader = TestLoader(self.path)
@@ -645,7 +645,7 @@ def test_spec_from_loader_is_package_true_with_fileloader(self):
645645
self.assertEqual(spec.loader, self.fileloader)
646646
self.assertEqual(spec.origin, self.path)
647647
self.assertIs(spec.loader_state, None)
648-
self.assertEqual(spec.submodule_search_locations, [os.getcwd()])
648+
self.assertEqual(spec.submodule_search_locations, [''])
649649
self.assertEqual(spec.cached, self.cached)
650650
self.assertTrue(spec.has_location)
651651

@@ -744,7 +744,7 @@ def test_spec_from_file_location_smsl_empty(self):
744744
self.assertEqual(spec.loader, self.fileloader)
745745
self.assertEqual(spec.origin, self.path)
746746
self.assertIs(spec.loader_state, None)
747-
self.assertEqual(spec.submodule_search_locations, [os.getcwd()])
747+
self.assertEqual(spec.submodule_search_locations, [''])
748748
self.assertEqual(spec.cached, self.cached)
749749
self.assertTrue(spec.has_location)
750750

@@ -769,7 +769,7 @@ def test_spec_from_file_location_smsl_default(self):
769769
self.assertEqual(spec.loader, self.pkgloader)
770770
self.assertEqual(spec.origin, self.path)
771771
self.assertIs(spec.loader_state, None)
772-
self.assertEqual(spec.submodule_search_locations, [os.getcwd()])
772+
self.assertEqual(spec.submodule_search_locations, [''])
773773
self.assertEqual(spec.cached, self.cached)
774774
self.assertTrue(spec.has_location)
775775

@@ -817,17 +817,6 @@ def is_package(self, name):
817817
self.assertEqual(spec.cached, self.cached)
818818
self.assertTrue(spec.has_location)
819819

820-
def test_spec_from_file_location_relative_path(self):
821-
spec = self.util.spec_from_file_location(self.name,
822-
os.path.basename(self.path), loader=self.fileloader)
823-
824-
self.assertEqual(spec.name, self.name)
825-
self.assertEqual(spec.loader, self.fileloader)
826-
self.assertEqual(spec.origin, self.path)
827-
self.assertIs(spec.loader_state, None)
828-
self.assertIs(spec.submodule_search_locations, None)
829-
self.assertEqual(spec.cached, self.cached)
830-
self.assertTrue(spec.has_location)
831820

832821
(Frozen_FactoryTests,
833822
Source_FactoryTests

Lib/test/test_importlib/test_windows.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -144,48 +144,3 @@ def test_tagged_suffix(self):
144144
(Frozen_WindowsExtensionSuffixTests,
145145
Source_WindowsExtensionSuffixTests
146146
) = test_util.test_both(WindowsExtensionSuffixTests, machinery=machinery)
147-
148-
149-
@unittest.skipUnless(sys.platform.startswith('win'), 'requires Windows')
150-
class WindowsBootstrapPathTests(unittest.TestCase):
151-
def check_join(self, expected, *inputs):
152-
from importlib._bootstrap_external import _path_join
153-
actual = _path_join(*inputs)
154-
if expected.casefold() == actual.casefold():
155-
return
156-
self.assertEqual(expected, actual)
157-
158-
def test_path_join(self):
159-
self.check_join(r"C:\A\B", "C:\\", "A", "B")
160-
self.check_join(r"C:\A\B", "D:\\", "D", "C:\\", "A", "B")
161-
self.check_join(r"C:\A\B", "C:\\", "A", "C:B")
162-
self.check_join(r"C:\A\B", "C:\\", "A\\B")
163-
self.check_join(r"C:\A\B", r"C:\A\B")
164-
165-
self.check_join("D:A", r"D:", "A")
166-
self.check_join("D:A", r"C:\B\C", "D:", "A")
167-
self.check_join("D:A", r"C:\B\C", r"D:A")
168-
169-
self.check_join(r"A\B\C", "A", "B", "C")
170-
self.check_join(r"A\B\C", "A", r"B\C")
171-
self.check_join(r"A\B/C", "A", "B/C")
172-
self.check_join(r"A\B\C", "A/", "B\\", "C")
173-
174-
# Dots are not normalised by this function
175-
self.check_join(r"A\../C", "A", "../C")
176-
self.check_join(r"A.\.\B", "A.", ".", "B")
177-
178-
self.check_join(r"\\Server\Share\A\B\C", r"\\Server\Share", "A", "B", "C")
179-
self.check_join(r"\\Server\Share\A\B\C", r"\\Server\Share", "D", r"\A", "B", "C")
180-
self.check_join(r"\\Server\Share\A\B\C", r"\\Server2\Share2", "D",
181-
r"\\Server\Share", "A", "B", "C")
182-
self.check_join(r"\\Server\Share\A\B\C", r"\\Server", r"\Share", "A", "B", "C")
183-
self.check_join(r"\\Server\Share", r"\\Server\Share")
184-
self.check_join(r"\\Server\Share\\", r"\\Server\Share\\")
185-
186-
# Handle edge cases with empty segments
187-
self.check_join("C:\\A", "C:/A", "")
188-
self.check_join("C:\\", "C:/", "")
189-
self.check_join("C:", "C:", "")
190-
self.check_join("//Server/Share\\", "//Server/Share/", "")
191-
self.check_join("//Server/Share\\", "//Server/Share", "")

Lib/test/test_site.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ def test_addpackage_import_bad_pth_file(self):
175175
pth_dir, pth_fn = self.make_pth("abc\x00def\n")
176176
with captured_stderr() as err_out:
177177
self.assertFalse(site.addpackage(pth_dir, pth_fn, set()))
178-
self.maxDiff = None
179178
self.assertEqual(err_out.getvalue(), "")
180179
for path in sys.path:
181180
if isinstance(path, str):
@@ -411,6 +410,55 @@ def tearDown(self):
411410
"""Restore sys.path"""
412411
sys.path[:] = self.sys_path
413412

413+
def test_abs_paths(self):
414+
# Make sure all imported modules have their __file__ and __cached__
415+
# attributes as absolute paths. Arranging to put the Lib directory on
416+
# PYTHONPATH would cause the os module to have a relative path for
417+
# __file__ if abs_paths() does not get run. sys and builtins (the
418+
# only other modules imported before site.py runs) do not have
419+
# __file__ or __cached__ because they are built-in.
420+
try:
421+
parent = os.path.relpath(os.path.dirname(os.__file__))
422+
cwd = os.getcwd()
423+
except ValueError:
424+
# Failure to get relpath probably means we need to chdir
425+
# to the same drive.
426+
cwd, parent = os.path.split(os.path.dirname(os.__file__))
427+
with change_cwd(cwd):
428+
env = os.environ.copy()
429+
env['PYTHONPATH'] = parent
430+
code = ('import os, sys',
431+
# use ASCII to avoid locale issues with non-ASCII directories
432+
'os_file = os.__file__.encode("ascii", "backslashreplace")',
433+
r'sys.stdout.buffer.write(os_file + b"\n")',
434+
'os_cached = os.__cached__.encode("ascii", "backslashreplace")',
435+
r'sys.stdout.buffer.write(os_cached + b"\n")')
436+
command = '\n'.join(code)
437+
# First, prove that with -S (no 'import site'), the paths are
438+
# relative.
439+
proc = subprocess.Popen([sys.executable, '-S', '-c', command],
440+
env=env,
441+
stdout=subprocess.PIPE)
442+
stdout, stderr = proc.communicate()
443+
444+
self.assertEqual(proc.returncode, 0)
445+
os__file__, os__cached__ = stdout.splitlines()[:2]
446+
self.assertFalse(os.path.isabs(os__file__))
447+
self.assertFalse(os.path.isabs(os__cached__))
448+
# Now, with 'import site', it works.
449+
proc = subprocess.Popen([sys.executable, '-c', command],
450+
env=env,
451+
stdout=subprocess.PIPE)
452+
stdout, stderr = proc.communicate()
453+
self.assertEqual(proc.returncode, 0)
454+
os__file__, os__cached__ = stdout.splitlines()[:2]
455+
self.assertTrue(os.path.isabs(os__file__),
456+
"expected absolute path, got {}"
457+
.format(os__file__.decode('ascii')))
458+
self.assertTrue(os.path.isabs(os__cached__),
459+
"expected absolute path, got {}"
460+
.format(os__cached__.decode('ascii')))
461+
414462
def test_abs_paths_cached_None(self):
415463
"""Test for __cached__ is None.
416464

0 commit comments

Comments
 (0)