Skip to content

Commit 2894e0a

Browse files
committed
Adjust os.path.realpath to match pathlib.Path.resolve() behaviour when a new strict=True keyword-only argument is passed.
1 parent 164dbfd commit 2894e0a

File tree

4 files changed

+38
-25
lines changed

4 files changed

+38
-25
lines changed

Doc/library/os.path.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,22 +344,29 @@ the :mod:`glob` module.)
344344
Accepts a :term:`path-like object`.
345345

346346

347-
.. function:: realpath(path)
347+
.. function:: realpath(path, *, strict=False)
348348

349349
Return the canonical path of the specified filename, eliminating any symbolic
350350
links encountered in the path (if they are supported by the operating
351351
system).
352352

353-
.. note::
354-
When symbolic link cycles occur, the returned path will be one member of
355-
the cycle, but no guarantee is made about which member that will be.
353+
In non-strict mode (the default), missing or inaccessible ancestors are
354+
permitted; when encountered, the remainder of the path joined on and
355+
returned. In strict mode an :exc:`OSError` is raised in this scenario.
356356

357357
.. versionchanged:: 3.6
358358
Accepts a :term:`path-like object`.
359359

360360
.. versionchanged:: 3.8
361361
Symbolic links and junctions are now resolved on Windows.
362362

363+
.. versionchanged:: 3.10
364+
The *strict* parameter was added.
365+
366+
.. versionchanged:: 3.10
367+
Raises :exc:`OSError` with :const:`~errno.ELOOP` when a symbolic link
368+
cycle occurs. Previously returned one member of the cycle.
369+
363370

364371
.. function:: relpath(path, start=os.curdir)
365372

Lib/ntpath.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ def _getfinalpathname_nonstrict(path):
635635
tail = join(name, tail) if tail else name
636636
return tail
637637

638-
def realpath(path):
638+
def realpath(path, *, strict=False):
639639
path = normpath(path)
640640
if isinstance(path, bytes):
641641
prefix = b'\\\\?\\'
@@ -660,6 +660,8 @@ def realpath(path):
660660
path = _getfinalpathname(path)
661661
initial_winerror = 0
662662
except OSError as ex:
663+
if strict:
664+
raise
663665
initial_winerror = ex.winerror
664666
path = _getfinalpathname_nonstrict(path)
665667
# The path returned by _getfinalpathname will always start with \\?\ -

Lib/pathlib.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,14 +1058,13 @@ def resolve(self, strict=False):
10581058
normalizing it (for example turning slashes into backslashes under
10591059
Windows).
10601060
"""
1061-
p = self._from_parts((self._accessor.realpath(self),))
10621061
try:
1063-
if S_ISLNK(self.lstat().st_mode):
1064-
raise RuntimeError("Symlink loop from %r" % str(p))
1065-
except OSError:
1066-
if strict:
1067-
raise
1068-
return p
1062+
p = self._accessor.realpath(self, strict=strict)
1063+
except OSError as ex:
1064+
if ex.errno == ELOOP:
1065+
raise RuntimeError("Symlink loop from %r", ex.filename)
1066+
raise
1067+
return self._from_parts((p,))
10691068

10701069
def stat(self, *, follow_symlinks=True):
10711070
"""

Lib/posixpath.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
altsep = None
2323
devnull = '/dev/null'
2424

25+
import errno
2526
import os
2627
import sys
2728
import stat
@@ -387,16 +388,16 @@ def abspath(path):
387388
# Return a canonical path (i.e. the absolute location of a file on the
388389
# filesystem).
389390

390-
def realpath(filename):
391+
def realpath(filename, *, strict=False):
391392
"""Return the canonical path of the specified filename, eliminating any
392393
symbolic links encountered in the path."""
393394
filename = os.fspath(filename)
394-
path, ok = _joinrealpath(filename[:0], filename, {})
395+
path = _joinrealpath(filename[:0], filename, strict, {})
395396
return abspath(path)
396397

397398
# Join two paths, normalizing and eliminating any symbolic links
398399
# encountered in the second path.
399-
def _joinrealpath(path, rest, seen):
400+
def _joinrealpath(path, rest, strict, seen):
400401
if isinstance(path, bytes):
401402
sep = b'/'
402403
curdir = b'.'
@@ -425,9 +426,17 @@ def _joinrealpath(path, rest, seen):
425426
path = pardir
426427
continue
427428
newpath = join(path, name)
428-
if not islink(newpath):
429-
path = newpath
430-
continue
429+
try:
430+
st = os.lstat(newpath)
431+
except OSError:
432+
if strict:
433+
raise
434+
is_link = False
435+
else:
436+
is_link = stat.S_ISLNK(st.st_mode)
437+
if not is_link:
438+
path = newpath
439+
continue
431440
# Resolve the symbolic link
432441
if newpath in seen:
433442
# Already seen this path
@@ -436,15 +445,11 @@ def _joinrealpath(path, rest, seen):
436445
# use cached value
437446
continue
438447
# The symlink is not resolved, so we must have a symlink loop.
439-
# Return already resolved part + rest of the path unchanged.
440-
return join(newpath, rest), False
441-
seen[newpath] = None # not resolved symlink
442-
path, ok = _joinrealpath(path, os.readlink(newpath), seen)
443-
if not ok:
444-
return join(path, rest), False
448+
raise OSError(errno.ELOOP, os.strerror(errno.ELOOP), newpath)
449+
path = _joinrealpath(path, os.readlink(newpath), strict, seen)
445450
seen[newpath] = path # resolved symlink
446451

447-
return path, True
452+
return path
448453

449454

450455
supports_unicode_filenames = (sys.platform == 'darwin')

0 commit comments

Comments
 (0)