Skip to content

Commit

Permalink
pythonGH-109187: Improve symlink loop handling in `pathlib.Path.resol…
Browse files Browse the repository at this point in the history
…ve()`

Treat symlink loops like other errors: in strict mode, raise `OSError`, and
in non-strict mode, do not raise any exception.
  • Loading branch information
barneygale committed Sep 9, 2023
1 parent 75cd865 commit 245bcd2
Show file tree
Hide file tree
Showing 4 changed files with 19 additions and 27 deletions.
14 changes: 9 additions & 5 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1381,15 +1381,19 @@ call fails (for example because the path doesn't exist).
>>> p.resolve()
PosixPath('/home/antoine/pathlib/setup.py')

If the path doesn't exist and *strict* is ``True``, :exc:`FileNotFoundError`
is raised. If *strict* is ``False``, the path is resolved as far as possible
and any remainder is appended without checking whether it exists. If an
infinite loop is encountered along the resolution path, :exc:`RuntimeError`
is raised.
If a path doesn't exist or a symlink loop is encountered, and *strict* is
``True``, :exc:`OSError` is raised. If *strict* is ``False``, the path is
resolved as far as possible and any remainder is appended without checking
whether it exists.

.. versionchanged:: 3.6
The *strict* parameter was added (pre-3.6 behavior is strict).

.. versionchanged:: 3.13
Symlink loops are treated like other errors: :exc:`OSError` is raised in
strict mode, and no exception is raised in non-strict mode. In previous
versions, :exc:`RuntimeError` was raised no matter the value of *strict*.

.. method:: Path.rglob(pattern, *, case_sensitive=None, follow_symlinks=None)

Glob the given relative *pattern* recursively. This is like calling
Expand Down
21 changes: 1 addition & 20 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,26 +1230,7 @@ def resolve(self, strict=False):
normalizing it.
"""

def check_eloop(e):
winerror = getattr(e, 'winerror', 0)
if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME:
raise RuntimeError("Symlink loop from %r" % e.filename)

try:
s = os.path.realpath(self, strict=strict)
except OSError as e:
check_eloop(e)
raise
p = self.with_segments(s)

# In non-strict mode, realpath() doesn't raise on symlink loops.
# Ensure we get an exception by calling stat()
if not strict:
try:
p.stat()
except OSError as e:
check_eloop(e)
return p
return self.with_segments(os.path.realpath(self, strict=strict))

def owner(self):
"""
Expand Down
8 changes: 6 additions & 2 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3180,8 +3180,12 @@ def test_absolute(self):

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

@unittest.skipIf(
is_emscripten or is_wasi,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:meth:`pathlib.Path.resolve` now treats symlink loops like other errors: in
strict mode, :exc:`OSError` is raised, and in non-strict mode, no exception
is raised.

0 comments on commit 245bcd2

Please sign in to comment.