Skip to content

Commit

Permalink
Added argument 'target_path' to add_real_file() and add_real_directory()
Browse files Browse the repository at this point in the history
- allows to map real file system files and directories
  to another location in the fake file system
- see pytest-dev#347
  • Loading branch information
mrbean-bremen committed Feb 8, 2018
1 parent dac3479 commit 04fd290
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 54 deletions.
4 changes: 4 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ cheap. The access to the files is by default read-only, but even even if you
add them using ``read_only=False``, the files are written only in the fake
system (e.g. in memory). The real files are never changed.

``add_real_file()`` and ``add_real_directory()`` also allow you to map a
file or a directory tree into another location in the fake filesystem via the
argument ``target_path``.

.. code:: python
from fake_filesystem_unittest import TestCase
Expand Down
118 changes: 68 additions & 50 deletions pyfakefs/fake_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,27 +476,19 @@ class FakeFileFromRealFile(FakeFile):
The contents of the file are read on demand only.
"""

def __init__(self, file_path, filesystem, read_only=True):
def __init__(self, file_path, filesystem):
"""init.
Args:
file_path: path to the existing file.
filesystem: the fake filesystem where the file is created.
read_only: if set, the file is treated as read-only, e.g.
a write access raises an exception; otherwise, writing
to the file changes the fake file only as usually.
file_path: Path to the existing file.
filesystem: The fake filesystem where the file is created.
Raises:
OSError: if the file does not exist in the real file system.
OSError: if the file already exists in the fake file system.
"""
real_stat = os.stat(file_path)
# for read-only mode, remove the write/executable permission bits
super(FakeFileFromRealFile, self).__init__(name=os.path.basename(file_path),
filesystem=filesystem)
self.stat_result.set_from_stat_result(real_stat)
if read_only:
self.st_mode &= 0o777444
self.file_path = file_path
super(FakeFileFromRealFile, self).__init__(
name=os.path.basename(file_path), filesystem=filesystem)
self.contents_read = False

@property
Expand Down Expand Up @@ -675,23 +667,27 @@ class FakeDirectoryFromRealDirectory(FakeDirectory):
The contents of the directory are read on demand only.
"""

def __init__(self, dir_path, filesystem, read_only):
def __init__(self, source_path, filesystem, read_only,
target_path=None):
"""init.
Args:
dir_path: Full directory path.
source_path: Full directory path.
filesystem: The fake filesystem where the directory is created.
read_only: If set, all files under the directory are treated
as read-only, e.g. a write access raises an exception;
otherwise, writing to the files changes the fake files
only as usually.
target_path: If given, the target path of the directory,
otherwise the target is the samae as `source_path`.
Raises:
OSError: if the directory does not exist in the real file system
"""
real_stat = os.stat(dir_path)
target_path = target_path or source_path
real_stat = os.stat(source_path)
super(FakeDirectoryFromRealDirectory, self).__init__(
name=os.path.split(dir_path)[1],
name=os.path.split(target_path)[1],
perm_bits=real_stat.st_mode,
filesystem=filesystem)

Expand All @@ -700,7 +696,7 @@ def __init__(self, dir_path, filesystem, read_only):
self.st_mtime = real_stat.st_mtime
self.st_gid = real_stat.st_gid
self.st_uid = real_stat.st_uid
self.dir_path = dir_path
self.source_path = source_path
self.read_only = read_only
self.contents_read = False

Expand All @@ -710,10 +706,16 @@ def contents(self):
if not already loaded."""
if not self.contents_read:
self.contents_read = True
self.filesystem.add_real_paths(
[os.path.join(self.dir_path, entry) for entry in
os.listdir(self.dir_path)],
read_only=self.read_only)
base = self.path
for entry in os.listdir(self.source_path):
source_path = os.path.join(self.source_path, entry)
target_path = os.path.join(base, entry)
if os.path.isdir(source_path):
self.filesystem.add_real_directory(
source_path, self.read_only, target_path=target_path)
else:
self.filesystem.add_real_file(
source_path, self.read_only, target_path=target_path)
return self.byte_contents

@property
Expand Down Expand Up @@ -2105,16 +2107,18 @@ def create_file(self, file_path, st_mode=S_IFREG | PERM_DEF_FILE,
file_path, st_mode, contents, st_size, create_missing_dirs,
apply_umask, encoding, errors)

def add_real_file(self, file_path, read_only=True):
def add_real_file(self, source_path, read_only=True, target_path=None):
"""Create file_path, including all the parent directories along the
way, for an existing real file. The contents of the real file are read
only on demand.
Args:
file_path: Path to an existing file in the real file system
source_path: Path to an existing file in the real file system
read_only: If `True` (the default), writing to the fake file
raises an exception. Otherwise, writing to the file changes
the fake file only.
target_path: If given, the path of the target direction,
otherwise it is equal to `source_path`.
Returns:
the newly created FakeFile object.
Expand All @@ -2131,17 +2135,28 @@ def add_real_file(self, file_path, read_only=True):
Further, Windows offers the option to enable atime, and older \
versions of Linux may also modify atime.
"""
return self.create_file_internally(file_path,
read_from_real_fs=True,
read_only=read_only)
target_path = target_path or source_path
real_stat = os.stat(source_path)
fake_file = self.create_file_internally(target_path,
read_from_real_fs=True)

def add_real_directory(self, dir_path, read_only=True, lazy_read=True):
"""Create a fake directory corresponding to the real directory at the specified
path. Add entries in the fake directory corresponding to the entries in the
real directory.
# for read-only mode, remove the write/executable permission bits
fake_file.stat_result.set_from_stat_result(real_stat)
if read_only:
fake_file.st_mode &= 0o777444
fake_file.file_path = source_path
self.change_disk_usage(fake_file.size, fake_file.name,
fake_file.st_dev)
return fake_file

def add_real_directory(self, source_path, read_only=True, lazy_read=True,
target_path=None):
"""Create a fake directory corresponding to the real directory at the
specified path. Add entries in the fake directory corresponding to
the entries in the real directory.
Args:
dir_path: The path to the existing directory.
source_path: The path to the existing directory.
read_only: If set, all files under the directory are treated as
read-only, e.g. a write access raises an exception;
otherwise, writing to the files changes the fake files only
Expand All @@ -2152,6 +2167,8 @@ def add_real_directory(self, dir_path, read_only=True, lazy_read=True):
at the time the directory contents are read; set this to
`False` only if you are dependent on accurate file system
size in your test
target_path: If given, the target directory, otherwise,
the target directory is the same as `source_path`.
Returns:
the newly created FakeDirectory object.
Expand All @@ -2160,24 +2177,28 @@ def add_real_directory(self, dir_path, read_only=True, lazy_read=True):
OSError: if the directory does not exist in the real file system.
IOError: if the directory already exists in the fake file system.
"""
if not os.path.exists(dir_path):
self.raise_io_error(errno.ENOENT, dir_path)
if not os.path.exists(source_path):
self.raise_io_error(errno.ENOENT, source_path)
target_path = target_path or source_path
if lazy_read:
parent_path = os.path.split(dir_path)[0]
parent_path = os.path.split(target_path)[0]
if self.exists(parent_path):
parent_dir = self.get_object(parent_path)
else:
parent_dir = self.create_dir(parent_path)
new_dir = FakeDirectoryFromRealDirectory(dir_path, filesystem=self,
read_only=read_only)
new_dir = FakeDirectoryFromRealDirectory(
source_path, self, read_only, target_path)
parent_dir.add_entry(new_dir)
self._last_ino += 1
new_dir.st_ino = self._last_ino
else:
new_dir = self.create_dir(dir_path)
for base, _, files in os.walk(dir_path):
new_dir = self.create_dir(target_path)
new_base = new_dir.path
for base, _, files in os.walk(source_path):
for fileEntry in files:
self.add_real_file(os.path.join(base, fileEntry), read_only)
self.add_real_file(os.path.join(base, fileEntry),
read_only,
os.path.join(new_base, fileEntry))
return new_dir

def add_real_paths(self, path_list, read_only=True, lazy_dir_read=True):
Expand Down Expand Up @@ -2208,7 +2229,7 @@ def add_real_paths(self, path_list, read_only=True, lazy_dir_read=True):
def create_file_internally(self, file_path, st_mode=S_IFREG | PERM_DEF_FILE,
contents='', st_size=None, create_missing_dirs=True,
apply_umask=False, encoding=None, errors=None,
read_from_real_fs=False, read_only=True, raw_io=False):
read_from_real_fs=False, raw_io=False):
"""Internal fake file creator that supports both normal fake files and fake
files based on real files.
Expand All @@ -2218,15 +2239,13 @@ def create_file_internally(self, file_path, st_mode=S_IFREG | PERM_DEF_FILE,
contents: the contents of the file.
st_size: file size; only valid if contents not given.
create_missing_dirs: if True, auto create missing directories.
apply_umask: whether or not the current umask must be applied on st_mode.
apply_umask: whether or not the current umask must be applied
on st_mode.
encoding: if contents is a unicode string, the encoding used for
serialization.
errors: the error mode used for encoding/decoding errors
read_from_real_fs: if True, the contents are reaf from the real file system
on demand.
read_only: if set, the file is treated as read-only, e.g. a write access
raises an exception;
otherwise, writing to the file changes the fake file only as usually.
read_from_real_fs: if True, the contents are read from the real
file system on demand.
raw_io: `True` if called from low-level API (`os.open`)
"""
error_class = OSError if raw_io else IOError
Expand All @@ -2251,8 +2270,7 @@ def create_file_internally(self, file_path, st_mode=S_IFREG | PERM_DEF_FILE,
if apply_umask:
st_mode &= ~self.umask
if read_from_real_fs:
file_object = FakeFileFromRealFile(
file_path, filesystem=self, read_only=read_only)
file_object = FakeFileFromRealFile(file_path, filesystem=self)
else:
file_object = FakeFile(new_file, st_mode, filesystem=self, encoding=encoding,
errors=errors)
Expand Down
52 changes: 48 additions & 4 deletions tests/fake_filesystem_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1790,8 +1790,14 @@ def test_existing_fake_directory_raises(self):
self.filesystem.add_real_directory,
self.root_path)

def check_fake_file_stat(self, fake_file, real_file_path):
self.assertTrue(self.filesystem.exists(real_file_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:
self.assertTrue(self.filesystem.exists(real_file_path))
else:
self.assertFalse(self.filesystem.exists(real_file_path))
self.assertTrue(self.filesystem.exists(target_path))

real_stat = os.stat(real_file_path)
self.assertIsNone(fake_file._byte_contents)
self.assertEqual(fake_file.st_size, real_stat.st_size)
Expand All @@ -1808,8 +1814,8 @@ def check_read_only_file(self, fake_file, real_file_path):
with open(real_file_path, 'rb') as f:
real_contents = f.read()
self.assertEqual(fake_file.byte_contents, real_contents)
self.assert_raises_io_error(errno.EACCES, self.fake_open, real_file_path,
'w')
self.assert_raises_io_error(
errno.EACCES, self.fake_open, real_file_path, 'w')

def check_writable_file(self, fake_file, real_file_path):
with open(real_file_path, 'rb') as f:
Expand Down Expand Up @@ -1840,6 +1846,20 @@ def test_add_existing_real_file_read_write(self):
self.assertEqual(fake_file.st_mode, os.stat(real_file_path).st_mode)
self.check_writable_file(fake_file, real_file_path)

def test_add_real_file_to_existing_path(self):
real_file_path = os.path.abspath(__file__)
self.filesystem.create_file('/foo/bar')
self.assert_raises_os_error(
errno.EEXIST, self.filesystem.add_real_file,
real_file_path, target_path='/foo/bar')

def test_add_real_file_to_non_existing_path(self):
real_file_path = os.path.abspath(__file__)
fake_file = self.filesystem.add_real_file(real_file_path,
target_path='/foo/bar')
self.check_fake_file_stat(fake_file, real_file_path,
target_path='/foo/bar')

def test_add_existing_real_directory_read_only(self):
real_dir_path = os.path.join(self.root_path, 'pyfakefs')
self.filesystem.add_real_directory(real_dir_path)
Expand All @@ -1866,6 +1886,30 @@ def test_add_existing_real_directory_tree(self):
self.filesystem.exists(
os.path.join(self.root_path, 'pyfakefs', '__init__.py')))

def test_add_existing_real_directory_tree_to_existing_path(self):
self.filesystem.create_dir('/foo/bar')
self.assert_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(
self.filesystem.exists(
os.path.join(self.root_path, 'tests', 'fake_filesystem_test.py')))
self.assertTrue(
self.filesystem.exists(
os.path.join('foo', 'bar', 'tests',
'fake_filesystem_test.py')))
self.assertFalse(
self.filesystem.exists(
os.path.join(self.root_path, 'pyfakefs', 'fake_filesystem.py')))
self.assertTrue(
self.filesystem.exists(
os.path.join('foo', 'bar', 'pyfakefs', '__init__.py')))

def test_get_object_from_lazily_added_real_directory(self):
self.filesystem.is_case_sensitive = True
self.filesystem.add_real_directory(self.root_path)
Expand Down

0 comments on commit 04fd290

Please sign in to comment.