diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 11bb297e16bf4e0..08f9dae30d223a2 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -527,8 +527,6 @@ def abspath(path): except (OSError, ValueError): return _abspath_fallback(path) -# realpath is a no-op on systems without islink support -realpath = abspath # Win9x family and earlier have no Unicode filename support. supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and sys.getwindowsversion()[3] >= 2) @@ -640,23 +638,65 @@ def commonpath(paths): genericpath._check_arg_types('commonpath', *paths) raise +MAX_PATH = 260 -# determine if two files are in fact the same file try: - # GetFinalPathNameByHandle is available starting with Windows 6.0. - # Windows XP and non-Windows OS'es will mock _getfinalpathname. - if sys.getwindowsversion()[:2] >= (6, 0): - from nt import _getfinalpathname - else: - raise ImportError -except (AttributeError, ImportError): - # On Windows XP and earlier, two files are the same if their absolute - # pathnames are the same. - # Non-Windows operating systems fake this method with an XP - # approximation. - def _getfinalpathname(f): - return normcase(abspath(f)) + from nt import _getfinalpathname + + # determine if two files are in fact the same file + def realpath(filename): + filename = os.fspath(filename) + is_str = isinstance(filename, str) + if not is_str: + filename = os.fsdecode(filename) + extended_path_prefix = '\\\\?\\' + is_extended_path = filename.startswith(extended_path_prefix) + unresolved = filename if is_extended_path else abspath(filename) + resolved_parts = [] + while True: + try: + resolved_parts.append(_getfinalpathname(unresolved)) + break + except OSError: + unresolved, tail = split(unresolved) + if not tail: + resolved_parts.append(unresolved) + break + resolved_parts.append(tail) + resolved = join(*reversed(resolved_parts)) + # try to convert extended path to normal if + # initial path did not use \\?\ prefix and result uses it + if not is_extended_path and resolved.startswith(extended_path_prefix): + resolved = _extended_to_normal(resolved) + return resolved if is_str else os.fsencode(resolved) + + def _extended_to_normal(path): + letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + if path[5:7] == ":\\" and path[4].upper() in letters: + # extended path with \\?\ prefix + # 4 is len('\\?\') + normal_path = normpath(path[4:]) + elif path[:8].upper() == '\\\\?\\UNC\\': + # UNC path with \\?\ prefix - drop prefix + # 7 is len('\\?\UNC') + normal_path = normpath('\\' + path[7:]) + else: + # not a UNC or drive-letter path + # return path as-is + return path + if (len(normal_path) < MAX_PATH and + normal_path == _getfullpathname(normal_path)): + return normal_path + return path +except ImportError: + def realpath(filename): + filename = os.fspath(filename) + extended_path_prefix = ( + '\\\\?\\' if isinstance(filename, str) else b'\\\\?\\' + ) + is_extended_path = filename.startswith(extended_path_prefix) + return filename if is_extended_path else abspath(filename) try: # The genericpath.isdir implementation uses os.stat and checks the mode diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 223e50f12c6d568..efeb0bdf6cf0658 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -5,7 +5,7 @@ import warnings from test.support import TestFailed, FakePath from test import support, test_genericpath -from tempfile import TemporaryFile +from tempfile import TemporaryFile, TemporaryDirectory try: import nt @@ -14,6 +14,12 @@ # but for those that require it we import here. nt = None +try: + import _winapi +except ImportError: + # realpath tests require _winapi + _winapi = None + def tester(fn, wantResult): fn = fn.replace("\\", "\\\\") gotResult = eval(fn) @@ -277,6 +283,52 @@ def test_expanduser(self): tester('ntpath.expanduser("~/foo/bar")', 'C:\\idle\\eric/foo/bar') + @unittest.skipUnless(nt and _winapi, "realpath requires 'nt' and '_winapi' modules") + def test_realpath(self): + def to_unc(p): + drive, rest = ntpath.splitdrive(file2) + return ntpath.join(f"\\\\localhost\\{drive[0]}$", rest) + def s2b(s): + return bytes(s, "utf-8") + + with TemporaryDirectory() as d: + f = ntpath.join(d, "f") + os.mkdir(f) + f2 = ntpath.join(d, "g") + _winapi.CreateJunction(f, f2) + try: + # realpath for original path and junction is the same + self.assertEqualCI(ntpath.realpath(f), ntpath.realpath(f2)) + self.assertEqualCI(ntpath.realpath(s2b(f)), ntpath.realpath(s2b(f2))) + + # realpath for UNC path is the same + file = ntpath.join(f, "file1") + open(file, "w+").close() + file2 = ntpath.join(f2, "file1") + unc1 = to_unc(file) + unc2 = to_unc(file2) + self.assertEqualCI(ntpath.realpath(unc1), ntpath.realpath(unc2)) + self.assertEqualCI(ntpath.realpath(s2b(unc1)), ntpath.realpath(s2b(unc2))) + + # realpath for non-existent file F in symlinked folder + # is original folder + F + file = ntpath.join(f, "missing") + file2 = ntpath.join(f2, "missing") + self.assertEqualCI(ntpath.realpath(file), ntpath.realpath(file2)) + self.assertEqualCI(ntpath.realpath(s2b(file)), ntpath.realpath(s2b(file2))) + + # realpath for short names used for non 8.3 directory names + dir = ntpath.join(f, "somelongname") + os.mkdir(dir) + file = ntpath.join(dir, "f") + open(file, "w+").close() + file_in_f2 = ntpath.join(f2, "somelo~1", "f") + self.assertEqualCI(ntpath.realpath(file), ntpath.realpath(file_in_f2)) + self.assertEqualCI(ntpath.realpath(s2b(file)), ntpath.realpath(s2b(file_in_f2))) + finally: + os.unlink(f2) + + @unittest.skipUnless(nt, "abspath requires 'nt' module") def test_abspath(self): tester('ntpath.abspath("C:\\")', "C:\\") diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 6f22e5378ff22cb..72eb7e1aec057e8 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1756,7 +1756,7 @@ def test_move_dangling_symlink(self): self.assertTrue(os.path.islink(dst_link)) # On Windows, os.path.realpath does not follow symlinks (issue #9949) if os.name == 'nt': - self.assertEqual(os.path.realpath(src), os.readlink(dst_link)) + self.assertEqual(os.path.realpath(src), os.path.realpath(os.readlink(dst_link))) else: self.assertEqual(os.path.realpath(src), os.path.realpath(dst_link)) diff --git a/Misc/NEWS.d/next/Library/2018-12-19-15-26-35.bpo-14094.8Hotek.rst b/Misc/NEWS.d/next/Library/2018-12-19-15-26-35.bpo-14094.8Hotek.rst new file mode 100644 index 000000000000000..85cfe82fae62728 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-12-19-15-26-35.bpo-14094.8Hotek.rst @@ -0,0 +1 @@ +ntpath.realpath() now uses ``GetFinalPathNameByHandle()``.