diff -r 50a94e1cabe0 Lib/pathlib.py --- a/Lib/pathlib.py Mon May 09 00:14:22 2016 +0300 +++ b/Lib/pathlib.py Tue May 10 10:16:24 2016 +0300 @@ -35,6 +35,9 @@ else: # Internals # +CAN_MISSING, CAN_ALL_BUT_LAST, CAN_EXISTING = 'mfe' + + def _is_wildcard_pattern(pat): # Whether this pattern needs actual matching using fnmatch, or can # be looked up directly as a file. @@ -285,7 +288,7 @@ class _PosixFlavour(_Flavour): def casefold_parts(self, parts): return parts - def resolve(self, path): + def resolve(self, path, mode=CAN_EXISTING): sep = self.sep accessor = path._accessor seen = {} diff -r 50a94e1cabe0 Lib/posixpath.py --- a/Lib/posixpath.py Mon May 09 00:14:22 2016 +0300 +++ b/Lib/posixpath.py Tue May 10 10:16:24 2016 +0300 @@ -366,15 +366,17 @@ def abspath(path): # Return a canonical path (i.e. the absolute location of a file on the # filesystem). -def realpath(filename): +CAN_MISSING, CAN_ALL_BUT_LAST, CAN_EXISTING = 'mfe' + +def realpath(filename, mode=CAN_MISSING): """Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.""" - path, ok = _joinrealpath(filename[:0], filename, {}) + path = _joinrealpath(filename[:0], filename, {}, mode, True, False) return abspath(path) # Join two paths, normalizing and eliminating any symbolic links # encountered in the second path. -def _joinrealpath(path, rest, seen): +def _joinrealpath(path, rest, seen, mode, last, isdir): if isinstance(path, bytes): sep = b'/' curdir = b'.' @@ -389,7 +391,7 @@ def _joinrealpath(path, rest, seen): path = sep while rest: - name, _, rest = rest.partition(sep) + name, hassep, rest = rest.partition(sep) if not name or name == curdir: # current dir continue @@ -403,7 +405,21 @@ def _joinrealpath(path, rest, seen): path = pardir continue newpath = join(path, name) - if not islink(newpath): + try: + st = os.lstat(newpath) + except OSError as e: + if (mode == CAN_ALL_BUT_LAST + and isinstance(e, FileNotFoundError) + and last and not rest.strip(sep)): + return newpath + if mode != CAN_MISSING: + raise + path = newpath + continue + if not stat.S_ISLNK(st.st_mode): + if (mode != CAN_MISSING and not stat.S_ISDIR(st.st_mode) + and (isdir or hassep)): + os.lstat(newpath + sep) # raises NotADirectoryError path = newpath continue # Resolve the symbolic link @@ -414,15 +430,24 @@ def _joinrealpath(path, rest, seen): # use cached value continue # The symlink is not resolved, so we must have a symlink loop. - # Return already resolved part + rest of the path unchanged. - return join(newpath, rest), False + if mode != CAN_MISSING: + os.stat(newpath) # raises OSError(ELOOP) + path = newpath + continue seen[newpath] = None # not resolved symlink - path, ok = _joinrealpath(path, os.readlink(newpath), seen) - if not ok: - return join(path, rest), False + try: + link = os.readlink(newpath) + except OSError: + if mode != CAN_MISSING: + raise + path = newpath + else: + path = _joinrealpath(path, os.readlink(newpath), seen, mode, + last and not rest.strip(sep), + isdir or hassep) seen[newpath] = path # resolved symlink - return path, True + return path supports_unicode_filenames = (sys.platform == 'darwin') diff -r 50a94e1cabe0 Lib/test/test_posixpath.py --- a/Lib/test/test_posixpath.py Mon May 09 00:14:22 2016 +0300 +++ b/Lib/test/test_posixpath.py Tue May 10 10:16:24 2016 +0300 @@ -1,8 +1,10 @@ +import errno import os import posixpath import unittest import warnings from posixpath import realpath, abspath, dirname, basename +from posixpath import CAN_MISSING, CAN_ALL_BUT_LAST, CAN_EXISTING from test import support, test_genericpath try: @@ -332,7 +334,7 @@ class PosixPathTest(unittest.TestCase): self.assertEqual(realpath(ABSTFN+"1/../x"), dirname(ABSTFN) + "/x") os.symlink(ABSTFN+"x", ABSTFN+"y") self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "y"), - ABSTFN + "y") + ABSTFN + "x") self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "1"), ABSTFN + "1") @@ -458,6 +460,130 @@ class PosixPathTest(unittest.TestCase): safe_rmdir(ABSTFN + "/k") safe_rmdir(ABSTFN) + @unittest.skipUnless(hasattr(os, "symlink"), + "Missing symlink implementation") + @skip_if_ABSTFN_contains_backslash + def test_realpath_mode(self): + try: + os.mkdir(ABSTFN) + os.mkdir(ABSTFN + "/dir") + open(ABSTFN + "/file", "wb").close() + open(ABSTFN + "/dir/file2", "wb").close() + os.symlink("file", ABSTFN + "/link") + os.symlink("dir", ABSTFN + "/link2") + os.symlink("nonexisting", ABSTFN + "/broken") + os.symlink("cycle", ABSTFN + "/cycle") + def check(path, mode, expected, errno=None): + if isinstance(expected, str): + assert errno is None + self.assertEqual(realpath(path, mode), ABSTFN + expected) + else: + with self.assertRaises(expected) as cm: + realpath(path, mode) + if errno is not None: + self.assertEqual(cm.exception.errno, errno) + + with support.change_cwd(ABSTFN): + check("file", CAN_MISSING, "/file") + check("file", CAN_ALL_BUT_LAST, "/file") + check("file", CAN_EXISTING, "/file") + check("file/", CAN_MISSING, "/file") + check("file/", CAN_ALL_BUT_LAST, NotADirectoryError) + check("file/", CAN_EXISTING, NotADirectoryError) + check("file/file2", CAN_MISSING, "/file/file2") + check("file/file2", CAN_ALL_BUT_LAST, NotADirectoryError) + check("file/file2", CAN_EXISTING, NotADirectoryError) + check("file/.", CAN_MISSING, "/file") + check("file/.", CAN_ALL_BUT_LAST, NotADirectoryError) + check("file/.", CAN_EXISTING, NotADirectoryError) + check("file/../link", CAN_MISSING, "/file") + check("file/../link", CAN_ALL_BUT_LAST, NotADirectoryError) + check("file/../link", CAN_EXISTING, NotADirectoryError) + + check("dir", CAN_MISSING, "/dir") + check("dir", CAN_ALL_BUT_LAST, "/dir") + check("dir", CAN_EXISTING, "/dir") + check("dir/", CAN_MISSING, "/dir") + check("dir/", CAN_ALL_BUT_LAST, "/dir") + check("dir/", CAN_EXISTING, "/dir") + check("dir/file2", CAN_MISSING, "/dir/file2") + check("dir/file2", CAN_ALL_BUT_LAST, "/dir/file2") + check("dir/file2", CAN_EXISTING, "/dir/file2") + + check("link", CAN_MISSING, "/file") + check("link", CAN_ALL_BUT_LAST, "/file") + check("link", CAN_EXISTING, "/file") + check("link/", CAN_MISSING, "/file") + check("link/", CAN_ALL_BUT_LAST, NotADirectoryError) + check("link/", CAN_EXISTING, NotADirectoryError) + check("link/file2", CAN_MISSING, "/file/file2") + check("link/file2", CAN_ALL_BUT_LAST, NotADirectoryError) + check("link/file2", CAN_EXISTING, NotADirectoryError) + check("link/.", CAN_MISSING, "/file") + check("link/.", CAN_ALL_BUT_LAST, NotADirectoryError) + check("link/.", CAN_EXISTING, NotADirectoryError) + check("link/../link", CAN_MISSING, "/file") + check("link/../link", CAN_ALL_BUT_LAST, NotADirectoryError) + check("link/../link", CAN_EXISTING, NotADirectoryError) + + check("link2", CAN_MISSING, "/dir") + check("link2", CAN_ALL_BUT_LAST, "/dir") + check("link2", CAN_EXISTING, "/dir") + check("link2/", CAN_MISSING, "/dir") + check("link2/", CAN_ALL_BUT_LAST, "/dir") + check("link2/", CAN_EXISTING, "/dir") + check("link2/file2", CAN_MISSING, "/dir/file2") + check("link2/file2", CAN_ALL_BUT_LAST, "/dir/file2") + check("link2/file2", CAN_EXISTING, "/dir/file2") + + check("nonexisting", CAN_MISSING, "/nonexisting") + check("nonexisting", CAN_ALL_BUT_LAST, "/nonexisting") + check("nonexisting", CAN_EXISTING, FileNotFoundError) + check("nonexisting/", CAN_MISSING, "/nonexisting") + check("nonexisting/", CAN_ALL_BUT_LAST, "/nonexisting") + check("nonexisting/", CAN_EXISTING, FileNotFoundError) + check("nonexisting/file", CAN_MISSING, "/nonexisting/file") + check("nonexisting/file", CAN_ALL_BUT_LAST, FileNotFoundError) + check("nonexisting/file", CAN_EXISTING, FileNotFoundError) + check("nonexisting/../link", CAN_MISSING, "/file") + check("nonexisting/../link", CAN_ALL_BUT_LAST, FileNotFoundError) + check("nonexisting/../link", CAN_EXISTING, FileNotFoundError) + + check("broken", CAN_MISSING, "/nonexisting") + check("broken", CAN_ALL_BUT_LAST, "/nonexisting") + check("broken", CAN_EXISTING, FileNotFoundError) + check("broken/", CAN_MISSING, "/nonexisting") + check("broken/", CAN_ALL_BUT_LAST, "/nonexisting") + check("broken/", CAN_EXISTING, FileNotFoundError) + check("broken/file", CAN_MISSING, "/nonexisting/file") + check("broken/file", CAN_ALL_BUT_LAST, FileNotFoundError) + check("broken/file", CAN_EXISTING, FileNotFoundError) + check("broken/../link", CAN_MISSING, "/file") + check("broken/../link", CAN_ALL_BUT_LAST, FileNotFoundError) + check("broken/../link", CAN_EXISTING, FileNotFoundError) + + check("cycle", CAN_MISSING, "/cycle") + check("cycle", CAN_ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle", CAN_EXISTING, OSError, errno.ELOOP) + check("cycle/", CAN_MISSING, "/cycle") + check("cycle/", CAN_ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/", CAN_EXISTING, OSError, errno.ELOOP) + check("cycle/file", CAN_MISSING, "/cycle/file") + check("cycle/file", CAN_ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/file", CAN_EXISTING, OSError, errno.ELOOP) + check("cycle/../link", CAN_MISSING, "/file") + check("cycle/../link", CAN_ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/../link", CAN_EXISTING, OSError, errno.ELOOP) + finally: + support.unlink(ABSTFN + "/file") + support.unlink(ABSTFN + "/dir/file2") + support.unlink(ABSTFN + "/link") + support.unlink(ABSTFN + "/link2") + support.unlink(ABSTFN + "/broken") + support.unlink(ABSTFN + "/cycle") + support.rmdir(ABSTFN + "/dir") + support.rmdir(ABSTFN) + def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: