Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4b29e2e
Add `pathlib._VirtualPath`
barneygale Jun 23, 2023
8ce0139
Add tests for `pathlib._VirtualPath`
barneygale Jul 2, 2023
b850d11
Fix tests on Windows
barneygale Jul 2, 2023
39bf6b3
Fix tests on Windows (take 2)
barneygale Jul 3, 2023
0515dea
Fix tests on Windows (take 3)
barneygale Jul 3, 2023
596016f
Fix tests on Windows (take 4)
barneygale Jul 3, 2023
1a6122b
Add `tarfile.TarPath`
barneygale Jul 3, 2023
6833ed8
Add docs for `tarfile.TarPath`
barneygale Jul 3, 2023
4d2e8a9
Add tests for `tarfile.TarPath`
barneygale Jul 3, 2023
e4daac9
Merge branch 'main' into gh-89812-omgtarpath
barneygale Jul 3, 2023
e3f2509
Merge branch 'main' into gh-89812-omgtarpath
barneygale Jul 12, 2023
508cabe
Undo changes to tarfile.
barneygale Jul 12, 2023
2c56591
`_VirtualPath` --> `_PathBase`
barneygale Jul 12, 2023
42fe91a
Merge branch 'main' into gh-89812-omgtarpath
barneygale Jul 19, 2023
8944098
Apply suggestions from code review
barneygale Aug 28, 2023
b61141a
Improve _PathBase docstring
barneygale Aug 28, 2023
1e462b0
Explain use of nullcontext() in comment
barneygale Aug 28, 2023
6318eb7
Merge branch 'main' into gh-89812-omgtarpath
barneygale Aug 28, 2023
d321cad
Align and test Path/PathBase docstrings
barneygale Aug 28, 2023
acfc1b0
Revise `_PathBase.is_junction()`
barneygale Aug 28, 2023
bc82225
Make is_junction() code more consistent with other is_*() methods.
barneygale Aug 28, 2023
9b6377a
Merge branch 'main' into gh-89812-omgtarpath
barneygale Sep 2, 2023
c3127b8
Improve `UnsupportedOperation` exception message.
barneygale Sep 2, 2023
3540ae1
Slightly improve symlink loop code, exception message.
barneygale Sep 2, 2023
c9f0f20
Restore deleted comment in `cwd()`, expand `_scandir()` comment.
barneygale Sep 2, 2023
0ee10ca
Make `_PathBase.is_junction()` immediately return false.
barneygale Sep 2, 2023
17eee2f
MAX_SYMLINKS --> _MAX_SYMLINKS
barneygale Sep 9, 2023
c7c46bc
`return self._unsupported()` --> `self._unsupported()`
barneygale Sep 9, 2023
a51d7a0
WIP
barneygale Sep 15, 2023
7e3729e
Undo test change.
barneygale Sep 23, 2023
b945cf8
Merge branch 'main' into gh-89812-omgtarpath
barneygale Sep 26, 2023
703fe5c
Ensure `..` segments are resolved in non-strict mode
barneygale Sep 26, 2023
e5e5be5
Move symlink loop resolution test from `PosixPathTest` to `DummyPathT…
barneygale Sep 26, 2023
38769a0
Add `PathBase._split_stack()` helper method.
barneygale Sep 26, 2023
7c78952
Use path object as stat/link target cache key
barneygale Sep 26, 2023
fe57725
Optimise resolve(): skip stat() in non-strict mode if readlink() is u…
barneygale Sep 27, 2023
cf9c8b6
Address code review comments
barneygale Sep 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 22 additions & 20 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,15 @@ def readlink(self):
Return the path to which the symbolic link points.
"""
self._unsupported("readlink")
readlink._unsupported = True

def _split_stack(self):
"""
Split the path into a 2-tuple (anchor, parts), where *anchor* is the
uppermost parent of the path (equivalent to path.parents[-1]), and
*parts* is a reversed list of parts following the anchor.
"""
return self._from_parsed_parts(self.drive, self.root, []), self._tail[::-1]

def resolve(self, strict=False):
"""
Expand All @@ -1266,13 +1275,11 @@ def resolve(self, strict=False):
except UnsupportedOperation:
path = self

def split(path):
return path._from_parsed_parts(path.drive, path.root, []), path._tail[::-1]

querying = strict or not getattr(self.readlink, '_unsupported', False)
link_count = 0
stat_cache = {}
target_cache = {}
path, parts = split(path)
path, parts = path._split_stack()
while parts:
part = parts.pop()
if part == '..':
Expand All @@ -1284,38 +1291,33 @@ def split(path):
# Delete '..' segment and its predecessor
path = path.parent
continue
path = path._make_child_relpath(part)
else:
lookup_path = path
path = path._make_child_relpath(part)
lookup_path = path
path = path._make_child_relpath(part)
if querying and part != '..':
path._resolving = True
path_str = str(path)
try:
st = stat_cache.get(path_str)
st = stat_cache.get(path)
if st is None:
st = stat_cache[path_str] = path.stat(follow_symlinks=False)
st = stat_cache[path] = path.stat(follow_symlinks=False)
if S_ISLNK(st.st_mode):
# Like Linux and macOS, raise OSError(errno.ELOOP) if too many symlinks are
# encountered during resolution.
link_count += 1
if link_count >= _MAX_SYMLINKS:
raise OSError(ELOOP, "Too many symbolic links in path", path_str)
target = target_cache.get(path_str)
raise OSError(ELOOP, "Too many symbolic links in path", str(path))
target = target_cache.get(path)
if target is None:
target = target_cache[path_str] = path.readlink()
target, target_parts = split(target)
target = target_cache[path] = path.readlink()
target, target_parts = target._split_stack()
path = target if target.root else lookup_path
parts.extend(target_parts)
elif parts and not S_ISDIR(st.st_mode):
raise NotADirectoryError(ENOTDIR, "Not a directory", path_str)
raise NotADirectoryError(ENOTDIR, "Not a directory", str(path))
except OSError:
if strict:
raise
else:
# Append remaining path segments without further processing.
for part in reversed(parts):
path = path._make_child_relpath(part)
break
querying = False
path._resolving = False
return path

Expand Down
62 changes: 32 additions & 30 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2334,6 +2334,38 @@ def test_resolve_dot(self):
# Non-strict
self.assertEqual(r.resolve(strict=False), p / '3' / '4')

def _check_symlink_loop(self, *args):
path = self.cls(*args)
with self.assertRaises(OSError) as cm:
path.resolve(strict=True)
self.assertEqual(cm.exception.errno, errno.ELOOP)

def test_resolve_loop(self):
if not self.can_symlink:
self.skipTest("symlinks required")
if os.name == 'nt' and issubclass(self.cls, pathlib.Path):
self.skipTest("symlink loops work differently with concrete Windows paths")
# Loops with relative symlinks.
self.cls(BASE, 'linkX').symlink_to('linkX/inside')
self._check_symlink_loop(BASE, 'linkX')
self.cls(BASE, 'linkY').symlink_to('linkY')
self._check_symlink_loop(BASE, 'linkY')
self.cls(BASE, 'linkZ').symlink_to('linkZ/../linkZ')
self._check_symlink_loop(BASE, 'linkZ')
# Non-strict
p = self.cls(BASE, 'linkZ', 'foo')
self.assertEqual(p.resolve(strict=False), p)
# Loops with absolute symlinks.
self.cls(BASE, 'linkU').symlink_to(join('linkU/inside'))
self._check_symlink_loop(BASE, 'linkU')
self.cls(BASE, 'linkV').symlink_to(join('linkV'))
self._check_symlink_loop(BASE, 'linkV')
self.cls(BASE, 'linkW').symlink_to(join('linkW/../linkW'))
self._check_symlink_loop(BASE, 'linkW')
# Non-strict
q = self.cls(BASE, 'linkW', 'foo')
self.assertEqual(q.resolve(strict=False), q)

def test_stat(self):
statA = self.cls(BASE).joinpath('fileA').stat()
statB = self.cls(BASE).joinpath('dirB', 'fileB').stat()
Expand Down Expand Up @@ -3428,12 +3460,6 @@ def test_absolute(self):
self.assertEqual(str(P('//a').absolute()), '//a')
self.assertEqual(str(P('//a/b').absolute()), '//a/b')

def _check_symlink_loop(self, *args):
path = self.cls(*args)
with self.assertRaises(OSError) as cm:
path.resolve(strict=True)
self.assertEqual(cm.exception.errno, errno.ELOOP)

@unittest.skipIf(
is_emscripten or is_wasi,
"umask is not implemented on Emscripten/WASI."
Expand Down Expand Up @@ -3480,30 +3506,6 @@ def test_touch_mode(self):
st = os.stat(join('masked_new_file'))
self.assertEqual(stat.S_IMODE(st.st_mode), 0o750)

def test_resolve_loop(self):
if not self.can_symlink:
self.skipTest("symlinks required")
# Loops with relative symlinks.
os.symlink('linkX/inside', join('linkX'))
self._check_symlink_loop(BASE, 'linkX')
os.symlink('linkY', join('linkY'))
self._check_symlink_loop(BASE, 'linkY')
os.symlink('linkZ/../linkZ', join('linkZ'))
self._check_symlink_loop(BASE, 'linkZ')
# Non-strict
p = self.cls(BASE, 'linkZ', 'foo')
self.assertEqual(p.resolve(strict=False), p)
# Loops with absolute symlinks.
os.symlink(join('linkU/inside'), join('linkU'))
self._check_symlink_loop(BASE, 'linkU')
os.symlink(join('linkV'), join('linkV'))
self._check_symlink_loop(BASE, 'linkV')
os.symlink(join('linkW/../linkW'), join('linkW'))
self._check_symlink_loop(BASE, 'linkW')
# Non-strict
q = self.cls(BASE, 'linkW', 'foo')
self.assertEqual(q.resolve(strict=False), q)

def test_glob(self):
P = self.cls
p = P(BASE)
Expand Down