Skip to content

Commit 25cc0a2

Browse files
committed
Fix gh-140774: os.chmod fails to clear Read-Only if Archive bit is cleared
1 parent 9cf6cd7 commit 25cc0a2

2 files changed

Lines changed: 37 additions & 12 deletions

File tree

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import stat
1313
import tempfile
1414
import unittest
15+
import subprocess
1516
from unittest import mock
1617
from urllib.request import pathname2url
1718

@@ -3622,33 +3623,52 @@ def test_walk_symlink_location(self):
36223623
class PosixPathTest(PathTest, PurePosixPathTest):
36233624
cls = pathlib.PosixPath
36243625

3626+
@unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system')
3627+
class WindowsPathTest(PathTest, PureWindowsPathTest):
3628+
cls = pathlib.WindowsPath
3629+
36253630
def test_chmod_archive_bit_behavior(self):
3631+
import subprocess
3632+
3633+
# gh-140774: Fix chmod failing to clear Read-Only if Archive bit is cleared.
36263634
base = self.cls(self.base)
36273635
filename = base / 'test_chmod_archive.txt'
36283636
filename.touch()
3629-
self.addCleanup(filename.unlink, missing_ok=True)
36303637

3631-
# Helper to check read-only status
3638+
# Robust cleanup: Force write permission before deleting
3639+
def force_remove():
3640+
try:
3641+
os.chmod(filename, stat.S_IWRITE)
3642+
except OSError:
3643+
pass
3644+
try:
3645+
filename.unlink(missing_ok=True)
3646+
except OSError:
3647+
pass
3648+
3649+
self.addCleanup(force_remove)
3650+
36323651
def is_read_only(p):
3633-
return (p.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY) > 0
3652+
try:
3653+
return (p.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY) > 0
3654+
except AttributeError:
3655+
return False
3656+
36343657
try:
36353658
# Case 1: Archive bit CLEARED, Read-Only SET
3636-
# We use attrib command to manipulate the Archive bit directly
3659+
# We use the Windows 'attrib' command to manipulate the Archive bit directly
36373660
subprocess.run(['attrib', '-a', '+r', str(filename)], check=True, shell=True)
36383661

3639-
# This line used to fail silently (bug #140774)
3662+
# Try to make it Writable (clearing the Read-Only flag)
36403663
filename.chmod(stat.S_IWRITE | stat.S_IREAD)
36413664

36423665
self.assertFalse(is_read_only(filename),
3643-
"chmod failed to clear Read-Only when Archive bit was cleared")
3666+
"chmod failed to clear Read-Only when Archive bit was cleared")
36443667

36453668
except subprocess.CalledProcessError:
3646-
self.skipTest("attrib command failed or not available")
3647-
3648-
@unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system')
3649-
class WindowsPathTest(PathTest, PureWindowsPathTest):
3650-
cls = pathlib.WindowsPath
3651-
3669+
self.skipTest("attrib command failed")
3670+
except FileNotFoundError:
3671+
self.skipTest("attrib command not found")
36523672

36533673
class PathSubclassTest(PathTest):
36543674
class cls(pathlib.Path):

Modules/posixmodule.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3987,6 +3987,11 @@ win32_lchmod(LPCWSTR path, int mode)
39873987
if (mode & _S_IWRITE) {
39883988
attr &= ~FILE_ATTRIBUTE_READONLY;
39893989
}
3990+
if (attr == 0) {
3991+
/* gh-140774: If all attributes are cleared, set to NORMAL
3992+
to avoid failing to clear the Read-Only bit. */
3993+
attr = FILE_ATTRIBUTE_NORMAL;
3994+
}
39903995
else {
39913996
attr |= FILE_ATTRIBUTE_READONLY;
39923997
}

0 commit comments

Comments
 (0)