gh-59616: Support os.chmod(follow_symlinks=True) and os.lchmod() on Windows (GH-113049)

This commit is contained in:
Serhiy Storchaka 2023-12-14 13:28:37 +02:00 committed by GitHub
parent c6e953be12
commit 29f7eb4859
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 93 additions and 28 deletions

View file

@ -2062,6 +2062,7 @@ features:
Although Windows supports :func:`chmod`, you can only set the file's Although Windows supports :func:`chmod`, you can only set the file's
read-only flag with it (via the ``stat.S_IWRITE`` and ``stat.S_IREAD`` read-only flag with it (via the ``stat.S_IWRITE`` and ``stat.S_IREAD``
constants or a corresponding integer value). All other bits are ignored. constants or a corresponding integer value). All other bits are ignored.
The default value of *follow_symlinks* is ``False`` on Windows.
The function is limited on Emscripten and WASI, see The function is limited on Emscripten and WASI, see
:ref:`wasm-availability` for more information. :ref:`wasm-availability` for more information.
@ -2075,6 +2076,9 @@ features:
.. versionchanged:: 3.6 .. versionchanged:: 3.6
Accepts a :term:`path-like object`. Accepts a :term:`path-like object`.
.. versionchanged:: 3.13
Added support for the *follow_symlinks* argument on Windows.
.. function:: chown(path, uid, gid, *, dir_fd=None, follow_symlinks=True) .. function:: chown(path, uid, gid, *, dir_fd=None, follow_symlinks=True)
@ -2165,11 +2169,14 @@ features:
.. audit-event:: os.chmod path,mode,dir_fd os.lchmod .. audit-event:: os.chmod path,mode,dir_fd os.lchmod
.. availability:: Unix, not Linux, FreeBSD >= 1.3, NetBSD >= 1.3, not OpenBSD .. availability:: Unix, Windows, not Linux, FreeBSD >= 1.3, NetBSD >= 1.3, not OpenBSD
.. versionchanged:: 3.6 .. versionchanged:: 3.6
Accepts a :term:`path-like object`. Accepts a :term:`path-like object`.
.. versionchanged:: 3.13
Added support on Windows.
.. function:: lchown(path, uid, gid) .. function:: lchown(path, uid, gid)
Change the owner and group id of *path* to the numeric *uid* and *gid*. This Change the owner and group id of *path* to the numeric *uid* and *gid*. This

View file

@ -261,6 +261,12 @@ os
CPU resources of a container system without having to modify the container (application code). CPU resources of a container system without having to modify the container (application code).
(Contributed by Donghee Na in :gh:`109595`) (Contributed by Donghee Na in :gh:`109595`)
* Add support of :func:`os.lchmod` and the *follow_symlinks* argument
in :func:`os.chmod` on Windows.
Note that the default value of *follow_symlinks* in :func:`!os.lchmod` is
``False`` on Windows.
(Contributed by Serhiy Storchaka in :gh:`59616`)
pathlib pathlib
------- -------

View file

@ -171,6 +171,7 @@ if _exists("_have_functions"):
_add("HAVE_FSTATAT", "stat") _add("HAVE_FSTATAT", "stat")
_add("HAVE_LCHFLAGS", "chflags") _add("HAVE_LCHFLAGS", "chflags")
_add("HAVE_LCHMOD", "chmod") _add("HAVE_LCHMOD", "chmod")
_add("MS_WINDOWS", "chmod")
if _exists("lchown"): # mac os x10.3 if _exists("lchown"): # mac os x10.3
_add("HAVE_LCHOWN", "chown") _add("HAVE_LCHOWN", "chown")
_add("HAVE_LINKAT", "link") _add("HAVE_LINKAT", "link")

View file

@ -273,7 +273,7 @@ def _dont_follow_symlinks(func, path, *args):
# Pass follow_symlinks=False, unless not supported on this platform. # Pass follow_symlinks=False, unless not supported on this platform.
if func in _os.supports_follow_symlinks: if func in _os.supports_follow_symlinks:
func(path, *args, follow_symlinks=False) 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) func(path, *args)
def _resetperms(path): def _resetperms(path):

View file

@ -1019,7 +1019,7 @@ class PosixTester(unittest.TestCase):
self.check_lchmod_link(posix.chmod, target, link) self.check_lchmod_link(posix.chmod, target, link)
else: else:
self.check_chmod_link(posix.chmod, target, link) self.check_chmod_link(posix.chmod, target, link)
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True) self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink
def test_chmod_dir_symlink(self): def test_chmod_dir_symlink(self):
@ -1031,7 +1031,7 @@ class PosixTester(unittest.TestCase):
self.check_lchmod_link(posix.chmod, target, link) self.check_lchmod_link(posix.chmod, target, link)
else: else:
self.check_chmod_link(posix.chmod, target, link) self.check_chmod_link(posix.chmod, target, link)
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True) self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)
@unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()') @unittest.skipUnless(hasattr(posix, 'lchmod'), 'test needs os.lchmod()')
@os_helper.skip_unless_symlink @os_helper.skip_unless_symlink

View file

@ -0,0 +1,3 @@
Add support of :func:`os.lchmod` and the *follow_symlinks* argument in
:func:`os.chmod` on Windows. Note that the default value of *follow_symlinks*
in :func:`!os.lchmod` is ``False`` on Windows.

View file

@ -493,7 +493,8 @@ exit:
#endif /* defined(HAVE_FCHDIR) */ #endif /* defined(HAVE_FCHDIR) */
PyDoc_STRVAR(os_chmod__doc__, PyDoc_STRVAR(os_chmod__doc__,
"chmod($module, /, path, mode, *, dir_fd=None, follow_symlinks=True)\n" "chmod($module, /, path, mode, *, dir_fd=None,\n"
" follow_symlinks=(os.name != \'nt\'))\n"
"--\n" "--\n"
"\n" "\n"
"Change the access permissions of a file.\n" "Change the access permissions of a file.\n"
@ -562,7 +563,7 @@ os_chmod(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kw
path_t path = PATH_T_INITIALIZE("chmod", "path", 0, PATH_HAVE_FCHMOD); path_t path = PATH_T_INITIALIZE("chmod", "path", 0, PATH_HAVE_FCHMOD);
int mode; int mode;
int dir_fd = DEFAULT_DIR_FD; int dir_fd = DEFAULT_DIR_FD;
int follow_symlinks = 1; int follow_symlinks = CHMOD_DEFAULT_FOLLOW_SYMLINKS;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf); args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf);
if (!args) { if (!args) {
@ -677,7 +678,7 @@ exit:
#endif /* defined(HAVE_FCHMOD) */ #endif /* defined(HAVE_FCHMOD) */
#if defined(HAVE_LCHMOD) #if (defined(HAVE_LCHMOD) || defined(MS_WINDOWS))
PyDoc_STRVAR(os_lchmod__doc__, PyDoc_STRVAR(os_lchmod__doc__,
"lchmod($module, /, path, mode)\n" "lchmod($module, /, path, mode)\n"
@ -747,7 +748,7 @@ exit:
return return_value; return return_value;
} }
#endif /* defined(HAVE_LCHMOD) */ #endif /* (defined(HAVE_LCHMOD) || defined(MS_WINDOWS)) */
#if defined(HAVE_CHFLAGS) #if defined(HAVE_CHFLAGS)
@ -12421,4 +12422,4 @@ os__supports_virtual_terminal(PyObject *module, PyObject *Py_UNUSED(ignored))
#ifndef OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #ifndef OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
#define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF #define OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF
#endif /* !defined(OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF) */ #endif /* !defined(OS__SUPPORTS_VIRTUAL_TERMINAL_METHODDEF) */
/*[clinic end generated code: output=ff0ec3371de19904 input=a9049054013a1b77]*/ /*[clinic end generated code: output=1be15e60a553b40d input=a9049054013a1b77]*/

View file

@ -3309,6 +3309,29 @@ os_fchdir_impl(PyObject *module, int fd)
} }
#endif /* HAVE_FCHDIR */ #endif /* HAVE_FCHDIR */
#ifdef MS_WINDOWS
# define CHMOD_DEFAULT_FOLLOW_SYMLINKS 0
#else
# define CHMOD_DEFAULT_FOLLOW_SYMLINKS 1
#endif
#ifdef MS_WINDOWS
static int
win32_lchmod(LPCWSTR path, int mode)
{
DWORD attr = GetFileAttributesW(path);
if (attr == INVALID_FILE_ATTRIBUTES) {
return 0;
}
if (mode & _S_IWRITE) {
attr &= ~FILE_ATTRIBUTE_READONLY;
}
else {
attr |= FILE_ATTRIBUTE_READONLY;
}
return SetFileAttributesW(path, attr);
}
#endif
/*[clinic input] /*[clinic input]
os.chmod os.chmod
@ -3331,7 +3354,8 @@ os.chmod
and path should be relative; path will then be relative to that and path should be relative; path will then be relative to that
directory. directory.
follow_symlinks: bool = True follow_symlinks: bool(c_default="CHMOD_DEFAULT_FOLLOW_SYMLINKS", \
py_default="(os.name != 'nt')") = CHMOD_DEFAULT_FOLLOW_SYMLINKS
If False, and the last element of the path is a symbolic link, If False, and the last element of the path is a symbolic link,
chmod will modify the symbolic link itself instead of the file chmod will modify the symbolic link itself instead of the file
the link points to. the link points to.
@ -3348,20 +3372,16 @@ dir_fd and follow_symlinks may not be implemented on your platform.
static PyObject * static PyObject *
os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd, os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
int follow_symlinks) int follow_symlinks)
/*[clinic end generated code: output=5cf6a94915cc7bff input=674a14bc998de09d]*/ /*[clinic end generated code: output=5cf6a94915cc7bff input=fcf115d174b9f3d8]*/
{ {
int result; int result;
#ifdef MS_WINDOWS
DWORD attr;
#endif
#ifdef HAVE_FCHMODAT #ifdef HAVE_FCHMODAT
int fchmodat_nofollow_unsupported = 0; int fchmodat_nofollow_unsupported = 0;
int fchmodat_unsupported = 0; int fchmodat_unsupported = 0;
#endif #endif
#if !(defined(HAVE_FCHMODAT) || defined(HAVE_LCHMOD)) #if !(defined(HAVE_FCHMODAT) || defined(HAVE_LCHMOD) || defined(MS_WINDOWS))
if (follow_symlinks_specified("chmod", follow_symlinks)) if (follow_symlinks_specified("chmod", follow_symlinks))
return NULL; return NULL;
#endif #endif
@ -3372,19 +3392,36 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
} }
#ifdef MS_WINDOWS #ifdef MS_WINDOWS
result = 0;
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
attr = GetFileAttributesW(path->wide); if (follow_symlinks) {
if (attr == INVALID_FILE_ATTRIBUTES) HANDLE hfile;
result = 0; FILE_BASIC_INFO info;
hfile = CreateFileW(path->wide,
FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES,
0, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hfile != INVALID_HANDLE_VALUE) {
if (GetFileInformationByHandleEx(hfile, FileBasicInfo,
&info, sizeof(info)))
{
if (mode & _S_IWRITE) {
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
}
else {
info.FileAttributes |= FILE_ATTRIBUTE_READONLY;
}
result = SetFileInformationByHandle(hfile, FileBasicInfo,
&info, sizeof(info));
}
(void)CloseHandle(hfile);
}
}
else { else {
if (mode & _S_IWRITE) result = win32_lchmod(path->wide, mode);
attr &= ~FILE_ATTRIBUTE_READONLY;
else
attr |= FILE_ATTRIBUTE_READONLY;
result = SetFileAttributesW(path->wide, attr);
} }
Py_END_ALLOW_THREADS Py_END_ALLOW_THREADS
if (!result) { if (!result) {
return path_error(path); return path_error(path);
} }
@ -3514,7 +3551,7 @@ os_fchmod_impl(PyObject *module, int fd, int mode)
#endif /* HAVE_FCHMOD */ #endif /* HAVE_FCHMOD */
#ifdef HAVE_LCHMOD #if defined(HAVE_LCHMOD) || defined(MS_WINDOWS)
/*[clinic input] /*[clinic input]
os.lchmod os.lchmod
@ -3535,6 +3572,15 @@ os_lchmod_impl(PyObject *module, path_t *path, int mode)
if (PySys_Audit("os.chmod", "Oii", path->object, mode, -1) < 0) { if (PySys_Audit("os.chmod", "Oii", path->object, mode, -1) < 0) {
return NULL; return NULL;
} }
#ifdef MS_WINDOWS
Py_BEGIN_ALLOW_THREADS
res = win32_lchmod(path->wide, mode);
Py_END_ALLOW_THREADS
if (!res) {
path_error(path);
return NULL;
}
#else /* MS_WINDOWS */
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
res = lchmod(path->narrow, mode); res = lchmod(path->narrow, mode);
Py_END_ALLOW_THREADS Py_END_ALLOW_THREADS
@ -3542,9 +3588,10 @@ os_lchmod_impl(PyObject *module, path_t *path, int mode)
path_error(path); path_error(path);
return NULL; return NULL;
} }
#endif /* MS_WINDOWS */
Py_RETURN_NONE; Py_RETURN_NONE;
} }
#endif /* HAVE_LCHMOD */ #endif /* HAVE_LCHMOD || MS_WINDOWS */
#ifdef HAVE_CHFLAGS #ifdef HAVE_CHFLAGS

View file

@ -3737,7 +3737,7 @@ class bool_converter(CConverter):
self.format_unit = 'i' self.format_unit = 'i'
elif accept != {object}: elif accept != {object}:
fail(f"bool_converter: illegal 'accept' argument {accept!r}") fail(f"bool_converter: illegal 'accept' argument {accept!r}")
if self.default is not unspecified: if self.default is not unspecified and self.default is not unknown:
self.default = bool(self.default) self.default = bool(self.default)
self.c_default = str(int(self.default)) self.c_default = str(int(self.default))