Skip to content

Commit ecd813f

Browse files
authored
GH-109187: Improve symlink loop handling in pathlib.Path.resolve() (GH-109192)
Treat symlink loops like other errors: in strict mode, raise `OSError`, and in non-strict mode, do not raise any exception.
1 parent 859618c commit ecd813f

File tree

4 files changed

+21
-30
lines changed

4 files changed

+21
-30
lines changed

Doc/library/pathlib.rst

+9-5
Original file line numberDiff line numberDiff line change
@@ -1381,15 +1381,19 @@ call fails (for example because the path doesn't exist).
13811381
>>> p.resolve()
13821382
PosixPath('/home/antoine/pathlib/setup.py')
13831383

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

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

1392+
.. versionchanged:: 3.13
1393+
Symlink loops are treated like other errors: :exc:`OSError` is raised in
1394+
strict mode, and no exception is raised in non-strict mode. In previous
1395+
versions, :exc:`RuntimeError` is raised no matter the value of *strict*.
1396+
13931397
.. method:: Path.rglob(pattern, *, case_sensitive=None, follow_symlinks=None)
13941398

13951399
Glob the given relative *pattern* recursively. This is like calling

Lib/pathlib.py

+1-20
Original file line numberDiff line numberDiff line change
@@ -1230,26 +1230,7 @@ def resolve(self, strict=False):
12301230
normalizing it.
12311231
"""
12321232

1233-
def check_eloop(e):
1234-
winerror = getattr(e, 'winerror', 0)
1235-
if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME:
1236-
raise RuntimeError("Symlink loop from %r" % e.filename)
1237-
1238-
try:
1239-
s = os.path.realpath(self, strict=strict)
1240-
except OSError as e:
1241-
check_eloop(e)
1242-
raise
1243-
p = self.with_segments(s)
1244-
1245-
# In non-strict mode, realpath() doesn't raise on symlink loops.
1246-
# Ensure we get an exception by calling stat()
1247-
if not strict:
1248-
try:
1249-
p.stat()
1250-
except OSError as e:
1251-
check_eloop(e)
1252-
return p
1233+
return self.with_segments(os.path.realpath(self, strict=strict))
12531234

12541235
def owner(self):
12551236
"""

Lib/test/test_pathlib.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -3178,10 +3178,11 @@ def test_absolute(self):
31783178
self.assertEqual(str(P('//a').absolute()), '//a')
31793179
self.assertEqual(str(P('//a/b').absolute()), '//a/b')
31803180

3181-
def _check_symlink_loop(self, *args, strict=True):
3181+
def _check_symlink_loop(self, *args):
31823182
path = self.cls(*args)
3183-
with self.assertRaises(RuntimeError):
3184-
print(path.resolve(strict))
3183+
with self.assertRaises(OSError) as cm:
3184+
path.resolve(strict=True)
3185+
self.assertEqual(cm.exception.errno, errno.ELOOP)
31853186

31863187
@unittest.skipIf(
31873188
is_emscripten or is_wasi,
@@ -3240,7 +3241,8 @@ def test_resolve_loop(self):
32403241
os.symlink('linkZ/../linkZ', join('linkZ'))
32413242
self._check_symlink_loop(BASE, 'linkZ')
32423243
# Non-strict
3243-
self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False)
3244+
p = self.cls(BASE, 'linkZ', 'foo')
3245+
self.assertEqual(p.resolve(strict=False), p)
32443246
# Loops with absolute symlinks.
32453247
os.symlink(join('linkU/inside'), join('linkU'))
32463248
self._check_symlink_loop(BASE, 'linkU')
@@ -3249,7 +3251,8 @@ def test_resolve_loop(self):
32493251
os.symlink(join('linkW/../linkW'), join('linkW'))
32503252
self._check_symlink_loop(BASE, 'linkW')
32513253
# Non-strict
3252-
self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False)
3254+
q = self.cls(BASE, 'linkW', 'foo')
3255+
self.assertEqual(q.resolve(strict=False), q)
32533256

32543257
def test_glob(self):
32553258
P = self.cls
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`pathlib.Path.resolve` now treats symlink loops like other errors: in
2+
strict mode, :exc:`OSError` is raised, and in non-strict mode, no exception
3+
is raised.

0 commit comments

Comments
 (0)