gh-71189: Support all-but-last mode in os.path.realpath() (GH-117562)
Some checks are pending
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / Windows MSI (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Ubuntu SSL tests with AWS-LC (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
mypy / Run mypy on Lib/_pyrepl (push) Waiting to run
mypy / Run mypy on Lib/test/libregrtest (push) Waiting to run
mypy / Run mypy on Lib/tomllib (push) Waiting to run
mypy / Run mypy on Tools/build (push) Waiting to run
mypy / Run mypy on Tools/cases_generator (push) Waiting to run
mypy / Run mypy on Tools/clinic (push) Waiting to run
mypy / Run mypy on Tools/jit (push) Waiting to run
mypy / Run mypy on Tools/peg_generator (push) Waiting to run

This commit is contained in:
Serhiy Storchaka 2025-07-30 10:19:19 +03:00 committed by GitHub
parent 5236b0281b
commit 9d3b53c47f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 332 additions and 37 deletions

View file

@ -424,6 +424,8 @@ the :mod:`glob` module.)
re-raised. re-raised.
In particular, :exc:`FileNotFoundError` is raised if *path* does not exist, In particular, :exc:`FileNotFoundError` is raised if *path* does not exist,
or another :exc:`OSError` if it is otherwise inaccessible. or another :exc:`OSError` if it is otherwise inaccessible.
If *strict* is :data:`ALL_BUT_LAST`, the last component of the path
is allowed to be missing, but all other errors are raised.
If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than
:exc:`FileNotFoundError` are re-raised (as with ``strict=True``). :exc:`FileNotFoundError` are re-raised (as with ``strict=True``).
@ -448,8 +450,14 @@ the :mod:`glob` module.)
The *strict* parameter was added. The *strict* parameter was added.
.. versionchanged:: next .. versionchanged:: next
The :py:data:`~os.path.ALLOW_MISSING` value for the *strict* parameter The :data:`ALL_BUT_LAST` and :data:`ALLOW_MISSING` values for
was added. the *strict* parameter was added.
.. data:: ALL_BUT_LAST
Special value used for the *strict* argument in :func:`realpath`.
.. versionadded:: next
.. data:: ALLOW_MISSING .. data:: ALLOW_MISSING
@ -457,6 +465,7 @@ the :mod:`glob` module.)
.. versionadded:: next .. versionadded:: next
.. function:: relpath(path, start=os.curdir) .. function:: relpath(path, start=os.curdir)
Return a relative filepath to *path* either from the current directory or Return a relative filepath to *path* either from the current directory or

View file

@ -264,13 +264,15 @@ math
os.path os.path
------- -------
* Add support of the all-but-last mode in :func:`~os.path.realpath`.
(Contributed by Serhiy Storchaka in :gh:`71189`.)
* The *strict* parameter to :func:`os.path.realpath` accepts a new value, * The *strict* parameter to :func:`os.path.realpath` accepts a new value,
:data:`os.path.ALLOW_MISSING`. :data:`os.path.ALLOW_MISSING`.
If used, errors other than :exc:`FileNotFoundError` will be re-raised; If used, errors other than :exc:`FileNotFoundError` will be re-raised;
the resulting path can be missing but it will be free of symlinks. the resulting path can be missing but it will be free of symlinks.
(Contributed by Petr Viktorin for :cve:`2025-4517`.) (Contributed by Petr Viktorin for :cve:`2025-4517`.)
shelve shelve
------ ------

View file

@ -8,7 +8,8 @@ import stat
__all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime', __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink', 'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink',
'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALLOW_MISSING'] 'lexists', 'samefile', 'sameopenfile', 'samestat',
'ALL_BUT_LAST', 'ALLOW_MISSING']
# Does a path exist? # Does a path exist?
@ -190,7 +191,17 @@ def _check_arg_types(funcname, *args):
if hasstr and hasbytes: if hasstr and hasbytes:
raise TypeError("Can't mix strings and bytes in path components") from None raise TypeError("Can't mix strings and bytes in path components") from None
# A singleton with a true boolean value.
# Singletons with a true boolean value.
@object.__new__
class ALL_BUT_LAST:
"""Special value for use in realpath()."""
def __repr__(self):
return 'os.path.ALL_BUT_LAST'
def __reduce__(self):
return self.__class__.__name__
@object.__new__ @object.__new__
class ALLOW_MISSING: class ALLOW_MISSING:
"""Special value for use in realpath().""" """Special value for use in realpath()."""

View file

@ -29,7 +29,7 @@ __all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext"
"abspath","curdir","pardir","sep","pathsep","defpath","altsep", "abspath","curdir","pardir","sep","pathsep","defpath","altsep",
"extsep","devnull","realpath","supports_unicode_filenames","relpath", "extsep","devnull","realpath","supports_unicode_filenames","relpath",
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction", "samefile", "sameopenfile", "samestat", "commonpath", "isjunction",
"isdevdrive", "ALLOW_MISSING"] "isdevdrive", "ALL_BUT_LAST", "ALLOW_MISSING"]
def _get_bothseps(path): def _get_bothseps(path):
if isinstance(path, bytes): if isinstance(path, bytes):
@ -726,7 +726,8 @@ else:
if strict is ALLOW_MISSING: if strict is ALLOW_MISSING:
ignored_error = FileNotFoundError ignored_error = FileNotFoundError
strict = True elif strict is ALL_BUT_LAST:
ignored_error = FileNotFoundError
elif strict: elif strict:
ignored_error = () ignored_error = ()
else: else:
@ -746,6 +747,12 @@ else:
raise OSError(str(ex)) from None raise OSError(str(ex)) from None
path = normpath(path) path = normpath(path)
except ignored_error as ex: except ignored_error as ex:
if strict is ALL_BUT_LAST:
dirname, basename = split(path)
if not basename:
dirname, basename = split(path)
if not isdir(dirname):
raise
initial_winerror = ex.winerror initial_winerror = ex.winerror
path = _getfinalpathname_nonstrict(path, path = _getfinalpathname_nonstrict(path,
ignored_error=ignored_error) ignored_error=ignored_error)

View file

@ -36,7 +36,8 @@ __all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext"
"samefile","sameopenfile","samestat", "samefile","sameopenfile","samestat",
"curdir","pardir","sep","pathsep","defpath","altsep","extsep", "curdir","pardir","sep","pathsep","defpath","altsep","extsep",
"devnull","realpath","supports_unicode_filenames","relpath", "devnull","realpath","supports_unicode_filenames","relpath",
"commonpath", "isjunction","isdevdrive","ALLOW_MISSING"] "commonpath","isjunction","isdevdrive",
"ALL_BUT_LAST","ALLOW_MISSING"]
def _get_sep(path): def _get_sep(path):
@ -404,7 +405,8 @@ symbolic links encountered in the path."""
getcwd = os.getcwd getcwd = os.getcwd
if strict is ALLOW_MISSING: if strict is ALLOW_MISSING:
ignored_error = FileNotFoundError ignored_error = FileNotFoundError
strict = True elif strict is ALL_BUT_LAST:
ignored_error = FileNotFoundError
elif strict: elif strict:
ignored_error = () ignored_error = ()
else: else:
@ -418,7 +420,7 @@ symbolic links encountered in the path."""
# indicates that a symlink target has been resolved, and that the original # indicates that a symlink target has been resolved, and that the original
# symlink path can be retrieved by popping again. The [::-1] slice is a # symlink path can be retrieved by popping again. The [::-1] slice is a
# very fast way of spelling list(reversed(...)). # very fast way of spelling list(reversed(...)).
rest = filename.split(sep)[::-1] rest = filename.rstrip(sep).split(sep)[::-1]
# Number of unprocessed parts in 'rest'. This can differ from len(rest) # Number of unprocessed parts in 'rest'. This can differ from len(rest)
# later, because 'rest' might contain markers for unresolved symlinks. # later, because 'rest' might contain markers for unresolved symlinks.
@ -427,6 +429,7 @@ symbolic links encountered in the path."""
# The resolved path, which is absolute throughout this function. # The resolved path, which is absolute throughout this function.
# Note: getcwd() returns a normalized and symlink-free path. # Note: getcwd() returns a normalized and symlink-free path.
path = sep if filename.startswith(sep) else getcwd() path = sep if filename.startswith(sep) else getcwd()
trailing_sep = filename.endswith(sep)
# Mapping from symlink paths to *fully resolved* symlink targets. If a # Mapping from symlink paths to *fully resolved* symlink targets. If a
# symlink is encountered but not yet resolved, the value is None. This is # symlink is encountered but not yet resolved, the value is None. This is
@ -459,7 +462,8 @@ symbolic links encountered in the path."""
try: try:
st_mode = lstat(newpath).st_mode st_mode = lstat(newpath).st_mode
if not stat.S_ISLNK(st_mode): if not stat.S_ISLNK(st_mode):
if strict and part_count and not stat.S_ISDIR(st_mode): if (strict and (part_count or trailing_sep)
and not stat.S_ISDIR(st_mode)):
raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR), raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR),
newpath) newpath)
path = newpath path = newpath
@ -486,7 +490,8 @@ symbolic links encountered in the path."""
continue continue
target = readlink(newpath) target = readlink(newpath)
except ignored_error: except ignored_error:
pass if strict is ALL_BUT_LAST and part_count:
raise
else: else:
# Resolve the symbolic link # Resolve the symbolic link
if target.startswith(sep): if target.startswith(sep):

View file

@ -2,8 +2,10 @@
Tests common to genericpath, ntpath and posixpath Tests common to genericpath, ntpath and posixpath
""" """
import copy
import genericpath import genericpath
import os import os
import pickle
import sys import sys
import unittest import unittest
import warnings import warnings
@ -320,6 +322,21 @@ class GenericTest:
fd2 = fp2.fileno() fd2 = fp2.fileno()
self.assertTrue(self.pathmodule.sameopenfile(fd1, fd2)) self.assertTrue(self.pathmodule.sameopenfile(fd1, fd2))
def test_realpath_mode_values(self):
for name in 'ALL_BUT_LAST', 'ALLOW_MISSING':
with self.subTest(name):
mode = getattr(self.pathmodule, name)
self.assertEqual(repr(mode), 'os.path.' + name)
self.assertEqual(str(mode), 'os.path.' + name)
self.assertTrue(mode)
self.assertIs(copy.copy(mode), mode)
self.assertIs(copy.deepcopy(mode), mode)
for proto in range(pickle.HIGHEST_PROTOCOL+1):
with self.subTest(protocol=proto):
pickled = pickle.dumps(mode, proto)
unpickled = pickle.loads(pickled)
self.assertIs(unpickled, mode)
class TestGenericTest(GenericTest, unittest.TestCase): class TestGenericTest(GenericTest, unittest.TestCase):
# Issue 16852: GenericTest can't inherit from unittest.TestCase # Issue 16852: GenericTest can't inherit from unittest.TestCase

View file

@ -1,3 +1,4 @@
import errno
import inspect import inspect
import ntpath import ntpath
import os import os
@ -6,7 +7,7 @@ import subprocess
import sys import sys
import unittest import unittest
import warnings import warnings
from ntpath import ALLOW_MISSING from ntpath import ALL_BUT_LAST, ALLOW_MISSING
from test import support from test import support
from test.support import TestFailed, cpython_only, os_helper from test.support import TestFailed, cpython_only, os_helper
from test.support.os_helper import FakePath from test.support.os_helper import FakePath
@ -587,59 +588,63 @@ class TestNtpath(NtpathTestCase):
# gh-106242: Embedded nulls and non-strict fallback to abspath # gh-106242: Embedded nulls and non-strict fallback to abspath
self.assertEqual(realpath(path, strict=False), path) self.assertEqual(realpath(path, strict=False), path)
# gh-106242: Embedded nulls should raise OSError (not ValueError) # gh-106242: Embedded nulls should raise OSError (not ValueError)
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFNb + b'\x00' path = ABSTFNb + b'\x00'
self.assertEqual(realpath(path, strict=False), path) self.assertEqual(realpath(path, strict=False), path)
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFN + '\\nonexistent\\x\x00' path = ABSTFN + '\\nonexistent\\x\x00'
self.assertEqual(realpath(path, strict=False), path) self.assertEqual(realpath(path, strict=False), path)
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFNb + b'\\nonexistent\\x\x00' path = ABSTFNb + b'\\nonexistent\\x\x00'
self.assertEqual(realpath(path, strict=False), path) self.assertEqual(realpath(path, strict=False), path)
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
path = ABSTFN + '\x00\\..' path = ABSTFN + '\x00\\..'
self.assertEqual(realpath(path, strict=False), os.getcwd()) self.assertEqual(realpath(path, strict=False), os.getcwd())
self.assertEqual(realpath(path, strict=ALL_BUT_LAST), os.getcwd())
self.assertEqual(realpath(path, strict=True), os.getcwd()) self.assertEqual(realpath(path, strict=True), os.getcwd())
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd()) self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd())
path = ABSTFNb + b'\x00\\..' path = ABSTFNb + b'\x00\\..'
self.assertEqual(realpath(path, strict=False), os.getcwdb()) self.assertEqual(realpath(path, strict=False), os.getcwdb())
self.assertEqual(realpath(path, strict=ALL_BUT_LAST), os.getcwdb())
self.assertEqual(realpath(path, strict=True), os.getcwdb()) self.assertEqual(realpath(path, strict=True), os.getcwdb())
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb()) self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb())
path = ABSTFN + '\\nonexistent\\x\x00\\..' path = ABSTFN + '\\nonexistent\\x\x00\\..'
self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent') self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent')
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent') self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent')
path = ABSTFNb + b'\\nonexistent\\x\x00\\..' path = ABSTFNb + b'\\nonexistent\\x\x00\\..'
self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent') self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent')
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent') self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent')
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_invalid_unicode_paths(self, kwargs): def test_realpath_invalid_unicode_paths(self, kwargs):
realpath = ntpath.realpath realpath = ntpath.realpath
ABSTFN = ntpath.abspath(os_helper.TESTFN) ABSTFN = ntpath.abspath(os_helper.TESTFN)
ABSTFNb = os.fsencode(ABSTFN) ABSTFNb = os.fsencode(ABSTFN)
path = ABSTFNb + b'\xff' path = ABSTFNb + b'\xff'
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
path = ABSTFNb + b'\\nonexistent\\\xff' path = ABSTFNb + b'\\nonexistent\\\xff'
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
path = ABSTFNb + b'\xff\\..' path = ABSTFNb + b'\xff\\..'
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
path = ABSTFNb + b'\\nonexistent\\\xff\\..' path = ABSTFNb + b'\\nonexistent\\\xff\\..'
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs)
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @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): def test_realpath_relative(self, kwargs):
ABSTFN = ntpath.abspath(os_helper.TESTFN) ABSTFN = ntpath.abspath(os_helper.TESTFN)
open(ABSTFN, "wb").close() open(ABSTFN, "wb").close()
@ -766,34 +771,53 @@ class TestNtpath(NtpathTestCase):
self.addCleanup(os_helper.unlink, ABSTFN + "a") self.addCleanup(os_helper.unlink, ABSTFN + "a")
os.symlink(ABSTFN, ABSTFN) os.symlink(ABSTFN, ABSTFN)
self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=ALL_BUT_LAST)
self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=True) self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=True)
os.symlink(ABSTFN + "1", ABSTFN + "2") os.symlink(ABSTFN + "1", ABSTFN + "2")
os.symlink(ABSTFN + "2", ABSTFN + "1") 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 + "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 + "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) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x", strict=True)
# Windows eliminates '..' components before resolving links, so the # Windows eliminates '..' components before resolving links, so the
# following call is not expected to raise. # 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), self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..", strict=True),
ntpath.dirname(ABSTFN)) 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) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\..\\x", strict=True)
os.symlink(ABSTFN + "x", ABSTFN + "y") 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\\..\\" self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\..\\"
+ ntpath.basename(ABSTFN) + "y", + ntpath.basename(ABSTFN) + "y",
strict=True) strict=True)
self.assertRaises(OSError, ntpath.realpath,
ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
strict=ALL_BUT_LAST)
self.assertRaises(OSError, ntpath.realpath, self.assertRaises(OSError, ntpath.realpath,
ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1", ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
strict=True) strict=True)
os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a") 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) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a", strict=True)
os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN)) os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN))
+ "\\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c") + "\\" + 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) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c", strict=True)
# Test using relative path as well. # 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), self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
strict=True) strict=True)
@ -853,7 +877,7 @@ class TestNtpath(NtpathTestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @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): def test_realpath_symlink_prefix(self, kwargs):
ABSTFN = ntpath.abspath(os_helper.TESTFN) ABSTFN = ntpath.abspath(os_helper.TESTFN)
self.addCleanup(os_helper.unlink, ABSTFN + "3") self.addCleanup(os_helper.unlink, ABSTFN + "3")
@ -891,6 +915,7 @@ class TestNtpath(NtpathTestCase):
tester("ntpath.realpath('NUL')", r'\\.\NUL') tester("ntpath.realpath('NUL')", r'\\.\NUL')
tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL') tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL')
tester("ntpath.realpath('NUL', strict=True)", 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') tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\\.\NUL')
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
@ -915,7 +940,7 @@ class TestNtpath(NtpathTestCase):
self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short)) 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 self.subTest(**kwargs):
with os_helper.change_cwd(test_dir_long): with os_helper.change_cwd(test_dir_long):
self.assertPathEqual( self.assertPathEqual(
@ -975,6 +1000,93 @@ class TestNtpath(NtpathTestCase):
self.assertPathEqual(test_file, ntpath.realpath(test_file_short)) self.assertPathEqual(test_file, ntpath.realpath(test_file_short))
@os_helper.skip_unless_symlink
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
def test_realpath_mode(self):
realpath = ntpath.realpath
ABSTFN = ntpath.abspath(os_helper.TESTFN)
self.addCleanup(os_helper.rmdir, ABSTFN)
self.addCleanup(os_helper.rmdir, ABSTFN + "/dir")
self.addCleanup(os_helper.unlink, ABSTFN + "/file")
self.addCleanup(os_helper.unlink, ABSTFN + "/dir/file2")
self.addCleanup(os_helper.unlink, ABSTFN + "/link")
self.addCleanup(os_helper.unlink, ABSTFN + "/link2")
self.addCleanup(os_helper.unlink, ABSTFN + "/broken")
self.addCleanup(os_helper.unlink, ABSTFN + "/cycle")
os.mkdir(ABSTFN)
os.mkdir(ABSTFN + "\\dir")
open(ABSTFN + "\\file", "wb").close()
open(ABSTFN + "\\dir\\file2", "wb").close()
os.symlink("file", ABSTFN + "\\link")
os.symlink("dir", ABSTFN + "\\link2")
os.symlink("nonexistent", ABSTFN + "\\broken")
os.symlink("cycle", ABSTFN + "\\cycle")
def check(path, modes, expected, errno=None):
path = path.replace('/', '\\')
if isinstance(expected, str):
assert errno is None
expected = expected.replace('/', os.sep)
for mode in modes:
with self.subTest(mode=mode):
self.assertEqual(realpath(path, strict=mode),
ABSTFN + expected)
else:
for mode in modes:
with self.subTest(mode=mode):
with self.assertRaises(expected) as cm:
realpath(path, strict=mode)
if errno is not None:
self.assertEqual(cm.exception.errno, errno)
self.enterContext(os_helper.change_cwd(ABSTFN))
all_modes = [False, ALLOW_MISSING, ALL_BUT_LAST, True]
check("file", all_modes, "/file")
check("file/", all_modes, "/file")
check("file/file2", [False, ALLOW_MISSING], "/file/file2")
check("file/file2", [ALL_BUT_LAST, True], FileNotFoundError)
check("file/.", all_modes, "/file")
check("file/../link2", all_modes, "/dir")
check("dir", all_modes, "/dir")
check("dir/", all_modes, "/dir")
check("dir/file2", all_modes, "/dir/file2")
check("link", all_modes, "/file")
check("link/", all_modes, "/file")
check("link/file2", [False, ALLOW_MISSING], "/file/file2")
check("link/file2", [ALL_BUT_LAST, True], FileNotFoundError)
check("link/.", all_modes, "/file")
check("link/../link", all_modes, "/file")
check("link2", all_modes, "/dir")
check("link2/", all_modes, "/dir")
check("link2/file2", all_modes, "/dir/file2")
check("nonexistent", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("nonexistent", [True], FileNotFoundError)
check("nonexistent/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("nonexistent/", [True], FileNotFoundError)
check("nonexistent/file", [False, ALLOW_MISSING], "/nonexistent/file")
check("nonexistent/file", [ALL_BUT_LAST, True], FileNotFoundError)
check("nonexistent/../link", all_modes, "/file")
check("broken", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("broken", [True], FileNotFoundError)
check("broken/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("broken/", [True], FileNotFoundError)
check("broken/file", [False, ALLOW_MISSING], "/nonexistent/file")
check("broken/file", [ALL_BUT_LAST, True], FileNotFoundError)
check("broken/../link", all_modes, "/file")
check("cycle", [False], "/cycle")
check("cycle", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.EINVAL)
check("cycle/", [False], "/cycle")
check("cycle/", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.EINVAL)
check("cycle/file", [False], "/cycle/file")
check("cycle/file", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.EINVAL)
check("cycle/../link", all_modes, "/file")
def test_expandvars(self): def test_expandvars(self):
with os_helper.EnvironmentVarGuard() as env: with os_helper.EnvironmentVarGuard() as env:
env.clear() env.clear()

View file

@ -1,3 +1,4 @@
import errno
import inspect import inspect
import os import os
import posixpath import posixpath
@ -5,7 +6,7 @@ import random
import sys import sys
import unittest import unittest
from functools import partial from functools import partial
from posixpath import realpath, abspath, dirname, basename, ALLOW_MISSING from posixpath import realpath, abspath, dirname, basename, ALL_BUT_LAST, ALLOW_MISSING
from test import support from test import support
from test import test_genericpath from test import test_genericpath
from test.support import import_helper from test.support import import_helper
@ -448,7 +449,7 @@ class PosixPathTest(unittest.TestCase):
self.assertEqual(result, expected) self.assertEqual(result, expected)
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_curdir(self, kwargs): def test_realpath_curdir(self, kwargs):
self.assertEqual(realpath('.', **kwargs), os.getcwd()) self.assertEqual(realpath('.', **kwargs), os.getcwd())
self.assertEqual(realpath('./.', **kwargs), os.getcwd()) self.assertEqual(realpath('./.', **kwargs), os.getcwd())
@ -459,7 +460,7 @@ class PosixPathTest(unittest.TestCase):
self.assertEqual(realpath(b'/'.join([b'.'] * 100), **kwargs), os.getcwdb()) self.assertEqual(realpath(b'/'.join([b'.'] * 100), **kwargs), os.getcwdb())
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_pardir(self, kwargs): def test_realpath_pardir(self, kwargs):
self.assertEqual(realpath('..', **kwargs), dirname(os.getcwd())) self.assertEqual(realpath('..', **kwargs), dirname(os.getcwd()))
self.assertEqual(realpath('../..', **kwargs), dirname(dirname(os.getcwd()))) self.assertEqual(realpath('../..', **kwargs), dirname(dirname(os.getcwd())))
@ -495,35 +496,43 @@ class PosixPathTest(unittest.TestCase):
def test_realpath_invalid_paths(self): def test_realpath_invalid_paths(self):
path = '/\x00' path = '/\x00'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
path = b'/\x00' path = b'/\x00'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
path = '/nonexistent/x\x00' path = '/nonexistent/x\x00'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
path = b'/nonexistent/x\x00' path = b'/nonexistent/x\x00'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
path = '/\x00/..' path = '/\x00/..'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
path = b'/\x00/..' path = b'/\x00/..'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
path = '/nonexistent/x\x00/..' path = '/nonexistent/x\x00/..'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
path = b'/nonexistent/x\x00/..' path = b'/nonexistent/x\x00/..'
self.assertRaises(ValueError, realpath, path, strict=False) self.assertRaises(ValueError, realpath, path, strict=False)
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING)
@ -534,6 +543,7 @@ class PosixPathTest(unittest.TestCase):
self.assertEqual(realpath(path, strict=ALLOW_MISSING), path) self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
else: else:
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
path = '/nonexistent/\udfff' path = '/nonexistent/\udfff'
@ -543,6 +553,7 @@ class PosixPathTest(unittest.TestCase):
else: else:
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
path = '/\udfff/..' path = '/\udfff/..'
if sys.platform == 'win32': if sys.platform == 'win32':
@ -551,6 +562,7 @@ class PosixPathTest(unittest.TestCase):
self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/') self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/')
else: else:
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) self.assertRaises(UnicodeEncodeError, realpath, path, strict=True)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
path = '/nonexistent/\udfff/..' path = '/nonexistent/\udfff/..'
@ -560,6 +572,7 @@ class PosixPathTest(unittest.TestCase):
else: else:
self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) self.assertRaises(UnicodeEncodeError, realpath, path, strict=False)
self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING)
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
path = b'/\xff' path = b'/\xff'
@ -570,9 +583,11 @@ class PosixPathTest(unittest.TestCase):
else: else:
self.assertEqual(realpath(path, strict=False), path) self.assertEqual(realpath(path, strict=False), path)
if support.is_wasi: if support.is_wasi:
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
else: else:
self.assertEqual(realpath(path, strict=ALL_BUT_LAST), path)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
self.assertEqual(realpath(path, strict=ALLOW_MISSING), path) self.assertEqual(realpath(path, strict=ALLOW_MISSING), path)
path = b'/nonexistent/\xff' path = b'/nonexistent/\xff'
@ -582,14 +597,16 @@ class PosixPathTest(unittest.TestCase):
else: else:
self.assertEqual(realpath(path, strict=False), path) self.assertEqual(realpath(path, strict=False), path)
if support.is_wasi: if support.is_wasi:
self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=True)
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
else: else:
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(FileNotFoundError, realpath, path, strict=True)
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_relative(self, kwargs): def test_realpath_relative(self, kwargs):
try: try:
os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN) os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN)
@ -599,12 +616,14 @@ class PosixPathTest(unittest.TestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': ALLOW_MISSING}) def test_realpath_missing_pardir(self):
def test_realpath_missing_pardir(self, kwargs):
try: try:
os.symlink(TESTFN + "1", TESTFN) os.symlink(TESTFN + "1", TESTFN)
self.assertEqual( path = "nonexistent/../" + TESTFN
realpath("nonexistent/../" + TESTFN, **kwargs), ABSTFN + "1") self.assertEqual(realpath(path), ABSTFN + "1")
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + "1")
self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST)
self.assertRaises(FileNotFoundError, realpath, path, strict=True)
finally: finally:
os_helper.unlink(TESTFN) os_helper.unlink(TESTFN)
@ -651,7 +670,7 @@ class PosixPathTest(unittest.TestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_symlink_loops_strict(self, kwargs): def test_realpath_symlink_loops_strict(self, kwargs):
# Bug #43757, raise OSError if we get into an infinite symlink loop in # Bug #43757, raise OSError if we get into an infinite symlink loop in
# the strict modes. # the strict modes.
@ -693,7 +712,7 @@ class PosixPathTest(unittest.TestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_repeated_indirect_symlinks(self, kwargs): def test_realpath_repeated_indirect_symlinks(self, kwargs):
# Issue #6975. # Issue #6975.
try: try:
@ -708,7 +727,7 @@ class PosixPathTest(unittest.TestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_deep_recursion(self, kwargs): def test_realpath_deep_recursion(self, kwargs):
depth = 10 depth = 10
try: try:
@ -728,7 +747,7 @@ class PosixPathTest(unittest.TestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_resolve_parents(self, kwargs): def test_realpath_resolve_parents(self, kwargs):
# We also need to resolve any symlinks in the parents of a relative # We also need to resolve any symlinks in the parents of a relative
# path passed to realpath. E.g.: current working directory is # path passed to realpath. E.g.: current working directory is
@ -749,7 +768,7 @@ class PosixPathTest(unittest.TestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_resolve_before_normalizing(self, kwargs): def test_realpath_resolve_before_normalizing(self, kwargs):
# Bug #990669: Symbolic links should be resolved before we # Bug #990669: Symbolic links should be resolved before we
# normalize the path. E.g.: if we have directories 'a', 'k' and 'y' # normalize the path. E.g.: if we have directories 'a', 'k' and 'y'
@ -778,7 +797,7 @@ class PosixPathTest(unittest.TestCase):
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_resolve_first(self, kwargs): def test_realpath_resolve_first(self, kwargs):
# Bug #1213894: The first component of the path, if not absolute, # Bug #1213894: The first component of the path, if not absolute,
# must be resolved too. # must be resolved too.
@ -816,7 +835,7 @@ class PosixPathTest(unittest.TestCase):
@skip_if_ABSTFN_contains_backslash @skip_if_ABSTFN_contains_backslash
@unittest.skipIf(os.chmod not in os.supports_follow_symlinks, "Can't set symlink permissions") @unittest.skipIf(os.chmod not in os.supports_follow_symlinks, "Can't set symlink permissions")
@unittest.skipIf(sys.platform != "darwin", "only macOS requires read permission to readlink()") @unittest.skipIf(sys.platform != "darwin", "only macOS requires read permission to readlink()")
@_parameterize({'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_realpath_unreadable_symlink_strict(self, kwargs): def test_realpath_unreadable_symlink_strict(self, kwargs):
try: try:
os.symlink(ABSTFN+"1", ABSTFN) os.symlink(ABSTFN+"1", ABSTFN)
@ -842,6 +861,7 @@ class PosixPathTest(unittest.TestCase):
os.chmod(ABSTFN, 0o000) os.chmod(ABSTFN, 0o000)
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN)
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN)
self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN)
self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN)
try: try:
@ -855,6 +875,8 @@ class PosixPathTest(unittest.TestCase):
ABSTFN + '/k') ABSTFN + '/k')
self.assertRaises(PermissionError, realpath, ABSTFN + '/k', self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
strict=True) strict=True)
self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
strict=ALL_BUT_LAST)
self.assertRaises(PermissionError, realpath, ABSTFN + '/k', self.assertRaises(PermissionError, realpath, ABSTFN + '/k',
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
@ -862,6 +884,8 @@ class PosixPathTest(unittest.TestCase):
ABSTFN + '/missing') ABSTFN + '/missing')
self.assertRaises(PermissionError, realpath, ABSTFN + '/missing', self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
strict=True) strict=True)
self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
strict=ALL_BUT_LAST)
self.assertRaises(PermissionError, realpath, ABSTFN + '/missing', self.assertRaises(PermissionError, realpath, ABSTFN + '/missing',
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
finally: finally:
@ -875,25 +899,30 @@ class PosixPathTest(unittest.TestCase):
with open(ABSTFN, 'w') as f: with open(ABSTFN, 'w') as f:
f.write('test_posixpath wuz ere') f.write('test_posixpath wuz ere')
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN) 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=True), ABSTFN)
self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN)
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN) self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN) self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "/subdir") self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "/subdir")
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
@ -908,25 +937,30 @@ class PosixPathTest(unittest.TestCase):
f.write('test_posixpath wuz ere') f.write('test_posixpath wuz ere')
os.symlink(ABSTFN + "1", ABSTFN) os.symlink(ABSTFN + "1", ABSTFN)
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1") 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=True), ABSTFN + "1")
self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN + "1") self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN + "1")
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "1") self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "1")
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "1") self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "1")
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "1/subdir") self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "1/subdir")
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
@ -943,25 +977,30 @@ class PosixPathTest(unittest.TestCase):
os.symlink(ABSTFN + "2", ABSTFN + "1") os.symlink(ABSTFN + "2", ABSTFN + "1")
os.symlink(ABSTFN + "1", ABSTFN) os.symlink(ABSTFN + "1", ABSTFN)
self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2") self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2")
self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN + "2")
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2") self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2") self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2")
self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "2") self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "2")
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "2") self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "2")
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN))
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "2/subdir") self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "2/subdir")
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALL_BUT_LAST)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True)
self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir",
strict=ALLOW_MISSING) strict=ALLOW_MISSING)
@ -970,6 +1009,98 @@ class PosixPathTest(unittest.TestCase):
os_helper.unlink(ABSTFN + "1") os_helper.unlink(ABSTFN + "1")
os_helper.unlink(ABSTFN + "2") os_helper.unlink(ABSTFN + "2")
@os_helper.skip_unless_symlink
def test_realpath_mode(self):
self.addCleanup(os_helper.rmdir, ABSTFN)
self.addCleanup(os_helper.rmdir, ABSTFN + "/dir")
self.addCleanup(os_helper.unlink, ABSTFN + "/file")
self.addCleanup(os_helper.unlink, ABSTFN + "/dir/file2")
self.addCleanup(os_helper.unlink, ABSTFN + "/link")
self.addCleanup(os_helper.unlink, ABSTFN + "/link2")
self.addCleanup(os_helper.unlink, ABSTFN + "/broken")
self.addCleanup(os_helper.unlink, ABSTFN + "/cycle")
os.mkdir(ABSTFN)
os.mkdir(ABSTFN + "/dir")
open(ABSTFN + "/file", "wb").close()
open(ABSTFN + "/dir/file2", "wb").close()
os.symlink("file", ABSTFN + "/link")
os.symlink("dir", ABSTFN + "/link2")
os.symlink("nonexistent", ABSTFN + "/broken")
os.symlink("cycle", ABSTFN + "/cycle")
def check(path, modes, expected, errno=None):
if isinstance(expected, str):
assert errno is None
expected = expected.replace('/', os.sep)
for mode in modes:
with self.subTest(mode=mode):
self.assertEqual(realpath(path, strict=mode).replace('/', os.sep),
ABSTFN.replace('/', os.sep) + expected)
else:
for mode in modes:
with self.subTest(mode=mode):
with self.assertRaises(expected) as cm:
realpath(path, strict=mode)
if errno is not None:
self.assertEqual(cm.exception.errno, errno)
self.enterContext(os_helper.change_cwd(ABSTFN))
all_modes = [False, ALLOW_MISSING, ALL_BUT_LAST, True]
check("file", all_modes, "/file")
check("file/", [False], "/file")
check("file/", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("file/file2", [False], "/file/file2")
check("file/file2", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("file/.", [False], "/file")
check("file/.", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("file/../link2", [False], "/dir")
check("file/../link2", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("dir", all_modes, "/dir")
check("dir/", all_modes, "/dir")
check("dir/file2", all_modes, "/dir/file2")
check("link", all_modes, "/file")
check("link/", [False], "/file")
check("link/", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("link/file2", [False], "/file/file2")
check("link/file2", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("link/.", [False], "/file")
check("link/.", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("link/../link", [False], "/file")
check("link/../link", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError)
check("link2", all_modes, "/dir")
check("link2/", all_modes, "/dir")
check("link2/file2", all_modes, "/dir/file2")
check("nonexistent", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("nonexistent", [True], FileNotFoundError)
check("nonexistent/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("nonexistent/", [True], FileNotFoundError)
check("nonexistent/file", [False, ALLOW_MISSING], "/nonexistent/file")
check("nonexistent/file", [ALL_BUT_LAST, True], FileNotFoundError)
check("nonexistent/../link", [False, ALLOW_MISSING], "/file")
check("nonexistent/../link", [ALL_BUT_LAST, True], FileNotFoundError)
check("broken", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("broken", [True], FileNotFoundError)
check("broken/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent")
check("broken/", [True], FileNotFoundError)
check("broken/file", [False, ALLOW_MISSING], "/nonexistent/file")
check("broken/file", [ALL_BUT_LAST, True], FileNotFoundError)
check("broken/../link", [False, ALLOW_MISSING], "/file")
check("broken/../link", [ALL_BUT_LAST, True], FileNotFoundError)
check("cycle", [False], "/cycle")
check("cycle", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP)
check("cycle/", [False], "/cycle")
check("cycle/", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP)
check("cycle/file", [False], "/cycle/file")
check("cycle/file", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP)
check("cycle/../link", [False], "/file")
check("cycle/../link", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP)
def test_relpath(self): def test_relpath(self):
(real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar")
try: try:
@ -1152,7 +1283,7 @@ class PathLikeTests(unittest.TestCase):
def test_path_abspath(self): def test_path_abspath(self):
self.assertPathEqual(self.path.abspath) self.assertPathEqual(self.path.abspath)
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING})
def test_path_realpath(self, kwargs): def test_path_realpath(self, kwargs):
self.assertPathEqual(self.path.realpath) self.assertPathEqual(self.path.realpath)

View file

@ -0,0 +1 @@
Add support of the all-but-last mode in :func:`os.path.realpath`.