Skip to content

Commit e12af77

Browse files
committed
bpo-43105: importlib now loads dynamic modules using full paths
1 parent 5143fd1 commit e12af77

File tree

14 files changed

+3255
-2982
lines changed

14 files changed

+3255
-2982
lines changed

Doc/library/os.path.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,11 @@ the :mod:`glob` module.)
459459
.. versionchanged:: 3.6
460460
Accepts a :term:`path-like object`.
461461

462+
.. versionchanged:: 3.10
463+
On Windows, now relies on an operating system API. This has changed the
464+
results for some invalid UNC paths, but reliably supports paths with
465+
special prefixes such as ``\\?\``.
466+
462467

463468
.. function:: splitext(path)
464469

Doc/whatsnew/3.10.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,11 @@ Added :data:`~os.O_EVTONLY`, :data:`~os.O_FSYNC`, :data:`~os.O_SYMLINK`
793793
and :data:`~os.O_NOFOLLOW_ANY` for macOS.
794794
(Contributed by Dong-hee Na in :issue:`43106`.)
795795
796+
Changed :func:`os.path.splitdrive()` on Windows to rely on a native API.
797+
This has improved support for special prefixes, but may have changed the
798+
results for some invalid UNC paths.
799+
(Contributed by Steve Dower in :issue:`43105`.)
800+
796801
pathlib
797802
-------
798803

Lib/importlib/_bootstrap_external.py

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
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)
4849
path_separators = ''.join(path_separators)
4950
_pathseps_with_colon = {f':{s}' for s in path_separators}
5051

@@ -91,22 +92,48 @@ def _unpack_uint16(data):
9192
return int.from_bytes(data, 'little')
9293

9394

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])
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+
if len(path) == 1 and path[0] in path_sep_tuple:
120+
# Avoid losing the root's trailing separator when joining with nothing
121+
return root + path_sep
122+
return root + path_sep.join(p.rstrip(path_separators) for p in path if p)
123+
124+
else:
125+
def _path_join(*path_parts):
126+
"""Replacement for os.path.join()."""
127+
return path_sep.join([part.rstrip(path_separators)
128+
for part in path_parts if part])
98129

99130

100131
def _path_split(path):
101132
"""Replacement for os.path.split()."""
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
133+
i = max(path.rfind(p) for p in path_separators)
134+
if i < 0:
135+
return '', path
136+
return path[:i], path[i + 1:]
110137

111138

112139
def _path_stat(path):
@@ -140,13 +167,18 @@ def _path_isdir(path):
140167
return _path_is_mode_type(path, 0o040000)
141168

142169

143-
def _path_isabs(path):
144-
"""Replacement for os.path.isabs.
170+
if _MS_WINDOWS:
171+
def _path_isabs(path):
172+
"""Replacement for os.path.isabs."""
173+
if not path:
174+
return False
175+
root = _os._path_splitroot(path)[0].replace('/', '\\')
176+
return len(root) > 1 and (root.startswith('\\\\') or root.endswith('\\'))
145177

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
178+
else:
179+
def _path_isabs(path):
180+
"""Replacement for os.path.isabs."""
181+
return path.startswith(path_separators)
150182

151183

152184
def _write_atomic(path, data, mode=0o666):
@@ -707,6 +739,11 @@ def spec_from_file_location(name, location=None, *, loader=None,
707739
pass
708740
else:
709741
location = _os.fspath(location)
742+
if not _path_isabs(location):
743+
try:
744+
location = _path_join(_os.getcwd(), location)
745+
except OSError:
746+
pass
710747

711748
# If the location is on the filesystem, but doesn't actually exist,
712749
# we could return None here, indicating that the location is not
@@ -1451,6 +1488,8 @@ def __init__(self, path, *loader_details):
14511488
self._loaders = loaders
14521489
# Base (directory) path
14531490
self.path = path or '.'
1491+
if not _path_isabs(self.path):
1492+
self.path = _path_join(_os.getcwd(), self.path)
14541493
self._path_mtime = -1
14551494
self._path_cache = set()
14561495
self._relaxed_path_cache = set()
@@ -1516,7 +1555,10 @@ def find_spec(self, fullname, target=None):
15161555
is_namespace = _path_isdir(base_path)
15171556
# Check for a file w/ a proper suffix exists.
15181557
for suffix, loader_class in self._loaders:
1519-
full_path = _path_join(self.path, tail_module + suffix)
1558+
try:
1559+
full_path = _path_join(self.path, tail_module + suffix)
1560+
except ValueError:
1561+
return None
15201562
_bootstrap._verbose_message('trying {}', full_path, verbosity=2)
15211563
if cache_module + suffix in cache:
15221564
if _path_isfile(full_path):

Lib/ntpath.py

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,21 @@ def normcase(s):
5858
# volume), or if a pathname after the volume-letter-and-colon or UNC-resource
5959
# starts with a slash or backslash.
6060

61+
try:
62+
from nt import _path_splitroot
63+
except ImportError:
64+
_path_splitroot = None
65+
66+
6167
def isabs(s):
6268
"""Test whether a path is absolute"""
69+
if _path_splitroot:
70+
try:
71+
r = _path_splitroot(os.fsdecode(s))[0].replace(altsep, sep)
72+
except ValueError:
73+
pass
74+
else:
75+
return r.startswith(sep + sep) or r.endswith(sep)
6376
s = os.fspath(s)
6477
# Paths beginning with \\?\ are always absolute, but do not
6578
# necessarily contain a drive.
@@ -141,17 +154,31 @@ def splitdrive(p):
141154
142155
"""
143156
p = os.fspath(p)
144-
if len(p) >= 2:
145-
if isinstance(p, bytes):
146-
sep = b'\\'
147-
altsep = b'/'
148-
colon = b':'
157+
if isinstance(p, bytes):
158+
sep = b'\\'
159+
altsep = b'/'
160+
colon = b':'
161+
else:
162+
sep = '\\'
163+
altsep = '/'
164+
colon = ':'
165+
166+
if _path_splitroot:
167+
try:
168+
d = _path_splitroot(p)[0].rstrip('\\/')
169+
except ValueError:
170+
pass
149171
else:
150-
sep = '\\'
151-
altsep = '/'
152-
colon = ':'
172+
if not d and p[1:2] == colon:
173+
# Special case for invalid drive letters (such as a wildcard in glob.escape)
174+
d = p[:2]
175+
if isinstance(p, bytes):
176+
d = os.fsencode(d)
177+
return p[:len(d)], p[len(d):]
178+
179+
if len(p) >= 2:
153180
normp = p.replace(altsep, sep)
154-
if (normp[0:2] == sep*2) and (normp[2:3] != sep):
181+
if (normp[0:2] == sep*2):
155182
# is a UNC path:
156183
# vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
157184
# \\machine\mountpoint\directory\etc\...
@@ -160,10 +187,11 @@ def splitdrive(p):
160187
if index == -1:
161188
return p[:0], p
162189
index2 = normp.find(sep, index + 1)
163-
# a UNC path can't have two slashes in a row
164-
# (after the initial two)
190+
# a UNC path shouldn't have two slashes in a row
191+
# (after the initial two), but to be consistent with
192+
# the native function, we split before them.
165193
if index2 == index + 1:
166-
return p[:0], p
194+
return p[:index], p[index:]
167195
if index2 == -1:
168196
index2 = len(p)
169197
return p[:index2], p[index2:]
@@ -262,15 +290,25 @@ def lexists(path):
262290
def ismount(path):
263291
"""Test whether a path is a mount point (a drive root, the root of a
264292
share, or a mounted volume)"""
293+
try_fallback = True
294+
if _path_splitroot:
295+
try:
296+
root, tail = _path_splitroot(os.fsdecode(path))
297+
except ValueError:
298+
pass
299+
else:
300+
try_fallback = False
301+
if not tail:
302+
root = root.replace(altsep, sep)
303+
return root.startswith(sep + sep) or root.endswith(sep)
304+
305+
if try_fallback:
306+
root, rest = splitdrive(os.fsdecode(path))
307+
if root:
308+
return rest in ("/", "\\") or root.startswith(("\\\\", "//"))
309+
265310
path = os.fspath(path)
266311
seps = _get_bothseps(path)
267-
path = abspath(path)
268-
root, rest = splitdrive(path)
269-
if root and root[0] in seps:
270-
return (not rest) or (rest in seps)
271-
if rest in seps:
272-
return True
273-
274312
if _getvolumepathname:
275313
return path.rstrip(seps) == _getvolumepathname(path).rstrip(seps)
276314
else:
@@ -505,7 +543,7 @@ def _abspath_fallback(path):
505543
"""
506544

507545
path = os.fspath(path)
508-
if not isabs(path):
546+
if not splitdrive(path)[0]:
509547
if isinstance(path, bytes):
510548
cwd = os.getcwdb()
511549
else:
@@ -671,9 +709,9 @@ def realpath(path):
671709
return path
672710

673711

674-
# Win9x family and earlier have no Unicode filename support.
675-
supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and
676-
sys.getwindowsversion()[3] >= 2)
712+
# All supported platforms have Unicode filenames
713+
supports_unicode_filenames = True
714+
677715

678716
def relpath(path, start=None):
679717
"""Return a relative version of a 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.curdir, os.path.relpath(pyc_file)))
909+
os.path.join(os.getcwd(), 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.curdir, pyc_file))
917+
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), 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.curdir, os.path.relpath(pyc_file)))
933+
os.path.join(os.getcwd(), 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.curdir, init_pyc))
953+
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), 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.curdir, foo_pyc))
956+
os.path.join(os.getcwd(), 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.curdir, init_pyc))
980+
self.assertEqual(m.__cached__, os.path.join(os.getcwd(), 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.curdir, foo_pyc))
983+
os.path.join(os.getcwd(), 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: 15 additions & 4 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 = 'spam.py'
509+
self.path = os.path.abspath('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, [''])
648+
self.assertEqual(spec.submodule_search_locations, [os.getcwd()])
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, [''])
747+
self.assertEqual(spec.submodule_search_locations, [os.getcwd()])
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, [''])
772+
self.assertEqual(spec.submodule_search_locations, [os.getcwd()])
773773
self.assertEqual(spec.cached, self.cached)
774774
self.assertTrue(spec.has_location)
775775

@@ -817,6 +817,17 @@ 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)
820831

821832
(Frozen_FactoryTests,
822833
Source_FactoryTests

0 commit comments

Comments
 (0)