https://github.com/python/cpython/commit/9d3b53c47fab9ebf1f40d6f21b7d1ad391c14cd7
commit: 9d3b53c47fab9ebf1f40d6f21b7d1ad391c14cd7
branch: main
author: Serhiy Storchaka <storch...@gmail.com>
committer: serhiy-storchaka <storch...@gmail.com>
date: 2025-07-30T10:19:19+03:00
summary:

gh-71189: Support all-but-last mode in os.path.realpath() (GH-117562)

files:
A Misc/NEWS.d/next/Library/2025-05-20-11-51-17.gh-issue-71189.0LpTB1.rst
M Doc/library/os.path.rst
M Doc/whatsnew/3.15.rst
M Lib/genericpath.py
M Lib/ntpath.py
M Lib/posixpath.py
M Lib/test/test_genericpath.py
M Lib/test/test_ntpath.py
M Lib/test/test_posixpath.py

diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst
index d27023cde4cfe9..cb021be4543e75 100644
--- a/Doc/library/os.path.rst
+++ b/Doc/library/os.path.rst
@@ -424,6 +424,8 @@ the :mod:`glob` module.)
    re-raised.
    In particular, :exc:`FileNotFoundError` is raised if *path* does not exist,
    or another :exc:`OSError` if it is otherwise inaccessible.
+   If *strict* is :data:`ALL_BUT_LAST`, the last component of the path
+   is allowed to be missing, but all other errors are raised.
 
    If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than
    :exc:`FileNotFoundError` are re-raised (as with ``strict=True``).
@@ -448,8 +450,14 @@ the :mod:`glob` module.)
       The *strict* parameter was added.
 
    .. versionchanged:: next
-      The :py:data:`~os.path.ALLOW_MISSING` value for the *strict* parameter
-      was added.
+      The :data:`ALL_BUT_LAST` and :data:`ALLOW_MISSING` values for
+      the *strict* parameter was added.
+
+.. data:: ALL_BUT_LAST
+
+   Special value used for the *strict* argument in :func:`realpath`.
+
+   .. versionadded:: next
 
 .. data:: ALLOW_MISSING
 
@@ -457,6 +465,7 @@ the :mod:`glob` module.)
 
    .. versionadded:: next
 
+
 .. function:: relpath(path, start=os.curdir)
 
    Return a relative filepath to *path* either from the current directory or
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index d89260b32dfb82..6c00a0fcb83fee 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -264,13 +264,15 @@ math
 os.path
 -------
 
+* Add support of the all-but-last mode in :func:`~os.path.realpath`.
+  (Contributed by Serhiy Storchaka in :gh:`71189`.)
+
 * The *strict* parameter to :func:`os.path.realpath` accepts a new value,
   :data:`os.path.ALLOW_MISSING`.
   If used, errors other than :exc:`FileNotFoundError` will be re-raised;
   the resulting path can be missing but it will be free of symlinks.
   (Contributed by Petr Viktorin for :cve:`2025-4517`.)
 
-
 shelve
 ------
 
diff --git a/Lib/genericpath.py b/Lib/genericpath.py
index 4a223654994194..7588fe5e8020f9 100644
--- a/Lib/genericpath.py
+++ b/Lib/genericpath.py
@@ -8,7 +8,8 @@
 
 __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
            'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink',
-           'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALLOW_MISSING']
+           'lexists', 'samefile', 'sameopenfile', 'samestat',
+           'ALL_BUT_LAST', 'ALLOW_MISSING']
 
 
 # Does a path exist?
@@ -190,7 +191,17 @@ def _check_arg_types(funcname, *args):
     if hasstr and hasbytes:
         raise TypeError("Can't mix strings and bytes in path components") from 
None
 
-# A singleton with a true boolean value.
+
+# Singletons with a true boolean value.
+
+@object.__new__
+class ALL_BUT_LAST:
+    """Special value for use in realpath()."""
+    def __repr__(self):
+        return 'os.path.ALL_BUT_LAST'
+    def __reduce__(self):
+        return self.__class__.__name__
+
 @object.__new__
 class ALLOW_MISSING:
     """Special value for use in realpath()."""
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
index fad15430a373fb..08c529c5963a4d 100644
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -29,7 +29,7 @@
            "abspath","curdir","pardir","sep","pathsep","defpath","altsep",
            
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
            "samefile", "sameopenfile", "samestat", "commonpath", "isjunction",
-           "isdevdrive", "ALLOW_MISSING"]
+           "isdevdrive", "ALL_BUT_LAST", "ALLOW_MISSING"]
 
 def _get_bothseps(path):
     if isinstance(path, bytes):
@@ -726,7 +726,8 @@ def realpath(path, /, *, strict=False):
 
         if strict is ALLOW_MISSING:
             ignored_error = FileNotFoundError
-            strict = True
+        elif strict is ALL_BUT_LAST:
+            ignored_error = FileNotFoundError
         elif strict:
             ignored_error = ()
         else:
@@ -746,6 +747,12 @@ def realpath(path, /, *, strict=False):
                 raise OSError(str(ex)) from None
             path = normpath(path)
         except ignored_error as ex:
+            if strict is ALL_BUT_LAST:
+                dirname, basename = split(path)
+                if not basename:
+                    dirname, basename = split(path)
+                if not isdir(dirname):
+                    raise
             initial_winerror = ex.winerror
             path = _getfinalpathname_nonstrict(path,
                                                ignored_error=ignored_error)
diff --git a/Lib/posixpath.py b/Lib/posixpath.py
index 5b5cde239e6275..7aaf9ff4faa056 100644
--- a/Lib/posixpath.py
+++ b/Lib/posixpath.py
@@ -36,7 +36,8 @@
            "samefile","sameopenfile","samestat",
            "curdir","pardir","sep","pathsep","defpath","altsep","extsep",
            "devnull","realpath","supports_unicode_filenames","relpath",
-           "commonpath", "isjunction","isdevdrive","ALLOW_MISSING"]
+           "commonpath","isjunction","isdevdrive",
+           "ALL_BUT_LAST","ALLOW_MISSING"]
 
 
 def _get_sep(path):
@@ -404,7 +405,8 @@ def realpath(filename, /, *, strict=False):
         getcwd = os.getcwd
     if strict is ALLOW_MISSING:
         ignored_error = FileNotFoundError
-        strict = True
+    elif strict is ALL_BUT_LAST:
+        ignored_error = FileNotFoundError
     elif strict:
         ignored_error = ()
     else:
@@ -418,7 +420,7 @@ def realpath(filename, /, *, strict=False):
     # indicates that a symlink target has been resolved, and that the original
     # symlink path can be retrieved by popping again. The [::-1] slice is a
     # very fast way of spelling list(reversed(...)).
-    rest = filename.split(sep)[::-1]
+    rest = filename.rstrip(sep).split(sep)[::-1]
 
     # Number of unprocessed parts in 'rest'. This can differ from len(rest)
     # later, because 'rest' might contain markers for unresolved symlinks.
@@ -427,6 +429,7 @@ def realpath(filename, /, *, strict=False):
     # The resolved path, which is absolute throughout this function.
     # Note: getcwd() returns a normalized and symlink-free path.
     path = sep if filename.startswith(sep) else getcwd()
+    trailing_sep = filename.endswith(sep)
 
     # Mapping from symlink paths to *fully resolved* symlink targets. If a
     # symlink is encountered but not yet resolved, the value is None. This is
@@ -459,7 +462,8 @@ def realpath(filename, /, *, strict=False):
         try:
             st_mode = lstat(newpath).st_mode
             if not stat.S_ISLNK(st_mode):
-                if strict and part_count and not stat.S_ISDIR(st_mode):
+                if (strict and (part_count or trailing_sep)
+                    and not stat.S_ISDIR(st_mode)):
                     raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR),
                                   newpath)
                 path = newpath
@@ -486,7 +490,8 @@ def realpath(filename, /, *, strict=False):
                 continue
             target = readlink(newpath)
         except ignored_error:
-            pass
+            if strict is ALL_BUT_LAST and part_count:
+                raise
         else:
             # Resolve the symbolic link
             if target.startswith(sep):
diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py
index 16c3268fefb034..71d92c0e45f7be 100644
--- a/Lib/test/test_genericpath.py
+++ b/Lib/test/test_genericpath.py
@@ -2,8 +2,10 @@
 Tests common to genericpath, ntpath and posixpath
 """
 
+import copy
 import genericpath
 import os
+import pickle
 import sys
 import unittest
 import warnings
@@ -320,6 +322,21 @@ def test_sameopenfile(self):
                 fd2 = fp2.fileno()
                 self.assertTrue(self.pathmodule.sameopenfile(fd1, fd2))
 
+    def test_realpath_mode_values(self):
+        for name in 'ALL_BUT_LAST', 'ALLOW_MISSING':
+            with self.subTest(name):
+                mode = getattr(self.pathmodule, name)
+                self.assertEqual(repr(mode), 'os.path.' + name)
+                self.assertEqual(str(mode), 'os.path.' + name)
+                self.assertTrue(mode)
+                self.assertIs(copy.copy(mode), mode)
+                self.assertIs(copy.deepcopy(mode), mode)
+                for proto in range(pickle.HIGHEST_PROTOCOL+1):
+                    with self.subTest(protocol=proto):
+                        pickled = pickle.dumps(mode, proto)
+                        unpickled = pickle.loads(pickled)
+                        self.assertIs(unpickled, mode)
+
 
 class TestGenericTest(GenericTest, unittest.TestCase):
     # Issue 16852: GenericTest can't inherit from unittest.TestCase
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index 22f6403d482bc4..b9cd75a3b8adea 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -1,3 +1,4 @@
+import errno
 import inspect
 import ntpath
 import os
@@ -6,7 +7,7 @@
 import sys
 import unittest
 import warnings
-from ntpath import ALLOW_MISSING
+from ntpath import ALL_BUT_LAST, ALLOW_MISSING
 from test import support
 from test.support import TestFailed, cpython_only, os_helper
 from test.support.os_helper import FakePath
@@ -587,59 +588,63 @@ def test_realpath_invalid_paths(self):
         # gh-106242: Embedded nulls and non-strict fallback to abspath
         self.assertEqual(realpath(path, strict=False), path)
         # gh-106242: Embedded nulls should raise OSError (not ValueError)
+        self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(OSError, realpath, path, strict=True)
         self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
         path = ABSTFNb + b'\x00'
         self.assertEqual(realpath(path, strict=False), path)
+        self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(OSError, realpath, path, strict=True)
         self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
         path = ABSTFN + '\\nonexistent\\x\x00'
         self.assertEqual(realpath(path, strict=False), path)
+        self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(OSError, realpath, path, strict=True)
         self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
         path = ABSTFNb + b'\\nonexistent\\x\x00'
         self.assertEqual(realpath(path, strict=False), path)
+        self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(OSError, realpath, path, strict=True)
         self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
         path = ABSTFN + '\x00\\..'
         self.assertEqual(realpath(path, strict=False), os.getcwd())
+        self.assertEqual(realpath(path, strict=ALL_BUT_LAST), os.getcwd())
         self.assertEqual(realpath(path, strict=True), os.getcwd())
         self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd())
         path = ABSTFNb + b'\x00\\..'
         self.assertEqual(realpath(path, strict=False), os.getcwdb())
+        self.assertEqual(realpath(path, strict=ALL_BUT_LAST), os.getcwdb())
         self.assertEqual(realpath(path, strict=True), os.getcwdb())
         self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb())
         path = ABSTFN + '\\nonexistent\\x\x00\\..'
         self.assertEqual(realpath(path, strict=False), ABSTFN + 
'\\nonexistent')
+        self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(OSError, realpath, path, strict=True)
         self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + 
'\\nonexistent')
         path = ABSTFNb + b'\\nonexistent\\x\x00\\..'
         self.assertEqual(realpath(path, strict=False), ABSTFNb + 
b'\\nonexistent')
+        self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(OSError, realpath, path, strict=True)
         self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + 
b'\\nonexistent')
 
     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_invalid_unicode_paths(self, kwargs):
         realpath = ntpath.realpath
         ABSTFN = ntpath.abspath(os_helper.TESTFN)
         ABSTFNb = os.fsencode(ABSTFN)
         path = ABSTFNb + b'\xff'
         self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
-        self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
         path = ABSTFNb + b'\\nonexistent\\\xff'
         self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
-        self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
         path = ABSTFNb + b'\xff\\..'
         self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
-        self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
         path = ABSTFNb + b'\\nonexistent\\\xff\\..'
         self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
-        self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
 
     @os_helper.skip_unless_symlink
     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_relative(self, kwargs):
         ABSTFN = ntpath.abspath(os_helper.TESTFN)
         open(ABSTFN, "wb").close()
@@ -766,34 +771,53 @@ def test_realpath_symlink_loops_strict(self):
         self.addCleanup(os_helper.unlink, ABSTFN + "a")
 
         os.symlink(ABSTFN, ABSTFN)
+        self.assertRaises(OSError, ntpath.realpath, ABSTFN, 
strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=True)
 
         os.symlink(ABSTFN + "1", ABSTFN + "2")
         os.symlink(ABSTFN + "2", ABSTFN + "1")
+        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1", 
strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1", strict=True)
+        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2", 
strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2", strict=True)
+        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x", 
strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x", 
strict=True)
         # Windows eliminates '..' components before resolving links, so the
         # following call is not expected to raise.
+        self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..", 
strict=ALL_BUT_LAST),
+                             ntpath.dirname(ABSTFN))
         self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..", strict=True),
                              ntpath.dirname(ABSTFN))
+        self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\x", 
strict=ALL_BUT_LAST),
+                             ntpath.dirname(ABSTFN) + "\\x")
         self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\..\\x", 
strict=True)
         os.symlink(ABSTFN + "x", ABSTFN + "y")
+        self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\"
+                                             + ntpath.basename(ABSTFN) + "y",
+                                             strict=ALL_BUT_LAST),
+                             ABSTFN + "x")
         self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\..\\"
                                              + ntpath.basename(ABSTFN) + "y",
                                              strict=True)
+        self.assertRaises(OSError, ntpath.realpath,
+                          ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
+                          strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath,
                           ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
                           strict=True)
 
         os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a")
+        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a", 
strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a", strict=True)
 
         os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN))
                    + "\\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c")
+        self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c", 
strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c", strict=True)
 
         # Test using relative path as well.
+        self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
+                          strict=ALL_BUT_LAST)
         self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
                           strict=True)
 
@@ -853,7 +877,7 @@ def test_realpath_symlink_loops_raise(self):
 
     @os_helper.skip_unless_symlink
     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_symlink_prefix(self, kwargs):
         ABSTFN = ntpath.abspath(os_helper.TESTFN)
         self.addCleanup(os_helper.unlink, ABSTFN + "3")
@@ -891,6 +915,7 @@ def test_realpath_nul(self):
         tester("ntpath.realpath('NUL')", r'\\.\NUL')
         tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL')
         tester("ntpath.realpath('NUL', strict=True)", r'\\.\NUL')
+        tester("ntpath.realpath('NUL', strict=ALL_BUT_LAST)", r'\\.\NUL')
         tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\\.\NUL')
 
     @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@@ -915,7 +940,7 @@ def test_realpath_cwd(self):
 
         self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short))
 
-        for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}:
+        for kwargs in {}, {'strict': True}, {'strict': ALL_BUT_LAST}, 
{'strict': ALLOW_MISSING}:
             with self.subTest(**kwargs):
                 with os_helper.change_cwd(test_dir_long):
                     self.assertPathEqual(
@@ -975,6 +1000,93 @@ def test_realpath_permission(self):
 
         self.assertPathEqual(test_file, ntpath.realpath(test_file_short))
 
+    @os_helper.skip_unless_symlink
+    @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
+    def test_realpath_mode(self):
+        realpath = ntpath.realpath
+        ABSTFN = ntpath.abspath(os_helper.TESTFN)
+        self.addCleanup(os_helper.rmdir, ABSTFN)
+        self.addCleanup(os_helper.rmdir, ABSTFN + "/dir")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/file")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/dir/file2")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/link")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/link2")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/broken")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/cycle")
+
+        os.mkdir(ABSTFN)
+        os.mkdir(ABSTFN + "\\dir")
+        open(ABSTFN + "\\file", "wb").close()
+        open(ABSTFN + "\\dir\\file2", "wb").close()
+        os.symlink("file", ABSTFN + "\\link")
+        os.symlink("dir", ABSTFN + "\\link2")
+        os.symlink("nonexistent", ABSTFN + "\\broken")
+        os.symlink("cycle", ABSTFN + "\\cycle")
+        def check(path, modes, expected, errno=None):
+            path = path.replace('/', '\\')
+            if isinstance(expected, str):
+                assert errno is None
+                expected = expected.replace('/', os.sep)
+                for mode in modes:
+                    with self.subTest(mode=mode):
+                        self.assertEqual(realpath(path, strict=mode),
+                                         ABSTFN + expected)
+            else:
+                for mode in modes:
+                    with self.subTest(mode=mode):
+                        with self.assertRaises(expected) as cm:
+                            realpath(path, strict=mode)
+                        if errno is not None:
+                            self.assertEqual(cm.exception.errno, errno)
+
+        self.enterContext(os_helper.change_cwd(ABSTFN))
+        all_modes = [False, ALLOW_MISSING, ALL_BUT_LAST, True]
+        check("file", all_modes, "/file")
+        check("file/", all_modes, "/file")
+        check("file/file2", [False, ALLOW_MISSING], "/file/file2")
+        check("file/file2", [ALL_BUT_LAST, True], FileNotFoundError)
+        check("file/.", all_modes, "/file")
+        check("file/../link2", all_modes, "/dir")
+
+        check("dir", all_modes, "/dir")
+        check("dir/", all_modes, "/dir")
+        check("dir/file2", all_modes, "/dir/file2")
+
+        check("link", all_modes, "/file")
+        check("link/", all_modes, "/file")
+        check("link/file2", [False, ALLOW_MISSING], "/file/file2")
+        check("link/file2", [ALL_BUT_LAST, True], FileNotFoundError)
+        check("link/.", all_modes, "/file")
+        check("link/../link", all_modes, "/file")
+
+        check("link2", all_modes, "/dir")
+        check("link2/", all_modes, "/dir")
+        check("link2/file2", all_modes, "/dir/file2")
+
+        check("nonexistent", [False, ALLOW_MISSING, ALL_BUT_LAST], 
"/nonexistent")
+        check("nonexistent", [True], FileNotFoundError)
+        check("nonexistent/", [False, ALLOW_MISSING, ALL_BUT_LAST], 
"/nonexistent")
+        check("nonexistent/", [True], FileNotFoundError)
+        check("nonexistent/file", [False, ALLOW_MISSING], "/nonexistent/file")
+        check("nonexistent/file", [ALL_BUT_LAST, True], FileNotFoundError)
+        check("nonexistent/../link", all_modes, "/file")
+
+        check("broken", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
+        check("broken", [True], FileNotFoundError)
+        check("broken/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
+        check("broken/", [True], FileNotFoundError)
+        check("broken/file", [False, ALLOW_MISSING], "/nonexistent/file")
+        check("broken/file", [ALL_BUT_LAST, True], FileNotFoundError)
+        check("broken/../link", all_modes, "/file")
+
+        check("cycle", [False], "/cycle")
+        check("cycle", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, 
errno.EINVAL)
+        check("cycle/", [False], "/cycle")
+        check("cycle/", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, 
errno.EINVAL)
+        check("cycle/file", [False], "/cycle/file")
+        check("cycle/file", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, 
errno.EINVAL)
+        check("cycle/../link", all_modes, "/file")
+
     def test_expandvars(self):
         with os_helper.EnvironmentVarGuard() as env:
             env.clear()
diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py
index 21f06712548d88..a9975b75f7c22b 100644
--- a/Lib/test/test_posixpath.py
+++ b/Lib/test/test_posixpath.py
@@ -1,3 +1,4 @@
+import errno
 import inspect
 import os
 import posixpath
@@ -5,7 +6,7 @@
 import sys
 import unittest
 from functools import partial
-from posixpath import realpath, abspath, dirname, basename, ALLOW_MISSING
+from posixpath import realpath, abspath, dirname, basename, ALL_BUT_LAST, 
ALLOW_MISSING
 from test import support
 from test import test_genericpath
 from test.support import import_helper
@@ -448,7 +449,7 @@ def test_normpath(self):
                 self.assertEqual(result, expected)
 
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_curdir(self, kwargs):
         self.assertEqual(realpath('.', **kwargs), os.getcwd())
         self.assertEqual(realpath('./.', **kwargs), os.getcwd())
@@ -459,7 +460,7 @@ def test_realpath_curdir(self, kwargs):
         self.assertEqual(realpath(b'/'.join([b'.'] * 100), **kwargs), 
os.getcwdb())
 
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_pardir(self, kwargs):
         self.assertEqual(realpath('..', **kwargs), dirname(os.getcwd()))
         self.assertEqual(realpath('../..', **kwargs), 
dirname(dirname(os.getcwd())))
@@ -495,35 +496,43 @@ def test_realpath_strict(self):
     def test_realpath_invalid_paths(self):
         path = '/\x00'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(ValueError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
         path = b'/\x00'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(ValueError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
         path = '/nonexistent/x\x00'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
         self.assertRaises(FileNotFoundError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
         path = b'/nonexistent/x\x00'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
         self.assertRaises(FileNotFoundError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
         path = '/\x00/..'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(ValueError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
         path = b'/\x00/..'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
         self.assertRaises(ValueError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
 
         path = '/nonexistent/x\x00/..'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
         self.assertRaises(FileNotFoundError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
         path = b'/nonexistent/x\x00/..'
         self.assertRaises(ValueError, realpath, path, strict=False)
+        self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
         self.assertRaises(FileNotFoundError, realpath, path, strict=True)
         self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
 
@@ -534,6 +543,7 @@ def test_realpath_invalid_paths(self):
             self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
         else:
             self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
+            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALL_BUT_LAST)
             self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
             self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
         path = '/nonexistent/\udfff'
@@ -543,6 +553,7 @@ def test_realpath_invalid_paths(self):
         else:
             self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
             self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
+        self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
         self.assertRaises(FileNotFoundError, realpath, path, strict=True)
         path = '/\udfff/..'
         if sys.platform == 'win32':
@@ -551,6 +562,7 @@ def test_realpath_invalid_paths(self):
             self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/')
         else:
             self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
+            self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALL_BUT_LAST)
             self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
             self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
         path = '/nonexistent/\udfff/..'
@@ -560,6 +572,7 @@ def test_realpath_invalid_paths(self):
         else:
             self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
             self.assertRaises(UnicodeEncodeError, realpath, path, 
strict=ALLOW_MISSING)
+        self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
         self.assertRaises(FileNotFoundError, realpath, path, strict=True)
 
         path = b'/\xff'
@@ -570,9 +583,11 @@ def test_realpath_invalid_paths(self):
         else:
             self.assertEqual(realpath(path, strict=False), path)
             if support.is_wasi:
+                self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
                 self.assertRaises(OSError, realpath, path, strict=True)
                 self.assertRaises(OSError, realpath, path, 
strict=ALLOW_MISSING)
             else:
+                self.assertEqual(realpath(path, strict=ALL_BUT_LAST), path)
                 self.assertRaises(FileNotFoundError, realpath, path, 
strict=True)
                 self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
         path = b'/nonexistent/\xff'
@@ -582,14 +597,16 @@ def test_realpath_invalid_paths(self):
         else:
             self.assertEqual(realpath(path, strict=False), path)
         if support.is_wasi:
+            self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
             self.assertRaises(OSError, realpath, path, strict=True)
             self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
         else:
+            self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
             self.assertRaises(FileNotFoundError, realpath, path, strict=True)
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
     def test_realpath_relative(self, kwargs):
         try:
             os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN)
@@ -599,12 +616,14 @@ def test_realpath_relative(self, kwargs):
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': ALLOW_MISSING})
-    def test_realpath_missing_pardir(self, kwargs):
+    def test_realpath_missing_pardir(self):
         try:
             os.symlink(TESTFN + "1", TESTFN)
-            self.assertEqual(
-                realpath("nonexistent/../" + TESTFN, **kwargs), ABSTFN + "1")
+            path = "nonexistent/../" + TESTFN
+            self.assertEqual(realpath(path), ABSTFN + "1")
+            self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + 
"1")
+            self.assertRaises(FileNotFoundError, realpath, path, 
strict=ALL_BUT_LAST)
+            self.assertRaises(FileNotFoundError, realpath, path, strict=True)
         finally:
             os_helper.unlink(TESTFN)
 
@@ -651,7 +670,7 @@ def test_realpath_symlink_loops(self):
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_symlink_loops_strict(self, kwargs):
         # Bug #43757, raise OSError if we get into an infinite symlink loop in
         # the strict modes.
@@ -693,7 +712,7 @@ def test_realpath_symlink_loops_strict(self, kwargs):
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_repeated_indirect_symlinks(self, kwargs):
         # Issue #6975.
         try:
@@ -708,7 +727,7 @@ def test_realpath_repeated_indirect_symlinks(self, kwargs):
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_deep_recursion(self, kwargs):
         depth = 10
         try:
@@ -728,7 +747,7 @@ def test_realpath_deep_recursion(self, kwargs):
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
     def test_realpath_resolve_parents(self, kwargs):
         # We also need to resolve any symlinks in the parents of a relative
         # path passed to realpath. E.g.: current working directory is
@@ -749,7 +768,7 @@ def test_realpath_resolve_parents(self, kwargs):
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_resolve_before_normalizing(self, kwargs):
         # Bug #990669: Symbolic links should be resolved before we
         # normalize the path. E.g.: if we have directories 'a', 'k' and 'y'
@@ -778,7 +797,7 @@ def test_realpath_resolve_before_normalizing(self, kwargs):
 
     @os_helper.skip_unless_symlink
     @skip_if_ABSTFN_contains_backslash
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_resolve_first(self, kwargs):
         # Bug #1213894: The first component of the path, if not absolute,
         # must be resolved too.
@@ -816,7 +835,7 @@ def test_realpath_unreadable_symlink(self):
     @skip_if_ABSTFN_contains_backslash
     @unittest.skipIf(os.chmod not in os.supports_follow_symlinks, "Can't set 
symlink permissions")
     @unittest.skipIf(sys.platform != "darwin", "only macOS requires read 
permission to readlink()")
-    @_parameterize({'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_realpath_unreadable_symlink_strict(self, kwargs):
         try:
             os.symlink(ABSTFN+"1", ABSTFN)
@@ -842,6 +861,7 @@ def test_realpath_unreadable_directory(self):
             os.chmod(ABSTFN, 0o000)
             self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
             self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
+            self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN)
             self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN)
 
             try:
@@ -855,6 +875,8 @@ def test_realpath_unreadable_directory(self):
                              ABSTFN + '/k')
             self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
                               strict=True)
+            self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
+                              strict=ALL_BUT_LAST)
             self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
                               strict=ALLOW_MISSING)
 
@@ -862,6 +884,8 @@ def test_realpath_unreadable_directory(self):
                              ABSTFN + '/missing')
             self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
                               strict=True)
+            self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
+                              strict=ALL_BUT_LAST)
             self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
                               strict=ALLOW_MISSING)
         finally:
@@ -875,25 +899,30 @@ def test_realpath_nonterminal_file(self):
             with open(ABSTFN, 'w') as f:
                 f.write('test_posixpath wuz ere')
             self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
+            self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN)
             self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
             self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN)
 
             self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN)
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN)
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/..", strict=False), 
dirname(ABSTFN))
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), 
ABSTFN + "/subdir")
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + 
"/subdir", strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + 
"/subdir", strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
                               strict=ALLOW_MISSING)
@@ -908,25 +937,30 @@ def test_realpath_nonterminal_symlink_to_file(self):
                 f.write('test_posixpath wuz ere')
             os.symlink(ABSTFN + "1", ABSTFN)
             self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1")
+            self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN + 
"1")
             self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "1")
             self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN + 
"1")
 
             self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + 
"1")
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + 
"1")
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/..", strict=False), 
dirname(ABSTFN))
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), 
ABSTFN + "1/subdir")
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + 
"/subdir", strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + 
"/subdir", strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
                               strict=ALLOW_MISSING)
@@ -943,25 +977,30 @@ def 
test_realpath_nonterminal_symlink_to_symlinks_to_file(self):
             os.symlink(ABSTFN + "2", ABSTFN + "1")
             os.symlink(ABSTFN + "1", ABSTFN)
             self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2")
+            self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN + 
"2")
             self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
             self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
 
             self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + 
"2")
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + 
"2")
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/..", strict=False), 
dirname(ABSTFN))
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", 
strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", 
strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
                               strict=ALLOW_MISSING)
 
             self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), 
ABSTFN + "2/subdir")
+            self.assertRaises(NotADirectoryError, realpath, ABSTFN + 
"/subdir", strict=ALL_BUT_LAST)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + 
"/subdir", strict=True)
             self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
                               strict=ALLOW_MISSING)
@@ -970,6 +1009,98 @@ def 
test_realpath_nonterminal_symlink_to_symlinks_to_file(self):
             os_helper.unlink(ABSTFN + "1")
             os_helper.unlink(ABSTFN + "2")
 
+    @os_helper.skip_unless_symlink
+    def test_realpath_mode(self):
+        self.addCleanup(os_helper.rmdir, ABSTFN)
+        self.addCleanup(os_helper.rmdir, ABSTFN + "/dir")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/file")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/dir/file2")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/link")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/link2")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/broken")
+        self.addCleanup(os_helper.unlink, ABSTFN + "/cycle")
+
+        os.mkdir(ABSTFN)
+        os.mkdir(ABSTFN + "/dir")
+        open(ABSTFN + "/file", "wb").close()
+        open(ABSTFN + "/dir/file2", "wb").close()
+        os.symlink("file", ABSTFN + "/link")
+        os.symlink("dir", ABSTFN + "/link2")
+        os.symlink("nonexistent", ABSTFN + "/broken")
+        os.symlink("cycle", ABSTFN + "/cycle")
+        def check(path, modes, expected, errno=None):
+            if isinstance(expected, str):
+                assert errno is None
+                expected = expected.replace('/', os.sep)
+                for mode in modes:
+                    with self.subTest(mode=mode):
+                        self.assertEqual(realpath(path, 
strict=mode).replace('/', os.sep),
+                                         ABSTFN.replace('/', os.sep) + 
expected)
+            else:
+                for mode in modes:
+                    with self.subTest(mode=mode):
+                        with self.assertRaises(expected) as cm:
+                            realpath(path, strict=mode)
+                        if errno is not None:
+                            self.assertEqual(cm.exception.errno, errno)
+
+        self.enterContext(os_helper.change_cwd(ABSTFN))
+        all_modes = [False, ALLOW_MISSING, ALL_BUT_LAST, True]
+        check("file", all_modes, "/file")
+        check("file/", [False], "/file")
+        check("file/", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
+        check("file/file2", [False], "/file/file2")
+        check("file/file2", [ALLOW_MISSING, ALL_BUT_LAST, True], 
NotADirectoryError)
+        check("file/.", [False], "/file")
+        check("file/.", [ALLOW_MISSING, ALL_BUT_LAST, True], 
NotADirectoryError)
+        check("file/../link2", [False], "/dir")
+        check("file/../link2", [ALLOW_MISSING, ALL_BUT_LAST, True], 
NotADirectoryError)
+
+        check("dir", all_modes, "/dir")
+        check("dir/", all_modes, "/dir")
+        check("dir/file2", all_modes, "/dir/file2")
+
+        check("link", all_modes, "/file")
+        check("link/", [False], "/file")
+        check("link/", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
+        check("link/file2", [False], "/file/file2")
+        check("link/file2", [ALLOW_MISSING, ALL_BUT_LAST, True], 
NotADirectoryError)
+        check("link/.", [False], "/file")
+        check("link/.", [ALLOW_MISSING, ALL_BUT_LAST, True], 
NotADirectoryError)
+        check("link/../link", [False], "/file")
+        check("link/../link", [ALLOW_MISSING, ALL_BUT_LAST, True], 
NotADirectoryError)
+
+        check("link2", all_modes, "/dir")
+        check("link2/", all_modes, "/dir")
+        check("link2/file2", all_modes, "/dir/file2")
+
+        check("nonexistent", [False, ALLOW_MISSING, ALL_BUT_LAST], 
"/nonexistent")
+        check("nonexistent", [True], FileNotFoundError)
+        check("nonexistent/", [False, ALLOW_MISSING, ALL_BUT_LAST], 
"/nonexistent")
+        check("nonexistent/", [True], FileNotFoundError)
+        check("nonexistent/file", [False, ALLOW_MISSING], "/nonexistent/file")
+        check("nonexistent/file", [ALL_BUT_LAST, True], FileNotFoundError)
+        check("nonexistent/../link", [False, ALLOW_MISSING], "/file")
+        check("nonexistent/../link", [ALL_BUT_LAST, True], FileNotFoundError)
+
+        check("broken", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
+        check("broken", [True], FileNotFoundError)
+        check("broken/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
+        check("broken/", [True], FileNotFoundError)
+        check("broken/file", [False, ALLOW_MISSING], "/nonexistent/file")
+        check("broken/file", [ALL_BUT_LAST, True], FileNotFoundError)
+        check("broken/../link", [False, ALLOW_MISSING], "/file")
+        check("broken/../link", [ALL_BUT_LAST, True], FileNotFoundError)
+
+        check("cycle", [False], "/cycle")
+        check("cycle", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, 
errno.ELOOP)
+        check("cycle/", [False], "/cycle")
+        check("cycle/", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, 
errno.ELOOP)
+        check("cycle/file", [False], "/cycle/file")
+        check("cycle/file", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, 
errno.ELOOP)
+        check("cycle/../link", [False], "/file")
+        check("cycle/../link", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, 
errno.ELOOP)
+
     def test_relpath(self):
         (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar")
         try:
@@ -1152,7 +1283,7 @@ def test_path_normpath(self):
     def test_path_abspath(self):
         self.assertPathEqual(self.path.abspath)
 
-    @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
+    @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': 
ALLOW_MISSING})
     def test_path_realpath(self, kwargs):
         self.assertPathEqual(self.path.realpath)
 
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-20-11-51-17.gh-issue-71189.0LpTB1.rst 
b/Misc/NEWS.d/next/Library/2025-05-20-11-51-17.gh-issue-71189.0LpTB1.rst
new file mode 100644
index 00000000000000..b46ddcba59c830
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-20-11-51-17.gh-issue-71189.0LpTB1.rst
@@ -0,0 +1 @@
+Add support of the all-but-last mode in :func:`os.path.realpath`.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to