diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 44f967eb12dd4f..f26b3e9a338668 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1222,7 +1222,8 @@ def chmod(self, mode, *, follow_symlinks=True): """ Change the permissions of the path, like os.chmod(). """ - os.chmod(self, mode, follow_symlinks=follow_symlinks) + # GH-127380: Force string path to ensure Windows archive bit handling works + os.chmod(str(self), mode, follow_symlinks=follow_symlinks) def lchmod(self, mode): """ diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index ef9ea0d11d06a6..6730994c8d9510 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -12,6 +12,7 @@ import stat import tempfile import unittest +import subprocess from unittest import mock from urllib.request import pathname2url @@ -3622,11 +3623,51 @@ def test_walk_symlink_location(self): class PosixPathTest(PathTest, PurePosixPathTest): cls = pathlib.PosixPath - @unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system') class WindowsPathTest(PathTest, PureWindowsPathTest): cls = pathlib.WindowsPath + def test_chmod_archive_bit_behavior(self): + + # gh-140774: Fix chmod failing to clear Read-Only if Archive bit is cleared. + base = self.cls(self.base) + filename = base / 'test_chmod_archive.txt' + filename.touch() + + # Robust cleanup: Force write permission before deleting + def force_remove(): + try: + os.chmod(filename, stat.S_IWRITE) + except OSError: + pass + try: + filename.unlink(missing_ok=True) + except OSError: + pass + + self.addCleanup(force_remove) + + def is_read_only(p): + try: + return (p.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY) > 0 + except AttributeError: + return False + + try: + # Case 1: Archive bit CLEARED, Read-Only SET + # We use the Windows 'attrib' command to manipulate the Archive bit directly + subprocess.run(['attrib', '-a', '+r', str(filename)], check=True, shell=True) + + # Try to make it Writable (clearing the Read-Only flag) + filename.chmod(stat.S_IWRITE | stat.S_IREAD) + + self.assertFalse(is_read_only(filename), + "chmod failed to clear Read-Only when Archive bit was cleared") + + except subprocess.CalledProcessError: + self.skipTest("attrib command failed") + except FileNotFoundError: + self.skipTest("attrib command not found") class PathSubclassTest(PathTest): class cls(pathlib.Path): diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index fc609b2707c6c6..53b09c8f32138b 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -3986,6 +3986,11 @@ win32_lchmod(LPCWSTR path, int mode) } if (mode & _S_IWRITE) { attr &= ~FILE_ATTRIBUTE_READONLY; + + if (attr == 0) { + /* gh-140774: If all attributes are cleared, set to NORMAL + to avoid failing to clear the Read-Only bit. */ + attr = FILE_ATTRIBUTE_NORMAL; } else { attr |= FILE_ATTRIBUTE_READONLY;