Skip to content

Commit

Permalink
Allow merging real with fake directories
Browse files Browse the repository at this point in the history
- changed behavior of add_real_directory() to be able to map
  a real directory to an existing directory in the fake filesystem
- fixes #901
  • Loading branch information
mrbean-bremen committed Oct 31, 2023
1 parent cc75310 commit bb5877e
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 45 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Adds official support for Python 3.12.

### Changes
* add official support for Python 3.12
* changed behavior of `add_real_directory` to be able to map a real directory
to an existing directory in the fake filesystem (see #901)

### Fixes
* removed a leftover debug print statement (see [#869](../../issues/869))
Expand Down
4 changes: 3 additions & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,9 @@ files are never changed.

``add_real_file()``, ``add_real_directory()`` and ``add_real_symlink()`` also
allow you to map a file or a directory tree into another location in the
fake filesystem via the argument ``target_path``.
fake filesystem via the argument ``target_path``. If the target directory already exists
in the fake filesystem, the directory contents are merged. If a file in the fake filesystem
would be overwritten by a file from the real filesystem, an exception is raised.

.. code:: python
Expand Down
99 changes: 65 additions & 34 deletions pyfakefs/fake_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -2232,6 +2232,8 @@ def add_real_directory(
:py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` object.
Add entries in the fake directory corresponding to
the entries in the real directory. Symlinks are supported.
If the target directory already exists in the fake filesystem, the directory
contents are merged. Overwriting existing files is not allowed.
Args:
source_path: The path to the existing directory.
Expand All @@ -2254,49 +2256,78 @@ def add_real_directory(
:py:class:`FakeDirectory<pyfakefs.fake_file.FakeDirectory>` object.
Raises:
OSError: if the directory does not exist in the real file system.
OSError: if the directory already exists in the fake file system.
OSError: if the directory does not exist in the real filesystem.
OSError: if a file or link exists in the fake filesystem where a real
file or directory shall be mapped.
"""
source_path_str = make_string_path(source_path) # TODO: add test
source_path_str = make_string_path(source_path)
source_path_str = self._path_without_trailing_separators(source_path_str)
if not os.path.exists(source_path_str):
self.raise_os_error(errno.ENOENT, source_path_str)
target_path_str = make_string_path(target_path or source_path_str)

# get rid of inconsistencies between real and fake path separators
if os.altsep is not None:
target_path_str = os.path.normpath(target_path_str)
if os.sep != self.path_separator:
target_path_str = target_path_str.replace(os.sep, self.path_separator)

self._auto_mount_drive_if_needed(target_path_str)
new_dir: FakeDirectory
if lazy_read:
parent_path = os.path.split(target_path_str)[0]
if self.exists(parent_path):
parent_dir = self.get_object(parent_path)
else:
parent_dir = self.create_dir(parent_path)
new_dir = FakeDirectoryFromRealDirectory(
source_path_str, self, read_only, target_path_str
self._create_fake_from_real_dir_lazily(
source_path_str, target_path_str, read_only
)
parent_dir.add_entry(new_dir)
else:
new_dir = self.create_dir(target_path_str)
for base, _, files in os.walk(source_path_str):
new_base = os.path.join(
new_dir.path, # type: ignore[arg-type]
os.path.relpath(base, source_path_str),
)
for fileEntry in os.listdir(base):
abs_fileEntry = os.path.join(base, fileEntry)

if not os.path.islink(abs_fileEntry):
continue

self.add_real_symlink(
abs_fileEntry, os.path.join(new_base, fileEntry)
)
for fileEntry in files:
path = os.path.join(base, fileEntry)
if os.path.islink(path):
continue
self._create_fake_from_real_dir(source_path_str, target_path_str, read_only)
return cast(FakeDirectory, self.get_object(target_path_str))

def _create_fake_from_real_dir(self, source_path_str, target_path_str, read_only):
if not self.exists(target_path_str):
self.create_dir(target_path_str)
for base, _, files in os.walk(source_path_str):
new_base = os.path.join(
target_path_str,
os.path.relpath(base, source_path_str),
)
for file_entry in os.listdir(base):
file_path = os.path.join(base, file_entry)
if os.path.islink(file_path):
self.add_real_symlink(file_path, os.path.join(new_base, file_entry))
for file_entry in files:
path = os.path.join(base, file_entry)
if not os.path.islink(path):
self.add_real_file(
path, read_only, os.path.join(new_base, fileEntry)
path, read_only, os.path.join(new_base, file_entry)
)

def _create_fake_from_real_dir_lazily(
self, source_path_str, target_path_str, read_only
):
if self.exists(target_path_str):
if not self.isdir(target_path_str):
raise OSError(errno.ENOTDIR, "Mapping target is not a directory")
for entry in os.listdir(source_path_str):
src_entry_path = os.path.join(source_path_str, entry)
target_entry_path = os.path.join(target_path_str, entry)
if os.path.isdir(src_entry_path):
self.add_real_directory(
src_entry_path, read_only, True, target_entry_path
)
elif os.path.islink(src_entry_path):
self.add_real_symlink(src_entry_path, target_entry_path)
elif os.path.isfile(src_entry_path):
self.add_real_file(src_entry_path, read_only, target_entry_path)
return self.get_object(target_path_str)

parent_path = os.path.split(target_path_str)[0]
if self.exists(parent_path):
parent_dir = self.get_object(parent_path)
else:
parent_dir = self.create_dir(parent_path)
new_dir = FakeDirectoryFromRealDirectory(
source_path_str, self, read_only, target_path_str
)
parent_dir.add_entry(new_dir)
return new_dir

def add_real_paths(
Expand All @@ -2322,8 +2353,8 @@ def add_real_paths(
Raises:
OSError: if any of the files and directories in the list
does not exist in the real file system.
OSError: if any of the files and directories in the list
already exists in the fake file system.
OSError: if a file or link exists in the fake filesystem where a real
file or directory shall be mapped.
"""
for path in path_list:
if os.path.isdir(path):
Expand Down
95 changes: 85 additions & 10 deletions pyfakefs/tests/fake_filesystem_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
import contextlib
import errno
import os
import shutil
import stat
import sys
import tempfile
import unittest
from unittest.mock import patch

Expand Down Expand Up @@ -2046,10 +2048,88 @@ def test_existing_fake_file_raises(self):
with self.raises_os_error(errno.EEXIST):
self.filesystem.add_real_file(real_file_path)

def test_existing_fake_directory_raises(self):
self.filesystem.create_dir(self.root_path)
with self.raises_os_error(errno.EEXIST):
self.filesystem.add_real_directory(self.root_path)
@contextlib.contextmanager
def create_real_paths(self):
real_dir_root = os.path.join(tempfile.gettempdir(), "root")
try:
for dir_name in ("foo", "bar"):
real_dir = os.path.join(real_dir_root, dir_name)
os.makedirs(real_dir, exist_ok=True)
with open(os.path.join(real_dir, "test.txt"), "w") as f:
f.write("test")
sub_dir = os.path.join(real_dir, "sub")
os.makedirs(sub_dir, exist_ok=True)
with open(os.path.join(sub_dir, "sub.txt"), "w") as f:
f.write("sub")
yield real_dir_root
finally:
shutil.rmtree(real_dir_root, ignore_errors=True)

def test_existing_fake_directory_is_merged_lazily(self):
self.filesystem.create_file(os.path.join("/", "root", "foo", "test1.txt"))
self.filesystem.create_dir(os.path.join("root", "baz"))
with self.create_real_paths() as root_dir:
self.filesystem.add_real_directory(root_dir, target_path="/root")
self.assertTrue(
self.filesystem.exists(os.path.join("root", "foo", "test.txt"))
)
self.assertTrue(
self.filesystem.exists(os.path.join("root", "foo", "test1.txt"))
)
self.assertTrue(
self.filesystem.exists(os.path.join("root", "bar", "sub", "sub.txt"))
)
self.assertTrue(self.filesystem.exists(os.path.join("root", "baz")))

def test_existing_fake_directory_is_merged(self):
self.filesystem.create_file(os.path.join("/", "root", "foo", "test1.txt"))
self.filesystem.create_dir(os.path.join("root", "baz"))
with self.create_real_paths() as root_dir:
self.filesystem.add_real_directory(
root_dir, target_path="/root", lazy_read=False
)
self.assertTrue(
self.filesystem.exists(os.path.join("root", "foo", "test.txt"))
)
self.assertTrue(
self.filesystem.exists(os.path.join("root", "foo", "test1.txt"))
)
self.assertTrue(
self.filesystem.exists(os.path.join("root", "bar", "sub", "sub.txt"))
)
self.assertTrue(self.filesystem.exists(os.path.join("root", "baz")))

def test_fake_files_cannot_be_overwritten(self):
self.filesystem.create_file(os.path.join("/", "root", "foo", "test.txt"))
with self.create_real_paths() as root_dir:
with self.raises_os_error(errno.EEXIST):
self.filesystem.add_real_directory(root_dir, target_path="/root")

def test_cannot_overwrite_file_with_dir(self):
self.filesystem.create_file(os.path.join("/", "root", "foo"))
with self.create_real_paths() as root_dir:
with self.raises_os_error(errno.ENOTDIR):
self.filesystem.add_real_directory(root_dir, target_path="/root/")

def test_cannot_overwrite_symlink_with_dir(self):
self.filesystem.create_symlink(
os.path.join("/", "root", "foo"), os.path.join("/", "root", "link")
)
with self.create_real_paths() as root_dir:
with self.raises_os_error(errno.EEXIST):
self.filesystem.add_real_directory(root_dir, target_path="/root/")

def test_symlink_is_merged(self):
self.skip_if_symlink_not_supported(force_real_fs=True)
self.filesystem.create_dir(os.path.join("/", "root", "foo"))
with self.create_real_paths() as root_dir:
link_path = os.path.join(root_dir, "link.txt")
target_path = os.path.join("foo", "sub", "sub.txt")
os.symlink(target_path, link_path)
self.filesystem.add_real_directory(root_dir, target_path="/root")
fake_link_path = os.path.join("/", "root", "link.txt")
self.assertTrue(self.filesystem.exists(fake_link_path))
self.assertTrue(self.filesystem.islink(fake_link_path))

def check_fake_file_stat(self, fake_file, real_file_path, target_path=None):
if target_path is None or target_path == real_file_path:
Expand Down Expand Up @@ -2366,11 +2446,6 @@ def test_add_existing_real_directory_symlink_lazy_read(self):
self.filesystem.exists("/path/fixtures/symlink_file_relative")
)

def test_add_existing_real_directory_tree_to_existing_path(self):
self.filesystem.create_dir("/foo/bar")
with self.raises_os_error(errno.EEXIST):
self.filesystem.add_real_directory(self.root_path, target_path="/foo/bar")

def test_add_existing_real_directory_tree_to_other_path(self):
self.filesystem.add_real_directory(self.root_path, target_path="/foo/bar")
self.assertFalse(
Expand Down Expand Up @@ -2420,7 +2495,7 @@ def test_add_existing_real_directory_lazily(self):
self.filesystem.set_disk_usage(disk_size, real_dir_path)
self.filesystem.add_real_directory(real_dir_path)

# the directory contents have not been read, the the disk usage
# the directory contents have not been read, the disk usage
# has not changed
self.assertEqual(disk_size, self.filesystem.get_disk_usage(real_dir_path).free)
# checking for existence shall read the directory contents
Expand Down

0 comments on commit bb5877e

Please sign in to comment.