Skip to content

Commit

Permalink
pythongh-59616: Support os.chmod(follow_symlinks=True) and os.lchmod(…
Browse files Browse the repository at this point in the history
…) on Windows
  • Loading branch information
serhiy-storchaka committed Dec 13, 2023
1 parent 3531ea4 commit 15c667b
Show file tree
Hide file tree
Showing 7 changed files with 401 additions and 28 deletions.
1 change: 1 addition & 0 deletions Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ def _add(str, fn):
_add("HAVE_FSTATAT", "stat")
_add("HAVE_LCHFLAGS", "chflags")
_add("HAVE_LCHMOD", "chmod")
_add("MS_WINDOWS", "chmod")
if _exists("lchown"): # mac os x10.3
_add("HAVE_LCHOWN", "chown")
_add("HAVE_LINKAT", "link")
Expand Down
2 changes: 1 addition & 1 deletion Lib/tempfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def _dont_follow_symlinks(func, path, *args):
# Pass follow_symlinks=False, unless not supported on this platform.
if func in _os.supports_follow_symlinks:
func(path, *args, follow_symlinks=False)
elif _os.name == 'nt' or not _os.path.islink(path):
elif not _os.path.islink(path):
func(path, *args)

def _resetperms(path):
Expand Down
1 change: 1 addition & 0 deletions Lib/test/libregrtest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
self.fail_rerun)

def run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
selected = ('test_posix', 'test_tempfile')
os.makedirs(self.tmp_dir, exist_ok=True)
work_dir = get_work_dir(self.tmp_dir)

Expand Down
50 changes: 50 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -3117,6 +3117,56 @@ def test_directory_link_nonlocal(self):
os.symlink('some_dir', src)
assert os.path.isdir(src)

def test_chmod_link(self):
TESTFN2 = support.TESTFN+"_link"

def cleanup():
if os.path.exists(TESTFN2):
os.chmod(TESTFN2, 0o100666)
os.unlink(TESTFN2)

if os.path.exists(support.TESTFN):
os.chmod(support.TESTFN, 0o666)
os.unlink(support.TESTFN)

cleanup()

open(support.TESTFN, "w").close()
try:
os.symlink(support.TESTFN, TESTFN2)
os.chmod(TESTFN2, 0o444)
self.assertEqual(os.stat(TESTFN2).st_mode & 0o777, 0o444)

os.chmod(TESTFN2, 0o666)
self.assertEqual(os.stat(support.TESTFN).st_mode & 0o777, 0o666)
finally:
cleanup()

def test_chmod_link(self):

Check failure on line 3145 in Lib/test/test_os.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (F811)

Lib/test/test_os.py:3145:9: F811 Redefinition of unused `test_chmod_link` from line 3120
TESTFN2 = support.TESTFN+"_link"

def cleanup():
if os.path.exists(TESTFN2):
os.chmod(TESTFN2, 0o100666)
os.unlink(TESTFN2)

if os.path.exists(support.TESTFN):
os.chmod(support.TESTFN, 0o666)
os.unlink(support.TESTFN)

cleanup()

open(support.TESTFN, "w").close()
try:
os.symlink(support.TESTFN, TESTFN2)
os.chmod(TESTFN2, 0o444)
self.assertEqual(os.stat(TESTFN2).st_mode & 0o777, 0o444)

os.chmod(TESTFN2, 0o666)
self.assertEqual(os.stat(support.TESTFN).st_mode & 0o777, 0o666)
finally:
cleanup()


class FSEncodingTests(unittest.TestCase):
def test_nop(self):
Expand Down
114 changes: 113 additions & 1 deletion Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@

try:
import posix
nt = None
except ImportError:
import nt as posix
import nt
posix = nt

try:
import pwd
Expand Down Expand Up @@ -935,6 +937,116 @@ def test_utime(self):
posix.utime(os_helper.TESTFN, (int(now), int(now)))
posix.utime(os_helper.TESTFN, (now, now))

def check_chmod(self, chmod_func, target, **kwargs):
mode = os.stat(target).st_mode
try:
chmod_func(target, mode & ~stat.S_IWRITE, **kwargs)
self.assertEqual(os.stat(target).st_mode, mode & ~stat.S_IWRITE)
if stat.S_ISREG(mode):
try:
with open(target, 'wb+'):
pass
except PermissionError:
pass
chmod_func(target, mode | stat.S_IWRITE, **kwargs)
self.assertEqual(os.stat(target).st_mode, mode | stat.S_IWRITE)
if stat.S_ISREG(mode):
with open(target, 'wb+'):
pass
finally:
posix.chmod(target, mode)

def test_chmod_file(self):
self.check_chmod(posix.chmod, os_helper.TESTFN)

def tempdir(self):
target = os_helper.TESTFN + 'd'
posix.mkdir(target)
self.addCleanup(posix.rmdir, target)
return target

def test_chmod_dir(self):
target = self.tempdir()
self.check_chmod(posix.chmod, target)

@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
def test_lchmod_file(self):
self.check_chmod(posix.lchmod, os_helper.TESTFN)
self.check_chmod(posix.chmod, os_helper.TESTFN, follow_symlinks=False)

@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
def test_lchmod_dir(self):
target = self.tempdir()
self.check_chmod(posix.lchmod, target)
self.check_chmod(posix.chmod, target, follow_symlinks=False)

def check_chmod_link(self, chmod_func, target, link, **kwargs):
target_mode = os.stat(target).st_mode
link_mode = os.lstat(link).st_mode
try:
chmod_func(link, target_mode & ~stat.S_IWRITE, **kwargs)
self.assertEqual(os.stat(target).st_mode, target_mode & ~stat.S_IWRITE)
self.assertEqual(os.lstat(link).st_mode, link_mode)
chmod_func(link, target_mode | stat.S_IWRITE)
self.assertEqual(os.stat(target).st_mode, target_mode | stat.S_IWRITE)
self.assertEqual(os.lstat(link).st_mode, link_mode)
finally:
posix.chmod(target, target_mode)

def check_lchmod_link(self, chmod_func, target, link, **kwargs):
target_mode = os.stat(target).st_mode
link_mode = os.lstat(link).st_mode
chmod_func(link, link_mode & ~stat.S_IWRITE, **kwargs)
self.assertEqual(os.stat(target).st_mode, target_mode)
self.assertEqual(os.lstat(link).st_mode, link_mode & ~stat.S_IWRITE)
chmod_func(link, link_mode | stat.S_IWRITE)
self.assertEqual(os.stat(target).st_mode, target_mode)
self.assertEqual(os.lstat(link).st_mode, link_mode | stat.S_IWRITE)

@os_helper.skip_unless_symlink
def test_chmod_file_symlink(self):
target = os_helper.TESTFN
link = os_helper.TESTFN + '-link'
os.symlink(target, link)
self.addCleanup(posix.unlink, link)
if os.name == 'nt':
self.check_lchmod_link(posix.chmod, target, link)
else:
self.check_chmod_link(posix.chmod, target, link)
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)

@os_helper.skip_unless_symlink
def test_chmod_dir_symlink(self):
target = self.tempdir()
link = os_helper.TESTFN + '-link'
os.symlink(target, link, target_is_directory=True)
self.addCleanup(posix.unlink, link)
if os.name == 'nt':
self.check_lchmod_link(posix.chmod, target, link)
else:
self.check_chmod_link(posix.chmod, target, link)
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)

@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
@os_helper.skip_unless_symlink
def test_lchmod_file_symlink(self):
target = os_helper.TESTFN
link = os_helper.TESTFN + '-link'
os.symlink(target, link)
self.addCleanup(posix.unlink, link)
self.check_lchmod_link(posix.chmod, target, link, follow_symlinks=False)
self.check_lchmod_link(posix.lchmod, target, link)

@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
@os_helper.skip_unless_symlink
def test_lchmod_dir_symlink(self):
target = self.tempdir()
link = os_helper.TESTFN + '-link'
os.symlink(target, link)
self.addCleanup(posix.unlink, link)
self.check_lchmod_link(posix.chmod, target, link, follow_symlinks=False)
self.check_lchmod_link(posix.lchmod, target, link)

def _test_chflags_regular_file(self, chflags_func, target_file, **kwargs):
st = os.stat(target_file)
self.assertTrue(hasattr(st, 'st_flags'))
Expand Down
9 changes: 5 additions & 4 deletions Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 15c667b

Please sign in to comment.