Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2062,6 +2062,7 @@ features:
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``
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
:ref:`wasm-availability` for more information.
Expand All @@ -2075,6 +2076,9 @@ features:
.. versionchanged:: 3.6
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)

Expand Down Expand Up @@ -2165,11 +2169,14 @@ features:

.. 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
Accepts a :term:`path-like object`.

.. versionchanged:: 3.13
Added support on Windows.

.. function:: lchown(path, uid, gid)

Change the owner and group id of *path* to the numeric *uid* and *gid*. This
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ os
CPU resources of a container system without having to modify the container (application code).
(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
-------

Expand Down
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
4 changes: 2 additions & 2 deletions Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,7 @@ def test_chmod_file_symlink(self):
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)
self.check_chmod_link(posix.chmod, target, link, follow_symlinks=True)

@os_helper.skip_unless_symlink
def test_chmod_dir_symlink(self):
Expand All @@ -1029,7 +1029,7 @@ def test_chmod_dir_symlink(self):
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)
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 6 additions & 5 deletions Modules/clinic/posixmodule.c.h

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

83 changes: 65 additions & 18 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3309,6 +3309,29 @@ os_fchdir_impl(PyObject *module, int fd)
}
#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]
os.chmod
Expand All @@ -3331,7 +3354,8 @@ os.chmod
and path should be relative; path will then be relative to that
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,
chmod will modify the symbolic link itself instead of the file
the link points to.
Expand All @@ -3348,20 +3372,16 @@ dir_fd and follow_symlinks may not be implemented on your platform.
static PyObject *
os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
int follow_symlinks)
/*[clinic end generated code: output=5cf6a94915cc7bff input=674a14bc998de09d]*/
/*[clinic end generated code: output=5cf6a94915cc7bff input=fcf115d174b9f3d8]*/
{
int result;

#ifdef MS_WINDOWS
DWORD attr;
#endif

#ifdef HAVE_FCHMODAT
int fchmodat_nofollow_unsupported = 0;
int fchmodat_unsupported = 0;
#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))
return NULL;
#endif
Expand All @@ -3372,19 +3392,36 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd,
}

#ifdef MS_WINDOWS
result = 0;
Py_BEGIN_ALLOW_THREADS
attr = GetFileAttributesW(path->wide);
if (attr == INVALID_FILE_ATTRIBUTES)
result = 0;
if (follow_symlinks) {
HANDLE hfile;
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 {
if (mode & _S_IWRITE)
attr &= ~FILE_ATTRIBUTE_READONLY;
else
attr |= FILE_ATTRIBUTE_READONLY;
result = SetFileAttributesW(path->wide, attr);
result = win32_lchmod(path->wide, mode);
}
Py_END_ALLOW_THREADS

if (!result) {
return path_error(path);
}
Expand Down Expand Up @@ -3514,7 +3551,7 @@ os_fchmod_impl(PyObject *module, int fd, int mode)
#endif /* HAVE_FCHMOD */


#ifdef HAVE_LCHMOD
#if defined(HAVE_LCHMOD) || defined(MS_WINDOWS)
/*[clinic input]
os.lchmod

Expand All @@ -3535,16 +3572,26 @@ os_lchmod_impl(PyObject *module, path_t *path, int mode)
if (PySys_Audit("os.chmod", "Oii", path->object, mode, -1) < 0) {
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
res = lchmod(path->narrow, mode);
Py_END_ALLOW_THREADS
if (res < 0) {
path_error(path);
return NULL;
}
#endif /* MS_WINDOWS */
Py_RETURN_NONE;
}
#endif /* HAVE_LCHMOD */
#endif /* HAVE_LCHMOD || MS_WINDOWS */


#ifdef HAVE_CHFLAGS
Expand Down
2 changes: 1 addition & 1 deletion Tools/clinic/clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3737,7 +3737,7 @@ def converter_init(self, *, accept: TypeSet = {object}) -> None:
self.format_unit = 'i'
elif accept != {object}:
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.c_default = str(int(self.default))

Expand Down