From e8c859c188042664c2c5a29b8013220319f6591b Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 5 Apr 2024 13:18:43 +0300 Subject: [PATCH 01/11] gh-71189: Support all-but-last mode in posixpath.realpath() --- Lib/posixpath.py | 45 ++++++++----- Lib/test/test_posixpath.py | 128 ++++++++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 17 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 76ee721bfb5e33..bf9472c545ca25 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -399,17 +399,21 @@ def abspath(path): # Return a canonical path (i.e. the absolute location of a file on the # filesystem). +# A singleton with true boolean value +ALL_BUT_LAST = ['ALL_BUT_LAST'] + def realpath(filename, *, strict=False): """Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.""" filename = os.fspath(filename) - path, ok = _joinrealpath(filename[:0], filename, strict, {}) + path = _joinrealpath(filename[:0], filename, {}, + strict, strict is ALL_BUT_LAST, False) return abspath(path) # Join two paths, normalizing and eliminating any symbolic links # encountered in the second path. # Two leading slashes are replaced by a single slash. -def _joinrealpath(path, rest, strict, seen): +def _joinrealpath(path, rest, seen, strict, last, isdir): if isinstance(path, bytes): sep = b'/' curdir = b'.' @@ -424,7 +428,7 @@ def _joinrealpath(path, rest, strict, seen): path = sep while rest: - name, _, rest = rest.partition(sep) + name, hassep, rest = rest.partition(sep) if not name or name == curdir: # current dir continue @@ -445,13 +449,18 @@ def _joinrealpath(path, rest, strict, seen): newpath = join(path, name) try: st = os.lstat(newpath) - except OSError: + except OSError as e: + if (last and not rest.strip(sep) and + isinstance(e, FileNotFoundError)): + return newpath if strict: raise - is_link = False - else: - is_link = stat.S_ISLNK(st.st_mode) - if not is_link: + path = newpath + continue + if not stat.S_ISLNK(st.st_mode): + if (strict and not stat.S_ISDIR(st.st_mode) + and (isdir or hassep)): + os.lstat(newpath + sep) # raises NotADirectoryError path = newpath continue # Resolve the symbolic link @@ -465,16 +474,22 @@ def _joinrealpath(path, rest, strict, seen): if strict: # Raise OSError(errno.ELOOP) os.stat(newpath) - else: - # Return already resolved part + rest of the path unchanged. - return join(newpath, rest), False + path = newpath + continue seen[newpath] = None # not resolved symlink - path, ok = _joinrealpath(path, os.readlink(newpath), strict, seen) - if not ok: - return join(path, rest), False + try: + link = os.readlink(newpath) + except OSError: + if strict: + raise + path = newpath + else: + path = _joinrealpath(path, link, seen, strict, + last and not rest.strip(sep), + isdir or hassep) seen[newpath] = path # resolved symlink - return path, True + return path supports_unicode_filenames = (sys.platform == 'darwin') diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index cbb7c4c52d9697..eebbffea2beab7 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -1,8 +1,9 @@ +import errno import os import posixpath import sys import unittest -from posixpath import realpath, abspath, dirname, basename +from posixpath import realpath, abspath, dirname, basename, ALL_BUT_LAST from test import test_genericpath from test.support import import_helper from test.support import os_helper @@ -475,7 +476,7 @@ def test_realpath_symlink_loops(self): self.assertEqual(realpath(ABSTFN+"1/../x"), dirname(ABSTFN) + "/x") os.symlink(ABSTFN+"x", ABSTFN+"y") self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "y"), - ABSTFN + "y") + ABSTFN + "x") self.assertEqual(realpath(ABSTFN+"1/../" + basename(ABSTFN) + "1"), ABSTFN + "1") @@ -637,6 +638,129 @@ def test_realpath_resolve_first(self): safe_rmdir(ABSTFN + "/k") safe_rmdir(ABSTFN) + @os_helper.skip_unless_symlink + @skip_if_ABSTFN_contains_backslash + def test_realpath_mode(self): + try: + 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("nonexisting", ABSTFN + "/broken") + os.symlink("cycle", ABSTFN + "/cycle") + def check(path, mode, expected, errno=None): + if isinstance(expected, str): + assert errno is None + self.assertEqual(realpath(path, strict=mode), ABSTFN + expected) + else: + with self.assertRaises(expected) as cm: + realpath(path, strict=mode) + if errno is not None: + self.assertEqual(cm.exception.errno, errno) + + with os_helper.change_cwd(ABSTFN): + check("file", False, "/file") + check("file", ALL_BUT_LAST, "/file") + check("file", True, "/file") + check("file/", False, "/file") + check("file/", ALL_BUT_LAST, NotADirectoryError) + check("file/", True, NotADirectoryError) + check("file/file2", False, "/file/file2") + check("file/file2", ALL_BUT_LAST, NotADirectoryError) + check("file/file2", True, NotADirectoryError) + check("file/.", False, "/file") + check("file/.", ALL_BUT_LAST, NotADirectoryError) + check("file/.", True, NotADirectoryError) + check("file/../link", False, "/file") + check("file/../link", ALL_BUT_LAST, NotADirectoryError) + check("file/../link", True, NotADirectoryError) + + check("dir", False, "/dir") + check("dir", ALL_BUT_LAST, "/dir") + check("dir", True, "/dir") + check("dir/", False, "/dir") + check("dir/", ALL_BUT_LAST, "/dir") + check("dir/", True, "/dir") + check("dir/file2", False, "/dir/file2") + check("dir/file2", ALL_BUT_LAST, "/dir/file2") + check("dir/file2", True, "/dir/file2") + + check("link", False, "/file") + check("link", ALL_BUT_LAST, "/file") + check("link", True, "/file") + check("link/", False, "/file") + check("link/", ALL_BUT_LAST, NotADirectoryError) + check("link/", True, NotADirectoryError) + check("link/file2", False, "/file/file2") + check("link/file2", ALL_BUT_LAST, NotADirectoryError) + check("link/file2", True, NotADirectoryError) + check("link/.", False, "/file") + check("link/.", ALL_BUT_LAST, NotADirectoryError) + check("link/.", True, NotADirectoryError) + check("link/../link", False, "/file") + check("link/../link", ALL_BUT_LAST, NotADirectoryError) + check("link/../link", True, NotADirectoryError) + + check("link2", False, "/dir") + check("link2", ALL_BUT_LAST, "/dir") + check("link2", True, "/dir") + check("link2/", False, "/dir") + check("link2/", ALL_BUT_LAST, "/dir") + check("link2/", True, "/dir") + check("link2/file2", False, "/dir/file2") + check("link2/file2", ALL_BUT_LAST, "/dir/file2") + check("link2/file2", True, "/dir/file2") + + check("nonexisting", False, "/nonexisting") + check("nonexisting", ALL_BUT_LAST, "/nonexisting") + check("nonexisting", True, FileNotFoundError) + check("nonexisting/", False, "/nonexisting") + check("nonexisting/", ALL_BUT_LAST, "/nonexisting") + check("nonexisting/", True, FileNotFoundError) + check("nonexisting/file", False, "/nonexisting/file") + check("nonexisting/file", ALL_BUT_LAST, FileNotFoundError) + check("nonexisting/file", True, FileNotFoundError) + check("nonexisting/../link", False, "/file") + check("nonexisting/../link", ALL_BUT_LAST, FileNotFoundError) + check("nonexisting/../link", True, FileNotFoundError) + + check("broken", False, "/nonexisting") + check("broken", ALL_BUT_LAST, "/nonexisting") + check("broken", True, FileNotFoundError) + check("broken/", False, "/nonexisting") + check("broken/", ALL_BUT_LAST, "/nonexisting") + check("broken/", True, FileNotFoundError) + check("broken/file", False, "/nonexisting/file") + check("broken/file", ALL_BUT_LAST, FileNotFoundError) + check("broken/file", True, FileNotFoundError) + check("broken/../link", False, "/file") + check("broken/../link", ALL_BUT_LAST, FileNotFoundError) + check("broken/../link", True, FileNotFoundError) + + check("cycle", False, "/cycle") + check("cycle", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle", True, OSError, errno.ELOOP) + check("cycle/", False, "/cycle") + check("cycle/", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/", True, OSError, errno.ELOOP) + check("cycle/file", False, "/cycle/file") + check("cycle/file", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/file", True, OSError, errno.ELOOP) + check("cycle/../link", False, "/file") + check("cycle/../link", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/../link", True, OSError, errno.ELOOP) + finally: + os_helper.unlink(ABSTFN + "/file") + os_helper.unlink(ABSTFN + "/dir/file2") + os_helper.unlink(ABSTFN + "/link") + os_helper.unlink(ABSTFN + "/link2") + os_helper.unlink(ABSTFN + "/broken") + os_helper.unlink(ABSTFN + "/cycle") + os_helper.rmdir(ABSTFN + "/dir") + os_helper.rmdir(ABSTFN) + def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: From b76a1c5f349ef56190e54ba97801914ee46d3b71 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 19 May 2025 11:46:51 +0300 Subject: [PATCH 02/11] Implement ALL_BUT_LAST on Windows. --- Lib/ntpath.py | 11 +++- Lib/test/test_ntpath.py | 127 +++++++++++++++++++++++++++++++++++++ Lib/test/test_posixpath.py | 5 +- 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 5481bb8888ef59..81ec9ee3bf6238 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -597,6 +597,9 @@ def abspath(path): path = join(getcwd(), path) return normpath(path) +# A singleton with true boolean value +ALL_BUT_LAST = ['ALL_BUT_LAST'] + try: from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink except ImportError: @@ -735,7 +738,13 @@ def realpath(path, *, strict=False): path = normpath(path) except OSError as ex: if strict: - raise + if strict is not ALL_BUT_LAST or not isinstance(ex, FileNotFoundError): + raise + dirname, basename = split(path) + if not basename: + dirname, basename = split(path) + if not isdir(dirname): + raise initial_winerror = ex.winerror path = _getfinalpathname_nonstrict(path) # The path returned by _getfinalpathname will always start with \\?\ - diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index c10387b58e3f9c..d15362c97855ec 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 @@ -705,6 +706,132 @@ def test_realpath_permission(self): self.assertPathEqual(test_file, ntpath.realpath(test_file_short)) + @os_helper.skip_unless_symlink + def test_realpath_mode(self): + realpath = ntpath.realpath + ALL_BUT_LAST = ntpath.ALL_BUT_LAST + ABSTFN = ntpath.abspath(os_helper.TESTFN) + try: + 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, mode, expected, errno=None): + path = path.replace('/', '\\') + if isinstance(expected, str): + assert errno is None + self.assertEqual(realpath(path, strict=mode), ABSTFN + expected.replace('/', '\\')) + else: + with self.assertRaises(expected) as cm: + realpath(path, strict=mode) + if errno is not None: + self.assertEqual(cm.exception.errno, errno) + + with os_helper.change_cwd(ABSTFN): + check("file", False, "/file") + check("file", ALL_BUT_LAST, "/file") + check("file", True, "/file") + check("file/", False, "/file") + # check("file/", ALL_BUT_LAST, NotADirectoryError) + # check("file/", True, NotADirectoryError) + check("file/file2", False, "/file/file2") + # check("file/file2", ALL_BUT_LAST, NotADirectoryError) + # check("file/file2", True, NotADirectoryError) + check("file/.", False, "/file") + # check("file/.", ALL_BUT_LAST, NotADirectoryError) + # check("file/.", True, NotADirectoryError) + check("file/../link2", False, "/dir") + check("file/../link2", ALL_BUT_LAST, "/dir") + check("file/../link2", True, "/dir") + + check("dir", False, "/dir") + check("dir", ALL_BUT_LAST, "/dir") + check("dir", True, "/dir") + check("dir/", False, "/dir") + check("dir/", ALL_BUT_LAST, "/dir") + check("dir/", True, "/dir") + check("dir/file2", False, "/dir/file2") + check("dir/file2", ALL_BUT_LAST, "/dir/file2") + check("dir/file2", True, "/dir/file2") + + check("link", False, "/file") + check("link", ALL_BUT_LAST, "/file") + check("link", True, "/file") + check("link/", False, "/file") + # check("link/", ALL_BUT_LAST, NotADirectoryError) + # check("link/", True, NotADirectoryError) + check("link/file2", False, "/file/file2") + # check("link/file2", ALL_BUT_LAST, NotADirectoryError) + # check("link/file2", True, NotADirectoryError) + check("link/.", False, "/file") + # check("link/.", ALL_BUT_LAST, NotADirectoryError) + # check("link/.", True, NotADirectoryError) + check("link/../link", False, "/file") + check("link/../link", ALL_BUT_LAST, "/file") + check("link/../link", True, "/file") + + check("link2", False, "/dir") + check("link2", ALL_BUT_LAST, "/dir") + check("link2", True, "/dir") + check("link2/", False, "/dir") + check("link2/", ALL_BUT_LAST, "/dir") + check("link2/", True, "/dir") + check("link2/file2", False, "/dir/file2") + check("link2/file2", ALL_BUT_LAST, "/dir/file2") + check("link2/file2", True, "/dir/file2") + + check("nonexistent", False, "/nonexistent") + check("nonexistent", ALL_BUT_LAST, "/nonexistent") + check("nonexistent", True, FileNotFoundError) + check("nonexistent/", False, "/nonexistent") + check("nonexistent/", ALL_BUT_LAST, "/nonexistent") + check("nonexistent/", True, FileNotFoundError) + check("nonexistent/file", False, "/nonexistent/file") + check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) + check("nonexistent/file", True, FileNotFoundError) + check("nonexistent/../link", False, "/file") + check("nonexistent/../link", ALL_BUT_LAST, "/file") + check("nonexistent/../link", True, "/file") + + check("broken", False, "/nonexistent") + check("broken", ALL_BUT_LAST, "/nonexistent") + check("broken", True, FileNotFoundError) + check("broken/", False, "/nonexistent") + check("broken/", ALL_BUT_LAST, "/nonexistent") + check("broken/", True, FileNotFoundError) + check("broken/file", False, "/nonexistent/file") + check("broken/file", ALL_BUT_LAST, FileNotFoundError) + check("broken/file", True, FileNotFoundError) + check("broken/../link", False, "/file") + check("broken/../link", ALL_BUT_LAST, "/file") + check("broken/../link", True, "/file") + + check("cycle", False, "/cycle") + check("cycle", ALL_BUT_LAST, OSError, errno.EINVAL) + check("cycle", True, OSError, errno.EINVAL) + check("cycle/", False, "/cycle") + check("cycle/", ALL_BUT_LAST, OSError, errno.EINVAL) + check("cycle/", True, OSError, errno.EINVAL) + check("cycle/file", False, "/cycle/file") + check("cycle/file", ALL_BUT_LAST, OSError, errno.EINVAL) + check("cycle/file", True, OSError, errno.EINVAL) + check("cycle/../link", False, "/file") + check("cycle/../link", ALL_BUT_LAST, "/file") + check("cycle/../link", True, "/file") + finally: + os_helper.unlink(ABSTFN + "/file") + os_helper.unlink(ABSTFN + "/dir/file2") + os_helper.unlink(ABSTFN + "/link") + os_helper.unlink(ABSTFN + "/link2") + os_helper.unlink(ABSTFN + "/broken") + os_helper.unlink(ABSTFN + "/cycle") + os_helper.rmdir(ABSTFN + "/dir") + os_helper.rmdir(ABSTFN) + 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 da55f664bfd306..1c305e0472fd1a 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -767,7 +767,7 @@ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self): os_helper.unlink(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + # @skip_if_ABSTFN_contains_backslash def test_realpath_mode(self): try: os.mkdir(ABSTFN) @@ -781,7 +781,8 @@ def test_realpath_mode(self): def check(path, mode, expected, errno=None): if isinstance(expected, str): assert errno is None - self.assertEqual(realpath(path, strict=mode), ABSTFN + expected) + self.assertEqual(realpath(path, strict=mode).replace('/', os.sep), + ABSTFN.replace('/', os.sep) + expected.replace('/', os.sep)) else: with self.assertRaises(expected) as cm: realpath(path, strict=mode) From d93263ff4001c93ddb22647c3ad0cc705eb1b1b7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 20 May 2025 11:03:34 +0300 Subject: [PATCH 03/11] Unindent tests. --- Lib/test/test_ntpath.py | 240 ++++++++++++++++++------------------- Lib/test/test_posixpath.py | 239 ++++++++++++++++++------------------ 2 files changed, 239 insertions(+), 240 deletions(-) diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 3de4e5b6450750..72be868add7736 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -832,130 +832,130 @@ 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 ALL_BUT_LAST = ntpath.ALL_BUT_LAST ABSTFN = ntpath.abspath(os_helper.TESTFN) - try: - 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, mode, expected, errno=None): - path = path.replace('/', '\\') - if isinstance(expected, str): - assert errno is None - self.assertEqual(realpath(path, strict=mode), ABSTFN + expected.replace('/', '\\')) - else: - with self.assertRaises(expected) as cm: - realpath(path, strict=mode) - if errno is not None: - self.assertEqual(cm.exception.errno, errno) - - with os_helper.change_cwd(ABSTFN): - check("file", False, "/file") - check("file", ALL_BUT_LAST, "/file") - check("file", True, "/file") - check("file/", False, "/file") - # check("file/", ALL_BUT_LAST, NotADirectoryError) - # check("file/", True, NotADirectoryError) - check("file/file2", False, "/file/file2") - # check("file/file2", ALL_BUT_LAST, NotADirectoryError) - # check("file/file2", True, NotADirectoryError) - check("file/.", False, "/file") - # check("file/.", ALL_BUT_LAST, NotADirectoryError) - # check("file/.", True, NotADirectoryError) - check("file/../link2", False, "/dir") - check("file/../link2", ALL_BUT_LAST, "/dir") - check("file/../link2", True, "/dir") - - check("dir", False, "/dir") - check("dir", ALL_BUT_LAST, "/dir") - check("dir", True, "/dir") - check("dir/", False, "/dir") - check("dir/", ALL_BUT_LAST, "/dir") - check("dir/", True, "/dir") - check("dir/file2", False, "/dir/file2") - check("dir/file2", ALL_BUT_LAST, "/dir/file2") - check("dir/file2", True, "/dir/file2") - - check("link", False, "/file") - check("link", ALL_BUT_LAST, "/file") - check("link", True, "/file") - check("link/", False, "/file") - # check("link/", ALL_BUT_LAST, NotADirectoryError) - # check("link/", True, NotADirectoryError) - check("link/file2", False, "/file/file2") - # check("link/file2", ALL_BUT_LAST, NotADirectoryError) - # check("link/file2", True, NotADirectoryError) - check("link/.", False, "/file") - # check("link/.", ALL_BUT_LAST, NotADirectoryError) - # check("link/.", True, NotADirectoryError) - check("link/../link", False, "/file") - check("link/../link", ALL_BUT_LAST, "/file") - check("link/../link", True, "/file") - - check("link2", False, "/dir") - check("link2", ALL_BUT_LAST, "/dir") - check("link2", True, "/dir") - check("link2/", False, "/dir") - check("link2/", ALL_BUT_LAST, "/dir") - check("link2/", True, "/dir") - check("link2/file2", False, "/dir/file2") - check("link2/file2", ALL_BUT_LAST, "/dir/file2") - check("link2/file2", True, "/dir/file2") - - check("nonexistent", False, "/nonexistent") - check("nonexistent", ALL_BUT_LAST, "/nonexistent") - check("nonexistent", True, FileNotFoundError) - check("nonexistent/", False, "/nonexistent") - check("nonexistent/", ALL_BUT_LAST, "/nonexistent") - check("nonexistent/", True, FileNotFoundError) - check("nonexistent/file", False, "/nonexistent/file") - check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) - check("nonexistent/file", True, FileNotFoundError) - check("nonexistent/../link", False, "/file") - check("nonexistent/../link", ALL_BUT_LAST, "/file") - check("nonexistent/../link", True, "/file") - - check("broken", False, "/nonexistent") - check("broken", ALL_BUT_LAST, "/nonexistent") - check("broken", True, FileNotFoundError) - check("broken/", False, "/nonexistent") - check("broken/", ALL_BUT_LAST, "/nonexistent") - check("broken/", True, FileNotFoundError) - check("broken/file", False, "/nonexistent/file") - check("broken/file", ALL_BUT_LAST, FileNotFoundError) - check("broken/file", True, FileNotFoundError) - check("broken/../link", False, "/file") - check("broken/../link", ALL_BUT_LAST, "/file") - check("broken/../link", True, "/file") - - check("cycle", False, "/cycle") - check("cycle", ALL_BUT_LAST, OSError, errno.EINVAL) - check("cycle", True, OSError, errno.EINVAL) - check("cycle/", False, "/cycle") - check("cycle/", ALL_BUT_LAST, OSError, errno.EINVAL) - check("cycle/", True, OSError, errno.EINVAL) - check("cycle/file", False, "/cycle/file") - check("cycle/file", ALL_BUT_LAST, OSError, errno.EINVAL) - check("cycle/file", True, OSError, errno.EINVAL) - check("cycle/../link", False, "/file") - check("cycle/../link", ALL_BUT_LAST, "/file") - check("cycle/../link", True, "/file") - finally: - os_helper.unlink(ABSTFN + "/file") - os_helper.unlink(ABSTFN + "/dir/file2") - os_helper.unlink(ABSTFN + "/link") - os_helper.unlink(ABSTFN + "/link2") - os_helper.unlink(ABSTFN + "/broken") - os_helper.unlink(ABSTFN + "/cycle") - os_helper.rmdir(ABSTFN + "/dir") - os_helper.rmdir(ABSTFN) + 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, mode, expected, errno=None): + path = path.replace('/', '\\') + if isinstance(expected, str): + assert errno is None + self.assertEqual(realpath(path, strict=mode), ABSTFN + expected.replace('/', '\\')) + else: + 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)) + check("file", False, "/file") + check("file", ALL_BUT_LAST, "/file") + check("file", True, "/file") + check("file/", False, "/file") + # check("file/", ALL_BUT_LAST, NotADirectoryError) + # check("file/", True, NotADirectoryError) + check("file/file2", False, "/file/file2") + # check("file/file2", ALL_BUT_LAST, NotADirectoryError) + # check("file/file2", True, NotADirectoryError) + check("file/.", False, "/file") + # check("file/.", ALL_BUT_LAST, NotADirectoryError) + # check("file/.", True, NotADirectoryError) + check("file/../link2", False, "/dir") + check("file/../link2", ALL_BUT_LAST, "/dir") + check("file/../link2", True, "/dir") + + check("dir", False, "/dir") + check("dir", ALL_BUT_LAST, "/dir") + check("dir", True, "/dir") + check("dir/", False, "/dir") + check("dir/", ALL_BUT_LAST, "/dir") + check("dir/", True, "/dir") + check("dir/file2", False, "/dir/file2") + check("dir/file2", ALL_BUT_LAST, "/dir/file2") + check("dir/file2", True, "/dir/file2") + + check("link", False, "/file") + check("link", ALL_BUT_LAST, "/file") + check("link", True, "/file") + check("link/", False, "/file") + # check("link/", ALL_BUT_LAST, NotADirectoryError) + # check("link/", True, NotADirectoryError) + check("link/file2", False, "/file/file2") + # check("link/file2", ALL_BUT_LAST, NotADirectoryError) + # check("link/file2", True, NotADirectoryError) + check("link/.", False, "/file") + # check("link/.", ALL_BUT_LAST, NotADirectoryError) + # check("link/.", True, NotADirectoryError) + check("link/../link", False, "/file") + check("link/../link", ALL_BUT_LAST, "/file") + check("link/../link", True, "/file") + + check("link2", False, "/dir") + check("link2", ALL_BUT_LAST, "/dir") + check("link2", True, "/dir") + check("link2/", False, "/dir") + check("link2/", ALL_BUT_LAST, "/dir") + check("link2/", True, "/dir") + check("link2/file2", False, "/dir/file2") + check("link2/file2", ALL_BUT_LAST, "/dir/file2") + check("link2/file2", True, "/dir/file2") + + check("nonexistent", False, "/nonexistent") + check("nonexistent", ALL_BUT_LAST, "/nonexistent") + check("nonexistent", True, FileNotFoundError) + check("nonexistent/", False, "/nonexistent") + check("nonexistent/", ALL_BUT_LAST, "/nonexistent") + check("nonexistent/", True, FileNotFoundError) + check("nonexistent/file", False, "/nonexistent/file") + check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) + check("nonexistent/file", True, FileNotFoundError) + check("nonexistent/../link", False, "/file") + check("nonexistent/../link", ALL_BUT_LAST, "/file") + check("nonexistent/../link", True, "/file") + + check("broken", False, "/nonexistent") + check("broken", ALL_BUT_LAST, "/nonexistent") + check("broken", True, FileNotFoundError) + check("broken/", False, "/nonexistent") + check("broken/", ALL_BUT_LAST, "/nonexistent") + check("broken/", True, FileNotFoundError) + check("broken/file", False, "/nonexistent/file") + check("broken/file", ALL_BUT_LAST, FileNotFoundError) + check("broken/file", True, FileNotFoundError) + check("broken/../link", False, "/file") + check("broken/../link", ALL_BUT_LAST, "/file") + check("broken/../link", True, "/file") + + check("cycle", False, "/cycle") + check("cycle", ALL_BUT_LAST, OSError, errno.EINVAL) + check("cycle", True, OSError, errno.EINVAL) + check("cycle/", False, "/cycle") + check("cycle/", ALL_BUT_LAST, OSError, errno.EINVAL) + check("cycle/", True, OSError, errno.EINVAL) + check("cycle/file", False, "/cycle/file") + check("cycle/file", ALL_BUT_LAST, OSError, errno.EINVAL) + check("cycle/file", True, OSError, errno.EINVAL) + check("cycle/../link", False, "/file") + check("cycle/../link", ALL_BUT_LAST, "/file") + check("cycle/../link", True, "/file") def test_expandvars(self): with os_helper.EnvironmentVarGuard() as env: diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 9f07ad2f0bc081..77e4c3e0c717e1 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -839,126 +839,125 @@ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self): @os_helper.skip_unless_symlink # @skip_if_ABSTFN_contains_backslash def test_realpath_mode(self): - try: - 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, mode, expected, errno=None): - if isinstance(expected, str): - assert errno is None - self.assertEqual(realpath(path, strict=mode).replace('/', os.sep), - ABSTFN.replace('/', os.sep) + expected.replace('/', os.sep)) - else: - with self.assertRaises(expected) as cm: - realpath(path, strict=mode) - if errno is not None: - self.assertEqual(cm.exception.errno, errno) - - with os_helper.change_cwd(ABSTFN): - check("file", False, "/file") - check("file", ALL_BUT_LAST, "/file") - check("file", True, "/file") - check("file/", False, "/file") - check("file/", ALL_BUT_LAST, NotADirectoryError) - check("file/", True, NotADirectoryError) - check("file/file2", False, "/file/file2") - check("file/file2", ALL_BUT_LAST, NotADirectoryError) - check("file/file2", True, NotADirectoryError) - check("file/.", False, "/file") - check("file/.", ALL_BUT_LAST, NotADirectoryError) - check("file/.", True, NotADirectoryError) - check("file/../link2", False, "/dir") - check("file/../link2", ALL_BUT_LAST, NotADirectoryError) - check("file/../link2", True, NotADirectoryError) - - check("dir", False, "/dir") - check("dir", ALL_BUT_LAST, "/dir") - check("dir", True, "/dir") - check("dir/", False, "/dir") - check("dir/", ALL_BUT_LAST, "/dir") - check("dir/", True, "/dir") - check("dir/file2", False, "/dir/file2") - check("dir/file2", ALL_BUT_LAST, "/dir/file2") - check("dir/file2", True, "/dir/file2") - - check("link", False, "/file") - check("link", ALL_BUT_LAST, "/file") - check("link", True, "/file") - check("link/", False, "/file") - check("link/", ALL_BUT_LAST, NotADirectoryError) - check("link/", True, NotADirectoryError) - check("link/file2", False, "/file/file2") - check("link/file2", ALL_BUT_LAST, NotADirectoryError) - check("link/file2", True, NotADirectoryError) - check("link/.", False, "/file") - check("link/.", ALL_BUT_LAST, NotADirectoryError) - check("link/.", True, NotADirectoryError) - check("link/../link", False, "/file") - check("link/../link", ALL_BUT_LAST, NotADirectoryError) - check("link/../link", True, NotADirectoryError) - - check("link2", False, "/dir") - check("link2", ALL_BUT_LAST, "/dir") - check("link2", True, "/dir") - check("link2/", False, "/dir") - check("link2/", ALL_BUT_LAST, "/dir") - check("link2/", True, "/dir") - check("link2/file2", False, "/dir/file2") - check("link2/file2", ALL_BUT_LAST, "/dir/file2") - check("link2/file2", True, "/dir/file2") - - check("nonexistent", False, "/nonexistent") - check("nonexistent", ALL_BUT_LAST, "/nonexistent") - check("nonexistent", True, FileNotFoundError) - check("nonexistent/", False, "/nonexistent") - check("nonexistent/", ALL_BUT_LAST, "/nonexistent") - check("nonexistent/", True, FileNotFoundError) - check("nonexistent/file", False, "/nonexistent/file") - check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) - check("nonexistent/file", True, FileNotFoundError) - check("nonexistent/../link", False, "/file") - check("nonexistent/../link", ALL_BUT_LAST, FileNotFoundError) - check("nonexistent/../link", True, FileNotFoundError) - - check("broken", False, "/nonexistent") - check("broken", ALL_BUT_LAST, "/nonexistent") - check("broken", True, FileNotFoundError) - check("broken/", False, "/nonexistent") - check("broken/", ALL_BUT_LAST, "/nonexistent") - check("broken/", True, FileNotFoundError) - check("broken/file", False, "/nonexistent/file") - check("broken/file", ALL_BUT_LAST, FileNotFoundError) - check("broken/file", True, FileNotFoundError) - check("broken/../link", False, "/file") - check("broken/../link", ALL_BUT_LAST, FileNotFoundError) - check("broken/../link", True, FileNotFoundError) - - check("cycle", False, "/cycle") - check("cycle", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle", True, OSError, errno.ELOOP) - check("cycle/", False, "/cycle") - check("cycle/", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle/", True, OSError, errno.ELOOP) - check("cycle/file", False, "/cycle/file") - check("cycle/file", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle/file", True, OSError, errno.ELOOP) - check("cycle/../link", False, "/file") - check("cycle/../link", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle/../link", True, OSError, errno.ELOOP) - finally: - os_helper.unlink(ABSTFN + "/file") - os_helper.unlink(ABSTFN + "/dir/file2") - os_helper.unlink(ABSTFN + "/link") - os_helper.unlink(ABSTFN + "/link2") - os_helper.unlink(ABSTFN + "/broken") - os_helper.unlink(ABSTFN + "/cycle") - os_helper.rmdir(ABSTFN + "/dir") - os_helper.rmdir(ABSTFN) + 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, mode, expected, errno=None): + if isinstance(expected, str): + assert errno is None + self.assertEqual(realpath(path, strict=mode).replace('/', os.sep), + ABSTFN.replace('/', os.sep) + expected.replace('/', os.sep)) + else: + 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)) + check("file", False, "/file") + check("file", ALL_BUT_LAST, "/file") + check("file", True, "/file") + check("file/", False, "/file") + check("file/", ALL_BUT_LAST, NotADirectoryError) + check("file/", True, NotADirectoryError) + check("file/file2", False, "/file/file2") + check("file/file2", ALL_BUT_LAST, NotADirectoryError) + check("file/file2", True, NotADirectoryError) + check("file/.", False, "/file") + check("file/.", ALL_BUT_LAST, NotADirectoryError) + check("file/.", True, NotADirectoryError) + check("file/../link2", False, "/dir") + check("file/../link2", ALL_BUT_LAST, NotADirectoryError) + check("file/../link2", True, NotADirectoryError) + + check("dir", False, "/dir") + check("dir", ALL_BUT_LAST, "/dir") + check("dir", True, "/dir") + check("dir/", False, "/dir") + check("dir/", ALL_BUT_LAST, "/dir") + check("dir/", True, "/dir") + check("dir/file2", False, "/dir/file2") + check("dir/file2", ALL_BUT_LAST, "/dir/file2") + check("dir/file2", True, "/dir/file2") + + check("link", False, "/file") + check("link", ALL_BUT_LAST, "/file") + check("link", True, "/file") + check("link/", False, "/file") + check("link/", ALL_BUT_LAST, NotADirectoryError) + check("link/", True, NotADirectoryError) + check("link/file2", False, "/file/file2") + check("link/file2", ALL_BUT_LAST, NotADirectoryError) + check("link/file2", True, NotADirectoryError) + check("link/.", False, "/file") + check("link/.", ALL_BUT_LAST, NotADirectoryError) + check("link/.", True, NotADirectoryError) + check("link/../link", False, "/file") + check("link/../link", ALL_BUT_LAST, NotADirectoryError) + check("link/../link", True, NotADirectoryError) + + check("link2", False, "/dir") + check("link2", ALL_BUT_LAST, "/dir") + check("link2", True, "/dir") + check("link2/", False, "/dir") + check("link2/", ALL_BUT_LAST, "/dir") + check("link2/", True, "/dir") + check("link2/file2", False, "/dir/file2") + check("link2/file2", ALL_BUT_LAST, "/dir/file2") + check("link2/file2", True, "/dir/file2") + + check("nonexistent", False, "/nonexistent") + check("nonexistent", ALL_BUT_LAST, "/nonexistent") + check("nonexistent", True, FileNotFoundError) + check("nonexistent/", False, "/nonexistent") + check("nonexistent/", ALL_BUT_LAST, "/nonexistent") + check("nonexistent/", True, FileNotFoundError) + check("nonexistent/file", False, "/nonexistent/file") + check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) + check("nonexistent/file", True, FileNotFoundError) + check("nonexistent/../link", False, "/file") + check("nonexistent/../link", ALL_BUT_LAST, FileNotFoundError) + check("nonexistent/../link", True, FileNotFoundError) + + check("broken", False, "/nonexistent") + check("broken", ALL_BUT_LAST, "/nonexistent") + check("broken", True, FileNotFoundError) + check("broken/", False, "/nonexistent") + check("broken/", ALL_BUT_LAST, "/nonexistent") + check("broken/", True, FileNotFoundError) + check("broken/file", False, "/nonexistent/file") + check("broken/file", ALL_BUT_LAST, FileNotFoundError) + check("broken/file", True, FileNotFoundError) + check("broken/../link", False, "/file") + check("broken/../link", ALL_BUT_LAST, FileNotFoundError) + check("broken/../link", True, FileNotFoundError) + + check("cycle", False, "/cycle") + check("cycle", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle", True, OSError, errno.ELOOP) + check("cycle/", False, "/cycle") + check("cycle/", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/", True, OSError, errno.ELOOP) + check("cycle/file", False, "/cycle/file") + check("cycle/file", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/file", True, OSError, errno.ELOOP) + check("cycle/../link", False, "/file") + check("cycle/../link", ALL_BUT_LAST, OSError, errno.ELOOP) + check("cycle/../link", True, OSError, errno.ELOOP) def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") From 621f476a9d24c7faaded35ac2122fe14ed3c7131 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 20 May 2025 11:30:54 +0300 Subject: [PATCH 04/11] Uncomment file on Windows. --- Lib/test/test_ntpath.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 72be868add7736..a6f6a2df96db98 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -858,7 +858,8 @@ def check(path, mode, expected, errno=None): path = path.replace('/', '\\') if isinstance(expected, str): assert errno is None - self.assertEqual(realpath(path, strict=mode), ABSTFN + expected.replace('/', '\\')) + self.assertEqual(realpath(path, strict=mode), + ABSTFN + expected.replace('/', '\\')) else: with self.assertRaises(expected) as cm: realpath(path, strict=mode) @@ -870,14 +871,14 @@ def check(path, mode, expected, errno=None): check("file", ALL_BUT_LAST, "/file") check("file", True, "/file") check("file/", False, "/file") - # check("file/", ALL_BUT_LAST, NotADirectoryError) - # check("file/", True, NotADirectoryError) + check("file/", ALL_BUT_LAST, "/file") + check("file/", True, "/file") check("file/file2", False, "/file/file2") - # check("file/file2", ALL_BUT_LAST, NotADirectoryError) - # check("file/file2", True, NotADirectoryError) + check("file/file2", ALL_BUT_LAST, FileNotFoundError) + check("file/file2", True, FileNotFoundError) check("file/.", False, "/file") - # check("file/.", ALL_BUT_LAST, NotADirectoryError) - # check("file/.", True, NotADirectoryError) + check("file/.", ALL_BUT_LAST, "/file") + check("file/.", True, "/file") check("file/../link2", False, "/dir") check("file/../link2", ALL_BUT_LAST, "/dir") check("file/../link2", True, "/dir") @@ -896,14 +897,14 @@ def check(path, mode, expected, errno=None): check("link", ALL_BUT_LAST, "/file") check("link", True, "/file") check("link/", False, "/file") - # check("link/", ALL_BUT_LAST, NotADirectoryError) - # check("link/", True, NotADirectoryError) + check("link/", ALL_BUT_LAST, "/file") + check("link/", True, "/file") check("link/file2", False, "/file/file2") - # check("link/file2", ALL_BUT_LAST, NotADirectoryError) - # check("link/file2", True, NotADirectoryError) + check("link/file2", ALL_BUT_LAST, FileNotFoundError) + check("link/file2", True, FileNotFoundError) check("link/.", False, "/file") - # check("link/.", ALL_BUT_LAST, NotADirectoryError) - # check("link/.", True, NotADirectoryError) + check("link/.", ALL_BUT_LAST, "/file") + check("link/.", True, "/file") check("link/../link", False, "/file") check("link/../link", ALL_BUT_LAST, "/file") check("link/../link", True, "/file") From db9e13b49b100c955c2e96db11cb7e407afd495e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 20 May 2025 11:52:51 +0300 Subject: [PATCH 05/11] Documentation. --- Doc/library/os.path.rst | 12 ++++++++++++ Doc/whatsnew/3.15.rst | 6 ++++++ .../2025-05-20-11-51-17.gh-issue-71189.0LpTB1.rst | 1 + 3 files changed, 19 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-20-11-51-17.gh-issue-71189.0LpTB1.rst diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index ecbbc1d7605f9f..64e0e71fcbc497 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -411,6 +411,8 @@ the :mod:`glob` module.) If a path doesn't exist or a symlink loop is encountered, and *strict* is ``True``, :exc:`OSError` is raised. If *strict* is ``False`` these errors are ignored, and so the result might be missing or otherwise inaccessible. + If *strict* is :data:`ALL_BUT_LAST`, the last component of the path + might be missing, but other errors are not ignored. .. note:: This function emulates the operating system's procedure for making a path @@ -429,6 +431,16 @@ the :mod:`glob` module.) .. versionchanged:: 3.10 The *strict* parameter was added. + .. versionchanged:: next + Support for :data:`ALL_BUT_LAST` was added. + + +.. data:: ALL_BUT_LAST + + Special value used for *strict* in :func:`realpath`. + + .. versionadded:: next + .. function:: relpath(path, start=os.curdir) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index bf186c191b04d1..34138a30952be0 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -89,6 +89,12 @@ New modules Improved modules ================ +os.path +------- + +* Add support of the all-but-last mode in :func:`~os.path.realpath`. + (Contributed by Serhiy Storchaka in :gh:`71189`.) + ssl --- 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`. From b67acdbacfd064740b62868b441dfb73938ccd93 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 20 May 2025 12:09:53 +0300 Subject: [PATCH 06/11] Improve ALL_BUT_LAST. --- Lib/genericpath.py | 12 +++++++++++- Lib/ntpath.py | 5 +---- Lib/posixpath.py | 5 +---- Lib/test/test_genericpath.py | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Lib/genericpath.py b/Lib/genericpath.py index ba7b0a13c7f81d..2c2f891af80a7a 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -8,7 +8,7 @@ __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime', 'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink', - 'lexists', 'samefile', 'sameopenfile', 'samestat'] + 'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALL_BUT_LAST'] # Does a path exist? @@ -189,3 +189,13 @@ def _check_arg_types(funcname, *args): f'os.PathLike object, not {s.__class__.__name__!r}') from None if hasstr and hasbytes: raise TypeError("Can't mix strings and bytes in path components") from None + + +# A singleton with a true boolean value. +@object.__new__ +class ALL_BUT_LAST: + """Special value for use in realpath().""" + def __repr__(self): + return self.__class__.__name__ + def __reduce__(self): + return self.__class__.__name__ diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 4b19cfdd7c01e1..32d53a05920460 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"] + "isdevdrive", 'ALL_BUT_LAST'] def _get_bothseps(path): if isinstance(path, bytes): @@ -597,9 +597,6 @@ def abspath(path): path = join(getcwd(), path) return normpath(path) -# A singleton with true boolean value -ALL_BUT_LAST = ['ALL_BUT_LAST'] - try: from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink except ImportError: diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 3a5287348913fc..d4e345bcbeeda0 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -36,7 +36,7 @@ "samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", "devnull","realpath","supports_unicode_filenames","relpath", - "commonpath", "isjunction","isdevdrive"] + "commonpath", "isjunction","isdevdrive", 'ALL_BUT_LAST'] def _get_sep(path): @@ -388,9 +388,6 @@ def abspath(path): # Return a canonical path (i.e. the absolute location of a file on the # filesystem). -# A singleton with true boolean value -ALL_BUT_LAST = ['ALL_BUT_LAST'] - def realpath(filename, *, strict=False): """Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.""" diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index 6c3abe602f557c..410b36e0805da2 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -320,6 +320,20 @@ def test_sameopenfile(self): fd2 = fp2.fileno() self.assertTrue(self.pathmodule.sameopenfile(fd1, fd2)) + def test_all_but_last(self): + ALL_BUT_LAST = self.pathmodule.ALL_BUT_LAST + self.assertEqual(repr(ALL_BUT_LAST), 'ALL_BUT_LAST') + self.assertTrue(ALL_BUT_LAST) + import copy + self.assertIs(copy.copy(ALL_BUT_LAST), ALL_BUT_LAST) + self.assertIs(copy.deepcopy(ALL_BUT_LAST), ALL_BUT_LAST) + import pickle + for proto in range(pickle.HIGHEST_PROTOCOL+1): + with self.subTest(protocol=proto): + pickled = pickle.dumps(ALL_BUT_LAST, proto) + unpickled = pickle.loads(pickled) + self.assertIs(unpickled, ALL_BUT_LAST) + class TestGenericTest(GenericTest, unittest.TestCase): # Issue 16852: GenericTest can't inherit from unittest.TestCase From 63f6c0c151fd01d34b5dcc1417effda8e661f810 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 20 May 2025 12:43:45 +0300 Subject: [PATCH 07/11] Add more tests. --- Lib/test/test_ntpath.py | 32 ++++++++++++++++++++++ Lib/test/test_posixpath.py | 55 +++++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index a6f6a2df96db98..a3827f16215b55 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -7,6 +7,7 @@ import sys import unittest import warnings +from ntpath import ALL_BUT_LAST from test.support import cpython_only, os_helper from test.support import TestFailed, is_emscripten from test.support.os_helper import FakePath @@ -527,40 +528,52 @@ 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) 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) 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) 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) 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()) 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()) 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) 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) path = ABSTFNb + b'\xff' self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) + self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) path = ABSTFNb + b'\\nonexistent\\\xff' self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) + self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) path = ABSTFNb + b'\xff\\..' self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) + self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) path = ABSTFNb + b'\\nonexistent\\\xff\\..' self.assertRaises(UnicodeDecodeError, realpath, path, strict=False) + self.assertRaises(UnicodeDecodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) @os_helper.skip_unless_symlink @@ -691,34 +704,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) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 77e4c3e0c717e1..4a79e57345ee96 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -487,27 +487,35 @@ 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) 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) 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) 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) 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) 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) 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) 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) path = '/\udfff' @@ -516,12 +524,14 @@ def test_realpath_invalid_paths(self): self.assertRaises(FileNotFoundError, realpath, path, strict=True) else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) path = '/nonexistent/\udfff' if sys.platform == 'win32': self.assertEqual(realpath(path, strict=False), path) else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) path = '/\udfff/..' if sys.platform == 'win32': @@ -529,12 +539,14 @@ def test_realpath_invalid_paths(self): self.assertRaises(FileNotFoundError, realpath, path, strict=True) else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) path = '/nonexistent/\udfff/..' if sys.platform == 'win32': self.assertEqual(realpath(path, strict=False), '/nonexistent') else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) path = b'/\xff' @@ -543,6 +555,7 @@ def test_realpath_invalid_paths(self): self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) else: self.assertEqual(realpath(path, strict=False), path) + self.assertEqual(realpath(path, strict=ALL_BUT_LAST), path) if support.is_wasi: self.assertRaises(OSError, realpath, path, strict=True) else: @@ -553,8 +566,10 @@ 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) else: + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) @os_helper.skip_unless_symlink @@ -623,30 +638,48 @@ def test_realpath_symlink_loops_strict(self): # strict mode. try: os.symlink(ABSTFN, ABSTFN) + self.assertRaises(OSError, realpath, ABSTFN, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN, strict=True) os.symlink(ABSTFN+"1", ABSTFN+"2") os.symlink(ABSTFN+"2", ABSTFN+"1") + self.assertRaises(OSError, realpath, ABSTFN+"1", strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN+"1", strict=True) + self.assertRaises(OSError, realpath, ABSTFN+"2", strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN+"2", strict=True) + self.assertRaises(OSError, realpath, ABSTFN+"1/x", strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN+"1/x", strict=True) + self.assertRaises(OSError, realpath, ABSTFN+"1/..", strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN+"1/..", strict=True) + self.assertRaises(OSError, realpath, ABSTFN+"1/../x", strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN+"1/../x", strict=True) os.symlink(ABSTFN+"x", ABSTFN+"y") self.assertRaises(OSError, realpath, - ABSTFN+"1/../" + basename(ABSTFN) + "y", strict=True) + ABSTFN+"1/../" + basename(ABSTFN) + "y", + strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, - ABSTFN+"1/../" + basename(ABSTFN) + "1", strict=True) + ABSTFN+"1/../" + basename(ABSTFN) + "y", + strict=True) + self.assertRaises(OSError, realpath, + ABSTFN+"1/../" + basename(ABSTFN) + "1", + strict=ALL_BUT_LAST) + self.assertRaises(OSError, realpath, + ABSTFN+"1/../" + basename(ABSTFN) + "1", + strict=True) os.symlink(basename(ABSTFN) + "a/b", ABSTFN+"a") + self.assertRaises(OSError, realpath, ABSTFN+"a", strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN+"a", strict=True) os.symlink("../" + basename(dirname(ABSTFN)) + "/" + basename(ABSTFN) + "c", ABSTFN+"c") + self.assertRaises(OSError, realpath, ABSTFN+"c", strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, ABSTFN+"c", strict=True) # Test using relative path as well. + with os_helper.change_cwd(dirname(ABSTFN)): + self.assertRaises(OSError, realpath, basename(ABSTFN), strict=ALL_BUT_LAST) with os_helper.change_cwd(dirname(ABSTFN)): self.assertRaises(OSError, realpath, basename(ABSTFN), strict=True) finally: @@ -768,6 +801,8 @@ def test_realpath_unreadable_symlink(self): self.assertEqual(realpath(ABSTFN + '/foo'), ABSTFN + '/foo') self.assertEqual(realpath(ABSTFN + '/../foo'), dirname(ABSTFN) + '/foo') self.assertEqual(realpath(ABSTFN + '/foo/..'), ABSTFN) + with self.assertRaises(PermissionError): + realpath(ABSTFN, strict=ALL_BUT_LAST) with self.assertRaises(PermissionError): realpath(ABSTFN, strict=True) finally: @@ -780,14 +815,19 @@ 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=False), ABSTFN) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) 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.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) finally: os_helper.unlink(ABSTFN) @@ -800,14 +840,19 @@ 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=False), ABSTFN + "1") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) 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.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) 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) finally: os_helper.unlink(ABSTFN) @@ -822,14 +867,19 @@ 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=False), ABSTFN + "2") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) 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.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) 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) finally: os_helper.unlink(ABSTFN) @@ -837,7 +887,6 @@ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self): os_helper.unlink(ABSTFN + "2") @os_helper.skip_unless_symlink - # @skip_if_ABSTFN_contains_backslash def test_realpath_mode(self): self.addCleanup(os_helper.rmdir, ABSTFN) self.addCleanup(os_helper.rmdir, ABSTFN + "/dir") From 4619540220ec7a63f8eb1f513f407820a3356de1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 20 May 2025 13:28:28 +0300 Subject: [PATCH 08/11] Fix tests on WASI. --- Lib/test/test_posixpath.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 4a79e57345ee96..28b58525328d4d 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -555,10 +555,11 @@ def test_realpath_invalid_paths(self): self.assertRaises(UnicodeDecodeError, realpath, path, strict=True) else: self.assertEqual(realpath(path, strict=False), path) - self.assertEqual(realpath(path, strict=ALL_BUT_LAST), path) if support.is_wasi: + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) else: + self.assertEqual(realpath(path, strict=ALL_BUT_LAST), path) self.assertRaises(FileNotFoundError, realpath, path, strict=True) path = b'/nonexistent/\xff' if sys.platform == 'win32': From c03cf4b3dd165e3be333837e5a79f1bab08af29e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 3 Jun 2025 15:36:50 +0300 Subject: [PATCH 09/11] Add more tests. --- Lib/test/test_ntpath.py | 29 ++++++++++++++++++++++++++++- Lib/test/test_posixpath.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 1f88ed95c519cb..2681468e8560b7 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1019,7 +1019,6 @@ def test_realpath_permission(self): @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') def test_realpath_mode(self): realpath = ntpath.realpath - ALL_BUT_LAST = ntpath.ALL_BUT_LAST ABSTFN = ntpath.abspath(os_helper.TESTFN) self.addCleanup(os_helper.rmdir, ABSTFN) self.addCleanup(os_helper.rmdir, ABSTFN + "/dir") @@ -1052,93 +1051,121 @@ def check(path, mode, expected, errno=None): self.enterContext(os_helper.change_cwd(ABSTFN)) check("file", False, "/file") + check("file", ALLOW_MISSING, "/file") check("file", ALL_BUT_LAST, "/file") check("file", True, "/file") check("file/", False, "/file") + check("file/", ALLOW_MISSING, "/file") check("file/", ALL_BUT_LAST, "/file") check("file/", True, "/file") check("file/file2", False, "/file/file2") + check("file/file2", ALLOW_MISSING, FileNotFoundError) check("file/file2", ALL_BUT_LAST, FileNotFoundError) check("file/file2", True, FileNotFoundError) check("file/.", False, "/file") + check("file/.", ALLOW_MISSING, "/file") check("file/.", ALL_BUT_LAST, "/file") check("file/.", True, "/file") check("file/../link2", False, "/dir") + check("file/../link2", ALLOW_MISSING, "/dir") check("file/../link2", ALL_BUT_LAST, "/dir") check("file/../link2", True, "/dir") check("dir", False, "/dir") + check("dir", ALLOW_MISSING, "/dir") check("dir", ALL_BUT_LAST, "/dir") check("dir", True, "/dir") check("dir/", False, "/dir") + check("dir/", ALLOW_MISSING, "/dir") check("dir/", ALL_BUT_LAST, "/dir") check("dir/", True, "/dir") check("dir/file2", False, "/dir/file2") + check("dir/file2", ALLOW_MISSING, "/dir/file2") check("dir/file2", ALL_BUT_LAST, "/dir/file2") check("dir/file2", True, "/dir/file2") check("link", False, "/file") + check("link", ALLOW_MISSING, "/file") check("link", ALL_BUT_LAST, "/file") check("link", True, "/file") check("link/", False, "/file") + check("link/", ALLOW_MISSING, "/file") check("link/", ALL_BUT_LAST, "/file") check("link/", True, "/file") check("link/file2", False, "/file/file2") + check("link/file2", ALLOW_MISSING, FileNotFoundError) check("link/file2", ALL_BUT_LAST, FileNotFoundError) check("link/file2", True, FileNotFoundError) check("link/.", False, "/file") + check("link/.", ALLOW_MISSING, "/file") check("link/.", ALL_BUT_LAST, "/file") check("link/.", True, "/file") check("link/../link", False, "/file") + check("link/../link", ALLOW_MISSING, "/file") check("link/../link", ALL_BUT_LAST, "/file") check("link/../link", True, "/file") check("link2", False, "/dir") + check("link2", ALLOW_MISSING, "/dir") check("link2", ALL_BUT_LAST, "/dir") check("link2", True, "/dir") check("link2/", False, "/dir") + check("link2/", ALLOW_MISSING, "/dir") check("link2/", ALL_BUT_LAST, "/dir") check("link2/", True, "/dir") check("link2/file2", False, "/dir/file2") + check("link2/file2", ALLOW_MISSING, "/dir/file2") check("link2/file2", ALL_BUT_LAST, "/dir/file2") check("link2/file2", True, "/dir/file2") check("nonexistent", False, "/nonexistent") + check("nonexistent", ALLOW_MISSING, "/nonexistent") check("nonexistent", ALL_BUT_LAST, "/nonexistent") check("nonexistent", True, FileNotFoundError) check("nonexistent/", False, "/nonexistent") + check("nonexistent/", ALLOW_MISSING, "/nonexistent") check("nonexistent/", ALL_BUT_LAST, "/nonexistent") check("nonexistent/", True, FileNotFoundError) check("nonexistent/file", False, "/nonexistent/file") + check("nonexistent/file", ALLOW_MISSING, "/nonexistent/file") check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) check("nonexistent/file", True, FileNotFoundError) check("nonexistent/../link", False, "/file") + check("nonexistent/../link", ALLOW_MISSING, "/file") check("nonexistent/../link", ALL_BUT_LAST, "/file") check("nonexistent/../link", True, "/file") check("broken", False, "/nonexistent") + check("broken", ALLOW_MISSING, "/nonexistent") check("broken", ALL_BUT_LAST, "/nonexistent") check("broken", True, FileNotFoundError) check("broken/", False, "/nonexistent") + check("broken/", ALLOW_MISSING, "/nonexistent") check("broken/", ALL_BUT_LAST, "/nonexistent") check("broken/", True, FileNotFoundError) check("broken/file", False, "/nonexistent/file") + check("broken/file", ALLOW_MISSING, "/nonexistent/file") check("broken/file", ALL_BUT_LAST, FileNotFoundError) check("broken/file", True, FileNotFoundError) check("broken/../link", False, "/file") + check("broken/../link", ALLOW_MISSING, "/file") check("broken/../link", ALL_BUT_LAST, "/file") check("broken/../link", True, "/file") check("cycle", False, "/cycle") + check("cycle", ALLOW_MISSING, OSError, errno.EINVAL) check("cycle", ALL_BUT_LAST, OSError, errno.EINVAL) check("cycle", True, OSError, errno.EINVAL) check("cycle/", False, "/cycle") + check("cycle/", ALLOW_MISSING, OSError, errno.EINVAL) check("cycle/", ALL_BUT_LAST, OSError, errno.EINVAL) check("cycle/", True, OSError, errno.EINVAL) check("cycle/file", False, "/cycle/file") + check("cycle/file", ALLOW_MISSING, OSError, errno.EINVAL) check("cycle/file", ALL_BUT_LAST, OSError, errno.EINVAL) check("cycle/file", True, OSError, errno.EINVAL) check("cycle/../link", False, "/file") + check("cycle/../link", ALLOW_MISSING, "/file") check("cycle/../link", ALL_BUT_LAST, "/file") check("cycle/../link", True, "/file") diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 092ce48c87f728..f593714f51f385 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -1057,93 +1057,121 @@ def check(path, mode, expected, errno=None): self.enterContext(os_helper.change_cwd(ABSTFN)) check("file", False, "/file") + check("file", ALLOW_MISSING, "/file") check("file", ALL_BUT_LAST, "/file") check("file", True, "/file") check("file/", False, "/file") + check("file/", ALLOW_MISSING, NotADirectoryError) check("file/", ALL_BUT_LAST, NotADirectoryError) check("file/", True, NotADirectoryError) check("file/file2", False, "/file/file2") + check("file/file2", ALLOW_MISSING, NotADirectoryError) check("file/file2", ALL_BUT_LAST, NotADirectoryError) check("file/file2", True, NotADirectoryError) check("file/.", False, "/file") + check("file/.", ALLOW_MISSING, NotADirectoryError) check("file/.", ALL_BUT_LAST, NotADirectoryError) check("file/.", True, NotADirectoryError) check("file/../link2", False, "/dir") + check("file/../link2", ALLOW_MISSING, NotADirectoryError) check("file/../link2", ALL_BUT_LAST, NotADirectoryError) check("file/../link2", True, NotADirectoryError) check("dir", False, "/dir") + check("dir", ALLOW_MISSING, "/dir") check("dir", ALL_BUT_LAST, "/dir") check("dir", True, "/dir") check("dir/", False, "/dir") + check("dir/", ALLOW_MISSING, "/dir") check("dir/", ALL_BUT_LAST, "/dir") check("dir/", True, "/dir") check("dir/file2", False, "/dir/file2") + check("dir/file2", ALLOW_MISSING, "/dir/file2") check("dir/file2", ALL_BUT_LAST, "/dir/file2") check("dir/file2", True, "/dir/file2") check("link", False, "/file") + check("link", ALLOW_MISSING, "/file") check("link", ALL_BUT_LAST, "/file") check("link", True, "/file") check("link/", False, "/file") + check("link/", ALLOW_MISSING, NotADirectoryError) check("link/", ALL_BUT_LAST, NotADirectoryError) check("link/", True, NotADirectoryError) check("link/file2", False, "/file/file2") + check("link/file2", ALLOW_MISSING, NotADirectoryError) check("link/file2", ALL_BUT_LAST, NotADirectoryError) check("link/file2", True, NotADirectoryError) check("link/.", False, "/file") + check("link/.", ALLOW_MISSING, NotADirectoryError) check("link/.", ALL_BUT_LAST, NotADirectoryError) check("link/.", True, NotADirectoryError) check("link/../link", False, "/file") + check("link/../link", ALLOW_MISSING, NotADirectoryError) check("link/../link", ALL_BUT_LAST, NotADirectoryError) check("link/../link", True, NotADirectoryError) check("link2", False, "/dir") + check("link2", ALLOW_MISSING, "/dir") check("link2", ALL_BUT_LAST, "/dir") check("link2", True, "/dir") check("link2/", False, "/dir") + check("link2/", ALLOW_MISSING, "/dir") check("link2/", ALL_BUT_LAST, "/dir") check("link2/", True, "/dir") check("link2/file2", False, "/dir/file2") + check("link2/file2", ALLOW_MISSING, "/dir/file2") check("link2/file2", ALL_BUT_LAST, "/dir/file2") check("link2/file2", True, "/dir/file2") check("nonexistent", False, "/nonexistent") + check("nonexistent", ALLOW_MISSING, "/nonexistent") check("nonexistent", ALL_BUT_LAST, "/nonexistent") check("nonexistent", True, FileNotFoundError) check("nonexistent/", False, "/nonexistent") + check("nonexistent/", ALLOW_MISSING, "/nonexistent") check("nonexistent/", ALL_BUT_LAST, "/nonexistent") check("nonexistent/", True, FileNotFoundError) check("nonexistent/file", False, "/nonexistent/file") + check("nonexistent/file", ALLOW_MISSING, "/nonexistent/file") check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) check("nonexistent/file", True, FileNotFoundError) check("nonexistent/../link", False, "/file") + check("nonexistent/../link", ALLOW_MISSING, "/file") check("nonexistent/../link", ALL_BUT_LAST, FileNotFoundError) check("nonexistent/../link", True, FileNotFoundError) check("broken", False, "/nonexistent") + check("broken", ALLOW_MISSING, "/nonexistent") check("broken", ALL_BUT_LAST, "/nonexistent") check("broken", True, FileNotFoundError) check("broken/", False, "/nonexistent") + check("broken/", ALLOW_MISSING, "/nonexistent") check("broken/", ALL_BUT_LAST, "/nonexistent") check("broken/", True, FileNotFoundError) check("broken/file", False, "/nonexistent/file") + check("broken/file", ALLOW_MISSING, "/nonexistent/file") check("broken/file", ALL_BUT_LAST, FileNotFoundError) check("broken/file", True, FileNotFoundError) check("broken/../link", False, "/file") + check("broken/../link", ALLOW_MISSING, "/file") check("broken/../link", ALL_BUT_LAST, FileNotFoundError) check("broken/../link", True, FileNotFoundError) check("cycle", False, "/cycle") + check("cycle", ALLOW_MISSING, OSError, errno.ELOOP) check("cycle", ALL_BUT_LAST, OSError, errno.ELOOP) check("cycle", True, OSError, errno.ELOOP) check("cycle/", False, "/cycle") + check("cycle/", ALLOW_MISSING, OSError, errno.ELOOP) check("cycle/", ALL_BUT_LAST, OSError, errno.ELOOP) check("cycle/", True, OSError, errno.ELOOP) check("cycle/file", False, "/cycle/file") + check("cycle/file", ALLOW_MISSING, OSError, errno.ELOOP) check("cycle/file", ALL_BUT_LAST, OSError, errno.ELOOP) check("cycle/file", True, OSError, errno.ELOOP) check("cycle/../link", False, "/file") + check("cycle/../link", ALLOW_MISSING, OSError, errno.ELOOP) check("cycle/../link", ALL_BUT_LAST, OSError, errno.ELOOP) check("cycle/../link", True, OSError, errno.ELOOP) From e142390e7e8789038940616dabbaef847679892d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 3 Jun 2025 16:02:35 +0300 Subject: [PATCH 10/11] Fix tests on Windows and add more tests. --- Lib/test/test_genericpath.py | 29 ++++++++++++++++------------- Lib/test/test_ntpath.py | 11 ++++++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index 8480076a7e5913..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,19 +322,20 @@ def test_sameopenfile(self): fd2 = fp2.fileno() self.assertTrue(self.pathmodule.sameopenfile(fd1, fd2)) - def test_all_but_last(self): - ALL_BUT_LAST = self.pathmodule.ALL_BUT_LAST - self.assertEqual(repr(ALL_BUT_LAST), 'os.path.ALL_BUT_LAST') - self.assertTrue(ALL_BUT_LAST) - import copy - self.assertIs(copy.copy(ALL_BUT_LAST), ALL_BUT_LAST) - self.assertIs(copy.deepcopy(ALL_BUT_LAST), ALL_BUT_LAST) - import pickle - for proto in range(pickle.HIGHEST_PROTOCOL+1): - with self.subTest(protocol=proto): - pickled = pickle.dumps(ALL_BUT_LAST, proto) - unpickled = pickle.loads(pickled) - self.assertIs(unpickled, ALL_BUT_LAST) + 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): diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 2681468e8560b7..cf3ae47935eb29 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -660,7 +660,7 @@ def test_realpath_invalid_unicode_paths(self, 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() @@ -893,7 +893,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") @@ -931,6 +931,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') @@ -955,7 +956,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( @@ -1059,7 +1060,7 @@ def check(path, mode, expected, errno=None): check("file/", ALL_BUT_LAST, "/file") check("file/", True, "/file") check("file/file2", False, "/file/file2") - check("file/file2", ALLOW_MISSING, FileNotFoundError) + check("file/file2", ALLOW_MISSING, "/file/file2") check("file/file2", ALL_BUT_LAST, FileNotFoundError) check("file/file2", True, FileNotFoundError) check("file/.", False, "/file") @@ -1093,7 +1094,7 @@ def check(path, mode, expected, errno=None): check("link/", ALL_BUT_LAST, "/file") check("link/", True, "/file") check("link/file2", False, "/file/file2") - check("link/file2", ALLOW_MISSING, FileNotFoundError) + check("link/file2", ALLOW_MISSING, "/file/file2") check("link/file2", ALL_BUT_LAST, FileNotFoundError) check("link/file2", True, FileNotFoundError) check("link/.", False, "/file") From 3b904c1d89fe5f388dd84a0037eefb43cdc0f413 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 6 Jun 2025 16:33:57 +0300 Subject: [PATCH 11/11] Make tests more compact. --- Lib/test/test_ntpath.py | 183 +++++++++++------------------------ Lib/test/test_posixpath.py | 192 +++++++++++++------------------------ 2 files changed, 125 insertions(+), 250 deletions(-) diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 2b6f610e1dde54..b9cd75a3b8adea 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1022,137 +1022,70 @@ def test_realpath_mode(self): os.symlink("dir", ABSTFN + "\\link2") os.symlink("nonexistent", ABSTFN + "\\broken") os.symlink("cycle", ABSTFN + "\\cycle") - def check(path, mode, expected, errno=None): + def check(path, modes, expected, errno=None): path = path.replace('/', '\\') if isinstance(expected, str): assert errno is None - self.assertEqual(realpath(path, strict=mode), - ABSTFN + expected.replace('/', '\\')) + expected = expected.replace('/', os.sep) + for mode in modes: + with self.subTest(mode=mode): + self.assertEqual(realpath(path, strict=mode), + ABSTFN + expected) else: - with self.assertRaises(expected) as cm: - realpath(path, strict=mode) - if errno is not None: - self.assertEqual(cm.exception.errno, errno) + 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)) - check("file", False, "/file") - check("file", ALLOW_MISSING, "/file") - check("file", ALL_BUT_LAST, "/file") - check("file", True, "/file") - check("file/", False, "/file") - check("file/", ALLOW_MISSING, "/file") - check("file/", ALL_BUT_LAST, "/file") - check("file/", True, "/file") - check("file/file2", False, "/file/file2") - check("file/file2", ALLOW_MISSING, "/file/file2") - check("file/file2", ALL_BUT_LAST, FileNotFoundError) - check("file/file2", True, FileNotFoundError) - check("file/.", False, "/file") - check("file/.", ALLOW_MISSING, "/file") - check("file/.", ALL_BUT_LAST, "/file") - check("file/.", True, "/file") - check("file/../link2", False, "/dir") - check("file/../link2", ALLOW_MISSING, "/dir") - check("file/../link2", ALL_BUT_LAST, "/dir") - check("file/../link2", True, "/dir") - - check("dir", False, "/dir") - check("dir", ALLOW_MISSING, "/dir") - check("dir", ALL_BUT_LAST, "/dir") - check("dir", True, "/dir") - check("dir/", False, "/dir") - check("dir/", ALLOW_MISSING, "/dir") - check("dir/", ALL_BUT_LAST, "/dir") - check("dir/", True, "/dir") - check("dir/file2", False, "/dir/file2") - check("dir/file2", ALLOW_MISSING, "/dir/file2") - check("dir/file2", ALL_BUT_LAST, "/dir/file2") - check("dir/file2", True, "/dir/file2") - - check("link", False, "/file") - check("link", ALLOW_MISSING, "/file") - check("link", ALL_BUT_LAST, "/file") - check("link", True, "/file") - check("link/", False, "/file") - check("link/", ALLOW_MISSING, "/file") - check("link/", ALL_BUT_LAST, "/file") - check("link/", True, "/file") - check("link/file2", False, "/file/file2") - check("link/file2", ALLOW_MISSING, "/file/file2") - check("link/file2", ALL_BUT_LAST, FileNotFoundError) - check("link/file2", True, FileNotFoundError) - check("link/.", False, "/file") - check("link/.", ALLOW_MISSING, "/file") - check("link/.", ALL_BUT_LAST, "/file") - check("link/.", True, "/file") - check("link/../link", False, "/file") - check("link/../link", ALLOW_MISSING, "/file") - check("link/../link", ALL_BUT_LAST, "/file") - check("link/../link", True, "/file") - - check("link2", False, "/dir") - check("link2", ALLOW_MISSING, "/dir") - check("link2", ALL_BUT_LAST, "/dir") - check("link2", True, "/dir") - check("link2/", False, "/dir") - check("link2/", ALLOW_MISSING, "/dir") - check("link2/", ALL_BUT_LAST, "/dir") - check("link2/", True, "/dir") - check("link2/file2", False, "/dir/file2") - check("link2/file2", ALLOW_MISSING, "/dir/file2") - check("link2/file2", ALL_BUT_LAST, "/dir/file2") - check("link2/file2", True, "/dir/file2") - - check("nonexistent", False, "/nonexistent") - check("nonexistent", ALLOW_MISSING, "/nonexistent") - check("nonexistent", ALL_BUT_LAST, "/nonexistent") - check("nonexistent", True, FileNotFoundError) - check("nonexistent/", False, "/nonexistent") - check("nonexistent/", ALLOW_MISSING, "/nonexistent") - check("nonexistent/", ALL_BUT_LAST, "/nonexistent") - check("nonexistent/", True, FileNotFoundError) - check("nonexistent/file", False, "/nonexistent/file") - check("nonexistent/file", ALLOW_MISSING, "/nonexistent/file") - check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) - check("nonexistent/file", True, FileNotFoundError) - check("nonexistent/../link", False, "/file") - check("nonexistent/../link", ALLOW_MISSING, "/file") - check("nonexistent/../link", ALL_BUT_LAST, "/file") - check("nonexistent/../link", True, "/file") - - check("broken", False, "/nonexistent") - check("broken", ALLOW_MISSING, "/nonexistent") - check("broken", ALL_BUT_LAST, "/nonexistent") - check("broken", True, FileNotFoundError) - check("broken/", False, "/nonexistent") - check("broken/", ALLOW_MISSING, "/nonexistent") - check("broken/", ALL_BUT_LAST, "/nonexistent") - check("broken/", True, FileNotFoundError) - check("broken/file", False, "/nonexistent/file") - check("broken/file", ALLOW_MISSING, "/nonexistent/file") - check("broken/file", ALL_BUT_LAST, FileNotFoundError) - check("broken/file", True, FileNotFoundError) - check("broken/../link", False, "/file") - check("broken/../link", ALLOW_MISSING, "/file") - check("broken/../link", ALL_BUT_LAST, "/file") - check("broken/../link", True, "/file") - - check("cycle", False, "/cycle") - check("cycle", ALLOW_MISSING, OSError, errno.EINVAL) - check("cycle", ALL_BUT_LAST, OSError, errno.EINVAL) - check("cycle", True, OSError, errno.EINVAL) - check("cycle/", False, "/cycle") - check("cycle/", ALLOW_MISSING, OSError, errno.EINVAL) - check("cycle/", ALL_BUT_LAST, OSError, errno.EINVAL) - check("cycle/", True, OSError, errno.EINVAL) - check("cycle/file", False, "/cycle/file") - check("cycle/file", ALLOW_MISSING, OSError, errno.EINVAL) - check("cycle/file", ALL_BUT_LAST, OSError, errno.EINVAL) - check("cycle/file", True, OSError, errno.EINVAL) - check("cycle/../link", False, "/file") - check("cycle/../link", ALLOW_MISSING, "/file") - check("cycle/../link", ALL_BUT_LAST, "/file") - check("cycle/../link", True, "/file") + 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: diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 4922a3433ad453..a9975b75f7c22b 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -1028,136 +1028,78 @@ def test_realpath_mode(self): os.symlink("dir", ABSTFN + "/link2") os.symlink("nonexistent", ABSTFN + "/broken") os.symlink("cycle", ABSTFN + "/cycle") - def check(path, mode, expected, errno=None): + def check(path, modes, expected, errno=None): if isinstance(expected, str): assert errno is None - self.assertEqual(realpath(path, strict=mode).replace('/', os.sep), - ABSTFN.replace('/', os.sep) + expected.replace('/', os.sep)) + 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: - with self.assertRaises(expected) as cm: - realpath(path, strict=mode) - if errno is not None: - self.assertEqual(cm.exception.errno, errno) + 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)) - check("file", False, "/file") - check("file", ALLOW_MISSING, "/file") - check("file", ALL_BUT_LAST, "/file") - check("file", True, "/file") - check("file/", False, "/file") - check("file/", ALLOW_MISSING, NotADirectoryError) - check("file/", ALL_BUT_LAST, NotADirectoryError) - check("file/", True, NotADirectoryError) - check("file/file2", False, "/file/file2") - check("file/file2", ALLOW_MISSING, NotADirectoryError) - check("file/file2", ALL_BUT_LAST, NotADirectoryError) - check("file/file2", True, NotADirectoryError) - check("file/.", False, "/file") - check("file/.", ALLOW_MISSING, NotADirectoryError) - check("file/.", ALL_BUT_LAST, NotADirectoryError) - check("file/.", True, NotADirectoryError) - check("file/../link2", False, "/dir") - check("file/../link2", ALLOW_MISSING, NotADirectoryError) - check("file/../link2", ALL_BUT_LAST, NotADirectoryError) - check("file/../link2", True, NotADirectoryError) - - check("dir", False, "/dir") - check("dir", ALLOW_MISSING, "/dir") - check("dir", ALL_BUT_LAST, "/dir") - check("dir", True, "/dir") - check("dir/", False, "/dir") - check("dir/", ALLOW_MISSING, "/dir") - check("dir/", ALL_BUT_LAST, "/dir") - check("dir/", True, "/dir") - check("dir/file2", False, "/dir/file2") - check("dir/file2", ALLOW_MISSING, "/dir/file2") - check("dir/file2", ALL_BUT_LAST, "/dir/file2") - check("dir/file2", True, "/dir/file2") - - check("link", False, "/file") - check("link", ALLOW_MISSING, "/file") - check("link", ALL_BUT_LAST, "/file") - check("link", True, "/file") - check("link/", False, "/file") - check("link/", ALLOW_MISSING, NotADirectoryError) - check("link/", ALL_BUT_LAST, NotADirectoryError) - check("link/", True, NotADirectoryError) - check("link/file2", False, "/file/file2") - check("link/file2", ALLOW_MISSING, NotADirectoryError) - check("link/file2", ALL_BUT_LAST, NotADirectoryError) - check("link/file2", True, NotADirectoryError) - check("link/.", False, "/file") - check("link/.", ALLOW_MISSING, NotADirectoryError) - check("link/.", ALL_BUT_LAST, NotADirectoryError) - check("link/.", True, NotADirectoryError) - check("link/../link", False, "/file") - check("link/../link", ALLOW_MISSING, NotADirectoryError) - check("link/../link", ALL_BUT_LAST, NotADirectoryError) - check("link/../link", True, NotADirectoryError) - - check("link2", False, "/dir") - check("link2", ALLOW_MISSING, "/dir") - check("link2", ALL_BUT_LAST, "/dir") - check("link2", True, "/dir") - check("link2/", False, "/dir") - check("link2/", ALLOW_MISSING, "/dir") - check("link2/", ALL_BUT_LAST, "/dir") - check("link2/", True, "/dir") - check("link2/file2", False, "/dir/file2") - check("link2/file2", ALLOW_MISSING, "/dir/file2") - check("link2/file2", ALL_BUT_LAST, "/dir/file2") - check("link2/file2", True, "/dir/file2") - - check("nonexistent", False, "/nonexistent") - check("nonexistent", ALLOW_MISSING, "/nonexistent") - check("nonexistent", ALL_BUT_LAST, "/nonexistent") - check("nonexistent", True, FileNotFoundError) - check("nonexistent/", False, "/nonexistent") - check("nonexistent/", ALLOW_MISSING, "/nonexistent") - check("nonexistent/", ALL_BUT_LAST, "/nonexistent") - check("nonexistent/", True, FileNotFoundError) - check("nonexistent/file", False, "/nonexistent/file") - check("nonexistent/file", ALLOW_MISSING, "/nonexistent/file") - check("nonexistent/file", ALL_BUT_LAST, FileNotFoundError) - check("nonexistent/file", True, FileNotFoundError) - check("nonexistent/../link", False, "/file") - check("nonexistent/../link", ALLOW_MISSING, "/file") - check("nonexistent/../link", ALL_BUT_LAST, FileNotFoundError) - check("nonexistent/../link", True, FileNotFoundError) - - check("broken", False, "/nonexistent") - check("broken", ALLOW_MISSING, "/nonexistent") - check("broken", ALL_BUT_LAST, "/nonexistent") - check("broken", True, FileNotFoundError) - check("broken/", False, "/nonexistent") - check("broken/", ALLOW_MISSING, "/nonexistent") - check("broken/", ALL_BUT_LAST, "/nonexistent") - check("broken/", True, FileNotFoundError) - check("broken/file", False, "/nonexistent/file") - check("broken/file", ALLOW_MISSING, "/nonexistent/file") - check("broken/file", ALL_BUT_LAST, FileNotFoundError) - check("broken/file", True, FileNotFoundError) - check("broken/../link", False, "/file") - check("broken/../link", ALLOW_MISSING, "/file") - check("broken/../link", ALL_BUT_LAST, FileNotFoundError) - check("broken/../link", True, FileNotFoundError) - - check("cycle", False, "/cycle") - check("cycle", ALLOW_MISSING, OSError, errno.ELOOP) - check("cycle", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle", True, OSError, errno.ELOOP) - check("cycle/", False, "/cycle") - check("cycle/", ALLOW_MISSING, OSError, errno.ELOOP) - check("cycle/", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle/", True, OSError, errno.ELOOP) - check("cycle/file", False, "/cycle/file") - check("cycle/file", ALLOW_MISSING, OSError, errno.ELOOP) - check("cycle/file", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle/file", True, OSError, errno.ELOOP) - check("cycle/../link", False, "/file") - check("cycle/../link", ALLOW_MISSING, OSError, errno.ELOOP) - check("cycle/../link", ALL_BUT_LAST, OSError, errno.ELOOP) - check("cycle/../link", True, OSError, errno.ELOOP) + 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")