diff -r b6fce698e467 Lib/pathlib.py --- a/Lib/pathlib.py Wed Dec 04 20:46:20 2013 +0100 +++ b/Lib/pathlib.py Thu Dec 05 17:36:55 2013 +0200 @@ -250,42 +250,45 @@ def resolve(self, path): sep = self.sep - def split(p): - return [x for x in p.split(sep) if x] - def absparts(p): - # Our own abspath(), since the posixpath one makes - # the mistake of "normalizing" the path without resolving the - # symlinks first. - if not p.startswith(sep): - return split(os.getcwd()) + split(p) - else: - return split(p) - parts = absparts(str(path))[::-1] accessor = path._accessor - resolved = cur = "" - symlinks = {} - while parts: - part = parts.pop() - cur = resolved + sep + part - if cur in symlinks and symlinks[cur] <= len(parts): - # We've already seen the symlink and there's not less - # work to do than the last time. - raise RuntimeError("Symlink loop from %r" % cur) - try: - target = accessor.readlink(cur) - except OSError as e: - if e.errno != EINVAL: - raise - # Not a symlink - resolved = cur - else: - # Take note of remaining work from this symlink - symlinks[cur] = len(parts) - if target.startswith(sep): - # Symlink points to absolute path - resolved = "" - parts.extend(split(target)[::-1]) - return resolved or sep + seen = {} + def _resolve(path, rest): + if rest.startswith(sep): + path = '' + + for name in rest.split(sep): + if not name or name == '.': + # current dir + continue + if name == '..': + # parent dir + path, _, _ = path.rpartition(sep) + continue + newpath = path + sep + name + if newpath in seen: + # Already seen this path + path = seen[newpath] + if path is not None: + # use cached value + continue + # The symlink is not resolved, so we must have a symlink loop. + raise RuntimeError("Symlink loop from %r" % newpath) + # Resolve the symbolic link + try: + target = accessor.readlink(newpath) + except OSError as e: + if e.errno != EINVAL: + raise + # Not a symlink + path = newpath + else: + seen[newpath] = None # not resolved symlink + path = _resolve(path, target) + seen[newpath] = path # resolved symlink + + return path + base = '' if path.is_absolute() else os.getcwd() + return _resolve(base, str(path)) or sep def is_reserved(self, parts): return False diff -r b6fce698e467 Lib/test/test_pathlib.py --- a/Lib/test/test_pathlib.py Wed Dec 04 20:46:20 2013 +0100 +++ b/Lib/test/test_pathlib.py Thu Dec 05 17:36:55 2013 +0200 @@ -1578,6 +1578,34 @@ # 'bin' self.assertIs(p.parts[2], q.parts[3]) + @with_symlinks + def test_recursion(self): + depths = (0, 1, 2, 3) + if os.name != 'nt': + depths += (10, 100) + def link(i): + return 'link%d' % i + old_path = os.getcwd() + for i in range(max(depths)): + os.symlink(os.path.join(link(i), link(i)), join(link(i + 1)), + target_is_directory=True) + link0 = join(link(0)) + for lastlink in BASE, '.', os.path.join('dirA', '..'): + os.symlink(lastlink, link0, target_is_directory=True) + # absolute path + for depth in depths: + with self.subTest(depth=depth, lastlink=lastlink): + P = self.cls(BASE) / link(depth) + self.assertEqual(str(P.resolve()), BASE) + os.chdir(BASE) + # relative path + for depth in depths: + with self.subTest(depth=depth, lastlink=lastlink): + P = self.cls(link(depth)) + self.assertEqual(str(P.resolve()), BASE) + os.chdir(old_path) + os.unlink(link0) + class PathTest(_BasePathTest, unittest.TestCase): cls = pathlib.Path