diff --git a/CHANGES.md b/CHANGES.md index f50c5aeb..23452efd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,8 +15,9 @@ The released versions correspond to PyPI releases. ## Unreleased ### Enhancements - -- refactor the implementation of `additional_skip_names` parameter to make it work with more modules (see [#1023](../../issues/1023)) +* the `additional_skip_names` parameter now works with more modules (see [#1023](../../issues/1023)) +* added support for `os.fchmod`, allow file descriptor argument for `os.chmod` only for POSIX + for Python < 3.13 ## [Version 5.6.0](https://pypi.python.org/pypi/pyfakefs/5.6.0) (2024-07-12) Adds preliminary Python 3.13 support. diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 04218593..56f0e47e 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -722,7 +722,7 @@ def raise_for_filepath_ending_with_separator( def chmod( self, - path: AnyStr, + path: Union[AnyStr, int], mode: int, follow_symlinks: bool = True, force_unix_mode: bool = False, @@ -730,15 +730,16 @@ def chmod( """Change the permissions of a file as encoded in integer mode. Args: - path: (str) Path to the file. + path: (str | int) Path to the file or file descriptor. mode: (int) Permissions. follow_symlinks: If `False` and `path` points to a symlink, the link itself is affected instead of the linked object. force_unix_mode: if True and run under Windows, the mode is not adapted for Windows to allow making dirs unreadable """ + allow_fd = not self.is_windows_fs or sys.version_info >= (3, 13) file_object = self.resolve( - path, follow_symlinks, allow_fd=True, check_owner=True + path, follow_symlinks, allow_fd=allow_fd, check_owner=True ) if self.is_windows_fs and not force_unix_mode: if mode & helpers.PERM_WRITE: @@ -1723,7 +1724,7 @@ def get_object(self, file_path: AnyPath, check_read_perm: bool = True) -> FakeFi def resolve( self, - file_path: AnyStr, + file_path: Union[AnyStr, int], follow_symlinks: bool = True, allow_fd: bool = False, check_read_perm: bool = True, @@ -1753,7 +1754,9 @@ def resolve( """ if isinstance(file_path, int): if allow_fd: - return self.get_open_file(file_path).get_object() + open_file = self.get_open_file(file_path).get_object() + assert isinstance(open_file, FakeFile) + return open_file raise TypeError("path should be string, bytes or " "os.PathLike, not int") if follow_symlinks: diff --git a/pyfakefs/fake_os.py b/pyfakefs/fake_os.py index c2da5d02..0b4a2cc7 100644 --- a/pyfakefs/fake_os.py +++ b/pyfakefs/fake_os.py @@ -210,7 +210,7 @@ def _umask(self) -> int: return 0o002 else: # under Unix, we return the real umask; - # as there is no pure getter for umask, so we have to first + # there is no pure getter for umask, so we have to first # set a mode to get the previous one and then re-set that mask = os.umask(0) os.umask(mask) @@ -1055,6 +1055,23 @@ def access( mode &= ~os.W_OK return (mode & ((stat_result.st_mode >> 6) & 7)) == mode + def fchmod( + self, + fd: int, + mode: int, + ) -> None: + """Change the permissions of an open file as encoded in integer mode. + + Args: + fd: (int) File descriptor. + mode: (int) Permissions. + """ + if self.filesystem.is_windows_fs and sys.version_info < (3, 13): + raise AttributeError( + "module 'os' has no attribute 'fchmod'. " "Did you mean: 'chmod'?" + ) + self.filesystem.chmod(fd, mode) + def chmod( self, path: AnyStr, diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index 3bd03f97..10c608eb 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -2064,16 +2064,37 @@ def test_chmod(self): self.assertFalse(st.st_mode & stat.S_IFDIR) def test_chmod_uses_open_fd_as_path(self): - self.check_posix_only() + if sys.version_info < (3, 13): + self.check_posix_only() self.skip_real_fs() self.assert_raises_os_error(errno.EBADF, self.os.chmod, 5, 0o6543) path = self.make_path("some_file") self.createTestFile(path) with self.open(path, encoding="utf8") as f: - self.os.chmod(f.filedes, 0o6543) + st = self.os.stat(f.fileno()) + # use a mode that will work under Windows + self.os.chmod(f.filedes, 0o444) st = self.os.stat(path) - self.assert_mode_equal(0o6543, st.st_mode) + self.assert_mode_equal(0o444, st.st_mode) + # fchmod should work the same way + self.os.fchmod(f.filedes, 0o666) + st = self.os.stat(path) + self.assert_mode_equal(0o666, st.st_mode) + + @unittest.skipIf( + sys.version_info >= (3, 13), "also available under Windows since Python 3.13" + ) + def test_chmod_uses_open_fd_as_path_not_available_under_windows(self): + self.check_windows_only() + self.skip_real_fs() + path = self.make_path("some_file") + self.createTestFile(path) + with self.open(path, encoding="utf8") as f: + with self.assertRaises(TypeError): + self.os.chmod(f.fileno(), 0o666) + with self.assertRaises(AttributeError): + self.os.fchmod(f.fileno(), 0o666) def test_chmod_follow_symlink(self): self.check_posix_only()