diff --git a/CHANGES.md b/CHANGES.md index 12955ed6..f9672323 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,8 @@ The released versions correspond to PyPI releases. (see [#959](../../issues/959)) * fixed handling of directory enumeration and search permissions under Posix systems (see [#960](../../issues/960)) +* fixed creation of the temp directory in the fake file system after a filesystem reset + (see [#965](../../issues/965)) ## [Version 5.3.5](https://pypi.python.org/pypi/pyfakefs/5.3.5) (2024-01-30) Fixes a regression. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 56572aaf..fd75a987 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -179,8 +179,7 @@ As ``pyfakefs`` does not fake the ``tempfile`` module (as described above), a temporary directory is required to ensure that ``tempfile`` works correctly, e.g., that ``tempfile.gettempdir()`` will return a valid value. This means that any newly created fake file system will always have either a -directory named ``/tmp`` when running on Linux or Unix systems, -``/var/folders//T`` when running on macOS, or +directory named ``/tmp`` when running on POSIX systems, or ``C:\Users\\AppData\Local\Temp`` on Windows: .. code:: python @@ -192,11 +191,13 @@ directory named ``/tmp`` when running on Linux or Unix systems, # the temp directory is always present at test start assert len(os.listdir("/")) == 1 -Under macOS and linux, if the actual temp path is not `/tmp` (which is always the case -under macOS), a symlink to the actual temp directory is additionally created as `/tmp` -in the fake filesystem. Note that the file size of this link is ignored while +Under macOS and linux, if the actual temp path is not `/tmp` (which will be the case if an environment variable +`TEMPDIR`, `TEMP` or `TMP` points to another path), a symlink to the actual temp directory is additionally created +as `/tmp` in the fake filesystem. Note that the file size of this link is ignored while calculating the fake filesystem size, so that the used size with an otherwise empty fake filesystem can always be assumed to be 0. +Note also that the temp directory may not be what you expect, if you emulate another file system. For example, +if you emulate Windows under Linux, the default temp directory will be at `C:\\tmp`. User rights diff --git a/pyfakefs/fake_file.py b/pyfakefs/fake_file.py index b4707ab2..22b215a7 100644 --- a/pyfakefs/fake_file.py +++ b/pyfakefs/fake_file.py @@ -18,6 +18,7 @@ import io import os import sys +import traceback from stat import ( S_IFREG, S_IFDIR, @@ -585,7 +586,24 @@ def remove_entry(self, pathname_name: str, recursive: bool = True) -> None: if entry.st_mode & helpers.PERM_WRITE == 0: self.filesystem.raise_os_error(errno.EACCES, pathname_name) if self.filesystem.has_open_file(entry): - self.filesystem.raise_os_error(errno.EACCES, pathname_name) + raise_error = True + if os.name == "posix" and not hasattr(os, "O_TMPFILE"): + # special handling for emulating Windows under macOS and PyPi + # tempfile uses unlink based on the real OS while deleting + # a temporary file, so we ignore that error in this specific case + st = traceback.extract_stack(limit=6) + if sys.version_info < (3, 10): + if ( + st[0].name == "TemporaryFile" + and st[0].line == "_os.unlink(name)" + ): + raise_error = False + else: + # TemporaryFile implementation has changed in Python 3.10 + if st[1].name == "opener" and st[1].line == "_os.unlink(name)": + raise_error = False + if raise_error: + self.filesystem.raise_os_error(errno.EACCES, pathname_name) else: if not helpers.is_root() and not self.has_permission( helpers.PERM_WRITE | helpers.PERM_EXE diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py index 1d71f2b4..b0077d5f 100644 --- a/pyfakefs/fake_filesystem.py +++ b/pyfakefs/fake_filesystem.py @@ -3013,10 +3013,27 @@ def _add_standard_streams(self) -> None: self._add_open_file(StandardStreamWrapper(sys.stdout)) self._add_open_file(StandardStreamWrapper(sys.stderr)) + def _tempdir_name(self): + """This logic is extracted from tempdir._candidate_tempdir_list. + We cannot rely on tempdir.gettempdir() in an empty filesystem, as it tries + to write to the filesystem to ensure that the tempdir is valid. + """ + # reset the cached tempdir in tempfile + tempfile.tempdir = None + for env_name in "TMPDIR", "TEMP", "TMP": + dir_name = os.getenv(env_name) + if dir_name: + return dir_name + # we have to check the real OS temp path here, as this is what + # tempfile assumes + if os.name == "nt": + return os.path.expanduser(r"~\AppData\Local\Temp") + return "/tmp" + def _create_temp_dir(self): # the temp directory is assumed to exist at least in `tempfile`, # so we create it here for convenience - temp_dir = tempfile.gettempdir() + temp_dir = self._tempdir_name() if not self.exists(temp_dir): self.create_dir(temp_dir) if sys.platform != "win32" and not self.exists("/tmp"): diff --git a/pyfakefs/tests/fake_filesystem_unittest_test.py b/pyfakefs/tests/fake_filesystem_unittest_test.py index c3a0a6df..3f5375f7 100644 --- a/pyfakefs/tests/fake_filesystem_unittest_test.py +++ b/pyfakefs/tests/fake_filesystem_unittest_test.py @@ -925,5 +925,31 @@ def test_using_fakefs(self): self.assertEqual("test", f.contents) +class TestTempPathCreation(fake_filesystem_unittest.TestCase): + """Regression test for #965. Checks that the temp file system + is properly created with a root-owned root path. + """ + + def setUp(self): + self.setUpPyfakefs() + + def check_write_tmp_after_reset(self, os_type): + self.fs.os = os_type + # Mark '/' to be modifiable by only root + os.chown("/", 0, 0) + os.chmod("/", 0b111_101_101) + with tempfile.TemporaryFile("wb") as f: + assert f.write(b"foo") == 3 + + def test_write_tmp_linux(self): + self.check_write_tmp_after_reset(OSType.LINUX) + + def test_write_tmp_macos(self): + self.check_write_tmp_after_reset(OSType.MACOS) + + def test_write_tmp_windows(self): + self.check_write_tmp_after_reset(OSType.WINDOWS) + + if __name__ == "__main__": unittest.main()