diff --git a/CHANGES.md b/CHANGES.md index c40e7404..4e2cd148 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,8 @@ The released versions correspond to PyPI releases. * fixed `os.rename` failing for the root user in a directory owned by another user (see [#1341](https://github.com/pytest-dev/pyfakefs/issues/1341)) +* fixed `os.rename` and `os.replace` succeeding under Posix when renaming + a file to a path with a trailing separator, or from `file/` ## [Version 6.2.0](https://pypi.python.org/pypi/pyfakefs/6.2.0) (2026-04-12) diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 6110aa5d..c0b89fee 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -2005,6 +2005,7 @@ def rename( old_path = make_string_path(old_file_path) new_path = make_string_path(new_file_path) ends_with_sep = self.ends_with_path_separator(old_path) + new_ends_with_sep = self.ends_with_path_separator(new_path) old_path = self.absnormpath(old_path) new_path = self.absnormpath(new_path) if not self.exists(old_path, check_link=True): @@ -2013,6 +2014,10 @@ def rename( self._handle_broken_link_with_trailing_sep(old_path) old_object = self.lresolve(old_path) + if not self.is_windows_fs and not S_ISDIR(old_object.st_mode): + self._handle_posix_non_dir_with_trailing_sep( + old_path, new_path, old_object, ends_with_sep, new_ends_with_sep + ) if not self.is_windows_fs: self._handle_posix_dir_link_errors(new_path, old_path, ends_with_sep) @@ -2066,6 +2071,27 @@ def _do_rename(self, old_dir_object, old_name, new_dir_object, new_name): old_dir_object.add_entry(object_to_rename) raise + def _handle_posix_non_dir_with_trailing_sep( + self, + old_path: AnyStr, + new_path: AnyStr, + old_object: AnyFile, + old_ends_with_sep: bool, + new_ends_with_sep: bool, + ) -> None: + # a trailing separator requires a directory, so renaming a file + # (or a symlink) from or to such a path fails + if old_ends_with_sep and S_ISREG(old_object.st_mode): + self.raise_os_error(errno.ENOTDIR, old_path) + if new_ends_with_sep and self.exists(self.splitpath(new_path)[0]): + error = errno.ENOTDIR + if self.is_macos: + if not self.exists(new_path): + error = errno.ENOENT + elif self.isdir(new_path): + error = errno.EISDIR + self.raise_os_error(error, new_path) + def _handle_broken_link_with_trailing_sep(self, path: AnyStr) -> None: # note that the check for trailing sep has to be done earlier if self.islink(path) and not self.exists(path): diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index 173dc46f..82e6bc56 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -2608,6 +2608,41 @@ def test_rename_to_path_ending_with_sep(self): errno.ENOTDIR, self.os.rename, file_path + self.os.sep, file_path ) + def check_rename_file_to_path_ending_with_sep(self, errors): + file_path = self.make_path("foo") + self.create_file(file_path) + self.create_dir(self.make_path("dir")) + for name, error in zip(("bar", "foo", "dir"), errors): + new_path = self.make_path(name) + self.os.sep + self.assert_raises_os_error(error, self.os.rename, file_path, new_path) + self.assert_raises_os_error(error, self.os.replace, file_path, new_path) + self.assertTrue(self.os.path.isfile(file_path)) + self.assertFalse(self.os.path.exists(self.make_path("bar"))) + + def test_rename_file_to_path_ending_with_sep_linux(self): + self.check_linux_only() + self.check_rename_file_to_path_ending_with_sep( + (errno.ENOTDIR, errno.ENOTDIR, errno.ENOTDIR) + ) + + def test_rename_file_to_path_ending_with_sep_macos(self): + self.check_macos_only() + self.check_rename_file_to_path_ending_with_sep( + (errno.ENOENT, errno.ENOTDIR, errno.EISDIR) + ) + + def test_rename_file_with_trailing_sep_posix(self): + self.check_posix_only() + file_path = self.make_path("foo") + self.create_file(file_path) + self.assert_raises_os_error( + errno.ENOTDIR, + self.os.rename, + file_path + self.os.sep, + self.make_path("bar"), + ) + self.assertTrue(self.os.path.isfile(file_path)) + def test_rmdir_link_with_trailing_sep_linux(self): self.check_linux_only() dir_path = self.make_path("foo")