diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index f72aee19d8f332..c79c3da3e9801f 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -423,6 +423,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 + might be missing, but other errors are not ignored. If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than :exc:`FileNotFoundError` are re-raised (as with ``strict=True``). @@ -447,8 +449,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 @@ -456,6 +464,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 f06d4c84ea53d1..45219e21e76d5d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -122,13 +122,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 9363f564aab7a6..cb2b9bd049e87e 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 9cdc16480f9afe..1485f9e32157fc 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 d38f3bd5872bcd..301b390132b898 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`.