diff --git a/CHANGES.md b/CHANGES.md index afed03d3..710fadc2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ The released versions correspond to PyPI releases. * fixed creation of the temp directory in the fake file system after a filesystem reset (see [#965](../../issues/965)) * fixed handling of `dirfd` in `os.symlink` (see [#968](../../issues/968)) +* add missing `follow_symlink` argument to `os.link` (see [#973](../../issues/973)) ## [Version 5.3.5](https://pypi.python.org/pypi/pyfakefs/5.3.5) (2024-01-30) Fixes a regression. diff --git a/pyfakefs/fake_os.py b/pyfakefs/fake_os.py index 6b6a487c..1bf1ea46 100644 --- a/pyfakefs/fake_os.py +++ b/pyfakefs/fake_os.py @@ -1185,6 +1185,7 @@ def link( *, src_dir_fd: Optional[int] = None, dst_dir_fd: Optional[int] = None, + follow_symlinks: Optional[bool] = None, ) -> None: """Create a hard link at dst, pointing at src. @@ -1195,14 +1196,21 @@ def link( with `src` being relative to this directory. dst_dir_fd: If not `None`, the file descriptor of a directory, with `dst` being relative to this directory. + follow_symlinks: (bool) If True (the default), symlinks in the + path are traversed. Raises: OSError: if something already exists at new_path. OSError: if the parent directory doesn't exist. """ + if IS_PYPY and follow_symlinks is not None: + raise OSError(errno.EINVAL, "Invalid argument: follow_symlinks") + if follow_symlinks is None: + follow_symlinks = True + src = self._path_with_dir_fd(src, self.link, src_dir_fd) dst = self._path_with_dir_fd(dst, self.link, dst_dir_fd) - self.filesystem.link(src, dst) + self.filesystem.link(src, dst, follow_symlinks=follow_symlinks) def fsync(self, fd: int) -> None: """Perform fsync for a fake file (in other words, do nothing). diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py index 209e51fd..708e82ff 100644 --- a/pyfakefs/tests/fake_os_test.py +++ b/pyfakefs/tests/fake_os_test.py @@ -2705,6 +2705,29 @@ def test_link_count1(self): self.os.unlink(file1_path) self.assertEqual(self.os.stat(file2_path).st_nlink, 1) + @unittest.skipIf(IS_PYPY, "follow_symlinks not supported in PyPi") + def test_link_no_follow_symlink(self): + self.skip_if_symlink_not_supported() + target_path = self.make_path("target_path") + self.create_file(target_path, contents="foo") + symlink_path = self.make_path("symlink_to_file") + self.create_symlink(symlink_path, target_path) + link_path = self.make_path("link_to_symlink") + self.os.link(symlink_path, link_path, follow_symlinks=False) + self.assertTrue(self.os.path.islink(link_path)) + + @unittest.skipIf(not IS_PYPY, "follow_symlinks only not supported in PyPi") + def test_link_follow_symlink_not_supported_inPypy(self): + self.skip_if_symlink_not_supported() + target_path = self.make_path("target_path") + self.create_file(target_path, contents="foo") + symlink_path = self.make_path("symlink_to_file") + self.create_symlink(symlink_path, target_path) + link_path = self.make_path("link_to_symlink") + with self.assertRaises(OSError) as cm: + self.os.link(symlink_path, link_path, follow_symlinks=False) + self.assertEqual(errno.EINVAL, cm.exception.errno) + def test_nlink_for_directories(self): self.skip_real_fs() self.create_dir(self.make_path("foo", "bar"))