Skip to content

Commit 29f7eb4

Browse files
gh-59616: Support os.chmod(follow_symlinks=True) and os.lchmod() on Windows (GH-113049)
1 parent c6e953b commit 29f7eb4

File tree

9 files changed

+93
-28
lines changed

9 files changed

+93
-28
lines changed

Doc/library/os.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2062,6 +2062,7 @@ features:
20622062
Although Windows supports :func:`chmod`, you can only set the file's
20632063
read-only flag with it (via the ``stat.S_IWRITE`` and ``stat.S_IREAD``
20642064
constants or a corresponding integer value). All other bits are ignored.
2065+
The default value of *follow_symlinks* is ``False`` on Windows.
20652066

20662067
The function is limited on Emscripten and WASI, see
20672068
:ref:`wasm-availability` for more information.
@@ -2075,6 +2076,9 @@ features:
20752076
.. versionchanged:: 3.6
20762077
Accepts a :term:`path-like object`.
20772078

2079+
.. versionchanged:: 3.13
2080+
Added support for the *follow_symlinks* argument on Windows.
2081+
20782082

20792083
.. function:: chown(path, uid, gid, *, dir_fd=None, follow_symlinks=True)
20802084

@@ -2165,11 +2169,14 @@ features:
21652169

21662170
.. audit-event:: os.chmod path,mode,dir_fd os.lchmod
21672171

2168-
.. availability:: Unix, not Linux, FreeBSD >= 1.3, NetBSD >= 1.3, not OpenBSD
2172+
.. availability:: Unix, Windows, not Linux, FreeBSD >= 1.3, NetBSD >= 1.3, not OpenBSD
21692173

21702174
.. versionchanged:: 3.6
21712175
Accepts a :term:`path-like object`.
21722176

2177+
.. versionchanged:: 3.13
2178+
Added support on Windows.
2179+
21732180
.. function:: lchown(path, uid, gid)
21742181

21752182
Change the owner and group id of *path* to the numeric *uid* and *gid*. This

Doc/whatsnew/3.13.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,12 @@ os
261261
CPU resources of a container system without having to modify the container (application code).
262262
(Contributed by Donghee Na in :gh:`109595`)
263263

264+
* Add support of :func:`os.lchmod` and the *follow_symlinks* argument
265+
in :func:`os.chmod` on Windows.
266+
Note that the default value of *follow_symlinks* in :func:`!os.lchmod` is
267+
``False`` on Windows.
268+
(Contributed by Serhiy Storchaka in :gh:`59616`)
269+
264270
pathlib
265271
-------
266272

Lib/os.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def _add(str, fn):
171171
_add("HAVE_FSTATAT", "stat")
172172
_add("HAVE_LCHFLAGS", "chflags")
173173
_add("HAVE_LCHMOD", "chmod")
174+
_add("MS_WINDOWS", "chmod")
174175
if _exists("lchown"): # mac os x10.3
175176
_add("HAVE_LCHOWN", "chown")
176177
_add("HAVE_LINKAT", "link")

Lib/tempfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def _dont_follow_symlinks(func, path, *args):
273273
# Pass follow_symlinks=False, unless not supported on this platform.
274274
if func in _os.supports_follow_symlinks:
275275
func(path, *args, follow_symlinks=False)
276-
elif _os.name == 'nt' or not _os.path.islink(path):
276+
elif not _os.path.islink(path):
277277
func(path, *args)
278278

279279
def _resetperms(path):

Lib/test/test_posix.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,7 @@ def test_chmod_file_symlink(self):
10191019
self.check_lchmod_link(posix.chmod, target, link)
10201020
else:
10211021
self.check_chmod_link(posix.chmod, target, link)
1022-
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
1022+
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
10231023

10241024
@os_helper.skip_unless_symlink
10251025
def test_chmod_dir_symlink(self):
@@ -1031,7 +1031,7 @@ def test_chmod_dir_symlink(self):
10311031
self.check_lchmod_link(posix.chmod, target, link)
10321032
else:
10331033
self.check_chmod_link(posix.chmod, target, link)
1034-
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
1034+
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
10351035

10361036
@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
10371037
@os_helper.skip_unless_symlink
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add support of :func:`os.lchmod` and the *follow_symlinks* argument in
2+
:func:`os.chmod` on Windows. Note that the default value of *follow_symlinks*
3+
in :func:`!os.lchmod` is ``False`` on Windows.

Modules/clinic/posixmodule.c.h

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3309,6 +3309,29 @@ os_fchdir_impl(PyObject *module, int fd)
33093309
}
33103310
#endif /* HAVE_FCHDIR */
33113311

3312+
#ifdef MS_WINDOWS
3313+
# define CHMOD_DEFAULT_FOLLOW_SYMLINKS 0
3314+
#else
3315+
# define CHMOD_DEFAULT_FOLLOW_SYMLINKS 1
3316+
#endif
3317+
3318+
#ifdef MS_WINDOWS
3319+
static int
3320+
win32_lchmod(LPCWSTR path, int mode)
3321+
{
3322+
DWORD attr = GetFileAttributesW(path);
3323+
if (attr == INVALID_FILE_ATTRIBUTES) {
3324+
return 0;
3325+
}
3326+
if (mode & _S_IWRITE) {
3327+
attr &= ~FILE_ATTRIBUTE_READONLY;
3328+
}
3329+
else {
3330+
attr |= FILE_ATTRIBUTE_READONLY;
3331+
}
3332+
return SetFileAttributesW(path, attr);
3333+
}
3334+
#endif
33123335

33133336
/*[clinic input]
33143337
os.chmod
@@ -3331,7 +3354,8 @@ os.chmod
33313354
and path should be relative; path will then be relative to that
33323355
directory.
33333356
3334-
follow_symlinks: bool = True
3357+
follow_symlinks: bool(c_default="CHMOD_DEFAULT_FOLLOW_SYMLINKS", \
3358+
py_default="(os.name != 'nt')") = CHMOD_DEFAULT_FOLLOW_SYMLINKS
33353359
If False, and the last element of the path is a symbolic link,
33363360
chmod will modify the symbolic link itself instead of the file
33373361
the link points to.
@@ -3348,20 +3372,16 @@ dir_fd and follow_symlinks may not be implemented on your platform.
33483372
static PyObject *
33493373
os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
33503374
int follow_symlinks)
3351-
/*[clinic end generated code: output=5cf6a94915cc7bff input=674a14bc998de09d]*/
3375+
/*[clinic end generated code: output=5cf6a94915cc7bff input=fcf115d174b9f3d8]*/
33523376
{
33533377
int result;
33543378

3355-
#ifdef MS_WINDOWS
3356-
DWORD attr;
3357-
#endif
3358-
33593379
#ifdef HAVE_FCHMODAT
33603380
int fchmodat_nofollow_unsupported = 0;
33613381
int fchmodat_unsupported = 0;
33623382
#endif
33633383

3364-
#if !(defined(HAVE_FCHMODAT) || defined(HAVE_LCHMOD))
3384+
#if !(defined(HAVE_FCHMODAT) || defined(HAVE_LCHMOD) || defined(MS_WINDOWS))
33653385
if (follow_symlinks_specified("chmod", follow_symlinks))
33663386
return NULL;
33673387
#endif
@@ -3372,19 +3392,36 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
33723392
}
33733393

33743394
#ifdef MS_WINDOWS
3395+
result = 0;
33753396
Py_BEGIN_ALLOW_THREADS
3376-
attr = GetFileAttributesW(path->wide);
3377-
if (attr == INVALID_FILE_ATTRIBUTES)
3378-
result = 0;
3397+
if (follow_symlinks) {
3398+
HANDLE hfile;
3399+
FILE_BASIC_INFO info;
3400+
3401+
hfile = CreateFileW(path->wide,
3402+
FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES,
3403+
0, NULL,
3404+
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
3405+
if (hfile != INVALID_HANDLE_VALUE) {
3406+
if (GetFileInformationByHandleEx(hfile, FileBasicInfo,
3407+
&info, sizeof(info)))
3408+
{
3409+
if (mode & _S_IWRITE) {
3410+
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
3411+
}
3412+
else {
3413+
info.FileAttributes |= FILE_ATTRIBUTE_READONLY;
3414+
}
3415+
result = SetFileInformationByHandle(hfile, FileBasicInfo,
3416+
&info, sizeof(info));
3417+
}
3418+
(void)CloseHandle(hfile);
3419+
}
3420+
}
33793421
else {
3380-
if (mode & _S_IWRITE)
3381-
attr &= ~FILE_ATTRIBUTE_READONLY;
3382-
else
3383-
attr |= FILE_ATTRIBUTE_READONLY;
3384-
result = SetFileAttributesW(path->wide, attr);
3422+
result = win32_lchmod(path->wide, mode);
33853423
}
33863424
Py_END_ALLOW_THREADS
3387-
33883425
if (!result) {
33893426
return path_error(path);
33903427
}
@@ -3514,7 +3551,7 @@ os_fchmod_impl(PyObject *module, int fd, int mode)
35143551
#endif /* HAVE_FCHMOD */
35153552

35163553

3517-
#ifdef HAVE_LCHMOD
3554+
#if defined(HAVE_LCHMOD) || defined(MS_WINDOWS)
35183555
/*[clinic input]
35193556
os.lchmod
35203557
@@ -3535,16 +3572,26 @@ os_lchmod_impl(PyObject *module, path_t *path, int mode)
35353572
if (PySys_Audit("os.chmod", "Oii", path->object, mode, -1) < 0) {
35363573
return NULL;
35373574
}
3575+
#ifdef MS_WINDOWS
3576+
Py_BEGIN_ALLOW_THREADS
3577+
res = win32_lchmod(path->wide, mode);
3578+
Py_END_ALLOW_THREADS
3579+
if (!res) {
3580+
path_error(path);
3581+
return NULL;
3582+
}
3583+
#else /* MS_WINDOWS */
35383584
Py_BEGIN_ALLOW_THREADS
35393585
res = lchmod(path->narrow, mode);
35403586
Py_END_ALLOW_THREADS
35413587
if (res < 0) {
35423588
path_error(path);
35433589
return NULL;
35443590
}
3591+
#endif /* MS_WINDOWS */
35453592
Py_RETURN_NONE;
35463593
}
3547-
#endif /* HAVE_LCHMOD */
3594+
#endif /* HAVE_LCHMOD || MS_WINDOWS */
35483595

35493596

35503597
#ifdef HAVE_CHFLAGS

Tools/clinic/clinic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3737,7 +3737,7 @@ def converter_init(self, *, accept: TypeSet = {object}) -> None:
37373737
self.format_unit = 'i'
37383738
elif accept != {object}:
37393739
fail(f"bool_converter: illegal 'accept' argument {accept!r}")
3740-
if self.default is not unspecified:
3740+
if self.default is not unspecified and self.default is not unknown:
37413741
self.default = bool(self.default)
37423742
self.c_default = str(int(self.default))
37433743

0 commit comments

Comments
 (0)