Skip to content

Commit e8c859c

Browse files
gh-71189: Support all-but-last mode in posixpath.realpath()
1 parent 0edde64 commit e8c859c

2 files changed

Lines changed: 156 additions & 17 deletions

File tree

Lib/posixpath.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -399,17 +399,21 @@ def abspath(path):
399399
# Return a canonical path (i.e. the absolute location of a file on the
400400
# filesystem).
401401

402+
# A singleton with true boolean value
403+
ALL_BUT_LAST = ['ALL_BUT_LAST']
404+
402405
def realpath(filename, *, strict=False):
403406
"""Return the canonical path of the specified filename, eliminating any
404407
symbolic links encountered in the path."""
405408
filename = os.fspath(filename)
406-
path, ok = _joinrealpath(filename[:0], filename, strict, {})
409+
path = _joinrealpath(filename[:0], filename, {},
410+
strict, strict is ALL_BUT_LAST, False)
407411
return abspath(path)
408412

409413
# Join two paths, normalizing and eliminating any symbolic links
410414
# encountered in the second path.
411415
# Two leading slashes are replaced by a single slash.
412-
def _joinrealpath(path, rest, strict, seen):
416+
def _joinrealpath(path, rest, seen, strict, last, isdir):
413417
if isinstance(path, bytes):
414418
sep = b'/'
415419
curdir = b'.'
@@ -424,7 +428,7 @@ def _joinrealpath(path, rest, strict, seen):
424428
path = sep
425429

426430
while rest:
427-
name, _, rest = rest.partition(sep)
431+
name, hassep, rest = rest.partition(sep)
428432
if not name or name == curdir:
429433
# current dir
430434
continue
@@ -445,13 +449,18 @@ def _joinrealpath(path, rest, strict, seen):
445449
newpath = join(path, name)
446450
try:
447451
st = os.lstat(newpath)
448-
except OSError:
452+
except OSError as e:
453+
if (last and not rest.strip(sep) and
454+
isinstance(e, FileNotFoundError)):
455+
return newpath
449456
if strict:
450457
raise
451-
is_link = False
452-
else:
453-
is_link = stat.S_ISLNK(st.st_mode)
454-
if not is_link:
458+
path = newpath
459+
continue
460+
if not stat.S_ISLNK(st.st_mode):
461+
if (strict and not stat.S_ISDIR(st.st_mode)
462+
and (isdir or hassep)):
463+
os.lstat(newpath + sep) # raises NotADirectoryError
455464
path = newpath
456465
continue
457466
# Resolve the symbolic link
@@ -465,16 +474,22 @@ def _joinrealpath(path, rest, strict, seen):
465474
if strict:
466475
# Raise OSError(errno.ELOOP)
467476
os.stat(newpath)
468-
else:
469-
# Return already resolved part + rest of the path unchanged.
470-
return join(newpath, rest), False
477+
path = newpath
478+
continue
471479
seen[newpath] = None # not resolved symlink
472-
path, ok = _joinrealpath(path, os.readlink(newpath), strict, seen)
473-
if not ok:
474-
return join(path, rest), False
480+
try:
481+
link = os.readlink(newpath)
482+
except OSError:
483+
if strict:
484+
raise
485+
path = newpath
486+
else:
487+
path = _joinrealpath(path, link, seen, strict,
488+
last and not rest.strip(sep),
489+
isdir or hassep)
475490
seen[newpath] = path # resolved symlink
476491

477-
return path, True
492+
return path
478493

479494

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

Lib/test/test_posixpath.py

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import errno
12
import os
23
import posixpath
34
import sys
45
import unittest
5-
from posixpath import realpath, abspath, dirname, basename
6+
from posixpath import realpath, abspath, dirname, basename, ALL_BUT_LAST
67
from test import test_genericpath
78
from test.support import import_helper
89
from test.support import os_helper
@@ -475,7 +476,7 @@ def test_realpath_symlink_loops(self):
475476
self.assertEqual(realpath(ABSTFN+"1/../x"), dirname(ABSTFN) + "/x")
476477
os.symlink(ABSTFN+"x", ABSTFN+"y")
477478
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "y"),
478-
ABSTFN + "y")
479+
ABSTFN + "x")
479480
self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "1"),
480481
ABSTFN + "1")
481482

@@ -637,6 +638,129 @@ def test_realpath_resolve_first(self):
637638
safe_rmdir(ABSTFN + "/k")
638639
safe_rmdir(ABSTFN)
639640

641+
@os_helper.skip_unless_symlink
642+
@skip_if_ABSTFN_contains_backslash
643+
def test_realpath_mode(self):
644+
try:
645+
os.mkdir(ABSTFN)
646+
os.mkdir(ABSTFN + "/dir")
647+
open(ABSTFN + "/file", "wb").close()
648+
open(ABSTFN + "/dir/file2", "wb").close()
649+
os.symlink("file", ABSTFN + "/link")
650+
os.symlink("dir", ABSTFN + "/link2")
651+
os.symlink("nonexisting", ABSTFN + "/broken")
652+
os.symlink("cycle", ABSTFN + "/cycle")
653+
def check(path, mode, expected, errno=None):
654+
if isinstance(expected, str):
655+
assert errno is None
656+
self.assertEqual(realpath(path, strict=mode), ABSTFN + expected)
657+
else:
658+
with self.assertRaises(expected) as cm:
659+
realpath(path, strict=mode)
660+
if errno is not None:
661+
self.assertEqual(cm.exception.errno, errno)
662+
663+
with os_helper.change_cwd(ABSTFN):
664+
check("file", False, "/file")
665+
check("file", ALL_BUT_LAST, "/file")
666+
check("file", True, "/file")
667+
check("file/", False, "/file")
668+
check("file/", ALL_BUT_LAST, NotADirectoryError)
669+
check("file/", True, NotADirectoryError)
670+
check("file/file2", False, "/file/file2")
671+
check("file/file2", ALL_BUT_LAST, NotADirectoryError)
672+
check("file/file2", True, NotADirectoryError)
673+
check("file/.", False, "/file")
674+
check("file/.", ALL_BUT_LAST, NotADirectoryError)
675+
check("file/.", True, NotADirectoryError)
676+
check("file/../link", False, "/file")
677+
check("file/../link", ALL_BUT_LAST, NotADirectoryError)
678+
check("file/../link", True, NotADirectoryError)
679+
680+
check("dir", False, "/dir")
681+
check("dir", ALL_BUT_LAST, "/dir")
682+
check("dir", True, "/dir")
683+
check("dir/", False, "/dir")
684+
check("dir/", ALL_BUT_LAST, "/dir")
685+
check("dir/", True, "/dir")
686+
check("dir/file2", False, "/dir/file2")
687+
check("dir/file2", ALL_BUT_LAST, "/dir/file2")
688+
check("dir/file2", True, "/dir/file2")
689+
690+
check("link", False, "/file")
691+
check("link", ALL_BUT_LAST, "/file")
692+
check("link", True, "/file")
693+
check("link/", False, "/file")
694+
check("link/", ALL_BUT_LAST, NotADirectoryError)
695+
check("link/", True, NotADirectoryError)
696+
check("link/file2", False, "/file/file2")
697+
check("link/file2", ALL_BUT_LAST, NotADirectoryError)
698+
check("link/file2", True, NotADirectoryError)
699+
check("link/.", False, "/file")
700+
check("link/.", ALL_BUT_LAST, NotADirectoryError)
701+
check("link/.", True, NotADirectoryError)
702+
check("link/../link", False, "/file")
703+
check("link/../link", ALL_BUT_LAST, NotADirectoryError)
704+
check("link/../link", True, NotADirectoryError)
705+
706+
check("link2", False, "/dir")
707+
check("link2", ALL_BUT_LAST, "/dir")
708+
check("link2", True, "/dir")
709+
check("link2/", False, "/dir")
710+
check("link2/", ALL_BUT_LAST, "/dir")
711+
check("link2/", True, "/dir")
712+
check("link2/file2", False, "/dir/file2")
713+
check("link2/file2", ALL_BUT_LAST, "/dir/file2")
714+
check("link2/file2", True, "/dir/file2")
715+
716+
check("nonexisting", False, "/nonexisting")
717+
check("nonexisting", ALL_BUT_LAST, "/nonexisting")
718+
check("nonexisting", True, FileNotFoundError)
719+
check("nonexisting/", False, "/nonexisting")
720+
check("nonexisting/", ALL_BUT_LAST, "/nonexisting")
721+
check("nonexisting/", True, FileNotFoundError)
722+
check("nonexisting/file", False, "/nonexisting/file")
723+
check("nonexisting/file", ALL_BUT_LAST, FileNotFoundError)
724+
check("nonexisting/file", True, FileNotFoundError)
725+
check("nonexisting/../link", False, "/file")
726+
check("nonexisting/../link", ALL_BUT_LAST, FileNotFoundError)
727+
check("nonexisting/../link", True, FileNotFoundError)
728+
729+
check("broken", False, "/nonexisting")
730+
check("broken", ALL_BUT_LAST, "/nonexisting")
731+
check("broken", True, FileNotFoundError)
732+
check("broken/", False, "/nonexisting")
733+
check("broken/", ALL_BUT_LAST, "/nonexisting")
734+
check("broken/", True, FileNotFoundError)
735+
check("broken/file", False, "/nonexisting/file")
736+
check("broken/file", ALL_BUT_LAST, FileNotFoundError)
737+
check("broken/file", True, FileNotFoundError)
738+
check("broken/../link", False, "/file")
739+
check("broken/../link", ALL_BUT_LAST, FileNotFoundError)
740+
check("broken/../link", True, FileNotFoundError)
741+
742+
check("cycle", False, "/cycle")
743+
check("cycle", ALL_BUT_LAST, OSError, errno.ELOOP)
744+
check("cycle", True, OSError, errno.ELOOP)
745+
check("cycle/", False, "/cycle")
746+
check("cycle/", ALL_BUT_LAST, OSError, errno.ELOOP)
747+
check("cycle/", True, OSError, errno.ELOOP)
748+
check("cycle/file", False, "/cycle/file")
749+
check("cycle/file", ALL_BUT_LAST, OSError, errno.ELOOP)
750+
check("cycle/file", True, OSError, errno.ELOOP)
751+
check("cycle/../link", False, "/file")
752+
check("cycle/../link", ALL_BUT_LAST, OSError, errno.ELOOP)
753+
check("cycle/../link", True, OSError, errno.ELOOP)
754+
finally:
755+
os_helper.unlink(ABSTFN + "/file")
756+
os_helper.unlink(ABSTFN + "/dir/file2")
757+
os_helper.unlink(ABSTFN + "/link")
758+
os_helper.unlink(ABSTFN + "/link2")
759+
os_helper.unlink(ABSTFN + "/broken")
760+
os_helper.unlink(ABSTFN + "/cycle")
761+
os_helper.rmdir(ABSTFN + "/dir")
762+
os_helper.rmdir(ABSTFN)
763+
640764
def test_relpath(self):
641765
(real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar")
642766
try:

0 commit comments

Comments
 (0)