Skip to content

Commit de0d107

Browse files
mrbean-bremenjmcgeheeiv
authored andcommitted
Added lazy evaluation of real directories (#185)
* Added lazy evaluation of real directories - real directory contents are only added to the fake file system on demand (makes adding large directory trees faster) - refactored real file/directory access to use own classes * Added possibility to switch off lazy directory reading - may be needed for tests that check the disk usage to avoid the side effect of changing disk usage during delayed directory content access
1 parent bbcdb68 commit de0d107

File tree

2 files changed

+167
-36
lines changed

2 files changed

+167
-36
lines changed

fake_filesystem_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4623,6 +4623,35 @@ def testAddExistingRealDirectoryTree(self):
46234623
self.assertTrue(self.filesystem.Exists(os.path.join(real_dir_path, 'pyfakefs', 'fake_filesystem.py')))
46244624
self.assertTrue(self.filesystem.Exists(os.path.join(real_dir_path, 'pyfakefs', '__init__.py')))
46254625

4626+
def testGetObjectFromLazilyAddedRealDirectory(self):
4627+
self.filesystem.is_case_sensitive = True
4628+
real_dir_path = os.path.dirname(__file__)
4629+
self.filesystem.add_real_directory(real_dir_path)
4630+
self.assertTrue(self.filesystem.GetObject(os.path.join(real_dir_path, 'pyfakefs', 'fake_filesystem.py')))
4631+
self.assertTrue(self.filesystem.GetObject(os.path.join(real_dir_path, 'pyfakefs', '__init__.py')))
4632+
4633+
def testAddExistingRealDirectoryLazily(self):
4634+
disk_size = 1024*1024*1024
4635+
real_dir_path = os.path.join(os.path.dirname(__file__), 'pyfakefs')
4636+
self.filesystem.SetDiskUsage(disk_size, real_dir_path)
4637+
self.filesystem.add_real_directory(real_dir_path)
4638+
4639+
# the directory contents have not been read, the the disk usage has not changed
4640+
self.assertEqual(disk_size, self.filesystem.GetDiskUsage(real_dir_path).free)
4641+
# checking for existence shall read the directory contents
4642+
self.assertTrue(self.filesystem.GetObject(os.path.join(real_dir_path, 'fake_filesystem.py')))
4643+
# so now the free disk space shall have decreased
4644+
self.assertGreater(disk_size, self.filesystem.GetDiskUsage(real_dir_path).free)
4645+
4646+
def testAddExistingRealDirectoryNotLazily(self):
4647+
disk_size = 1024*1024*1024
4648+
real_dir_path = os.path.join(os.path.dirname(__file__), 'pyfakefs')
4649+
self.filesystem.SetDiskUsage(disk_size, real_dir_path)
4650+
self.filesystem.add_real_directory(real_dir_path, lazy_read=False)
4651+
4652+
# the directory has been read, so the file sizes have been subtracted from the free space
4653+
self.assertGreater(disk_size, self.filesystem.GetDiskUsage(real_dir_path).free)
4654+
46264655
def testAddExistingRealDirectoryReadWrite(self):
46274656
real_dir_path = os.path.join(os.path.dirname(__file__), 'pyfakefs')
46284657
self.filesystem.add_real_directory(real_dir_path, read_only=False)

pyfakefs/fake_filesystem.py

Lines changed: 138 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,8 @@ def __init__(self, name, st_mode=stat.S_IFREG | PERM_DEF_FILE,
214214
self.st_uid = None
215215
self.st_gid = None
216216

217-
# members changed only by _CreateFile() to implement add_real_file()
218-
self.read_from_real_fs = False
219-
self.file_path = None
220-
221217
@property
222218
def byte_contents(self):
223-
if self._byte_contents is None and self.read_from_real_fs:
224-
with io.open(self.file_path, 'rb') as f:
225-
self._byte_contents = f.read()
226219
return self._byte_contents
227220

228221
@property
@@ -297,7 +290,7 @@ def SetLargeFileSize(self, st_size):
297290

298291
def IsLargeFile(self):
299292
"""Return True if this file was initialized with size but no contents."""
300-
return self._byte_contents is None and not self.read_from_real_fs
293+
return self._byte_contents is None
301294

302295
def _encode_contents(self, contents):
303296
# pylint: disable=undefined-variable
@@ -425,6 +418,53 @@ def SetIno(self, st_ino):
425418
self.st_ino = st_ino
426419

427420

421+
class FakeFileFromRealFile(FakeFile):
422+
"""Represents a fake file copied from the real file system.
423+
424+
The contents of the file are read on demand only.
425+
New in pyfakefs 3.2.
426+
"""
427+
428+
def __init__(self, file_path, filesystem, read_only=True):
429+
"""init.
430+
431+
Args:
432+
file_path: path to the existing file.
433+
filesystem: the fake filesystem where the file is created.
434+
read_only: if set, the file is treated as read-only, e.g. a write access raises an exception;
435+
otherwise, writing to the file changes the fake file only as usually.
436+
437+
Raises:
438+
OSError: if the file does not exist in the real file system.
439+
"""
440+
real_stat = os.stat(file_path)
441+
# for read-only mode, remove the write/executable permission bits
442+
mode = real_stat.st_mode & 0o777444 if read_only else real_stat.st_mode
443+
super(FakeFileFromRealFile, self).__init__(name=os.path.basename(file_path),
444+
st_mode=mode,
445+
filesystem=filesystem)
446+
self.st_ctime = real_stat.st_ctime
447+
self.st_atime = real_stat.st_atime
448+
self.st_mtime = real_stat.st_mtime
449+
self.st_gid = real_stat.st_gid
450+
self.st_uid = real_stat.st_uid
451+
self.st_size = real_stat.st_size
452+
self.file_path = file_path
453+
self.contents_read = False
454+
455+
@property
456+
def byte_contents(self):
457+
if not self.contents_read:
458+
self.contents_read = True
459+
with io.open(self.file_path, 'rb') as f:
460+
self._byte_contents = f.read()
461+
return self._byte_contents
462+
463+
def IsLargeFile(self):
464+
"""The contents are never faked."""
465+
return False
466+
467+
428468
class FakeDirectory(FakeFile):
429469
"""Provides the appearance of a real directory."""
430470

@@ -520,6 +560,58 @@ def __str__(self):
520560
return description
521561

522562

563+
class FakeDirectoryFromRealDirectory(FakeDirectory):
564+
"""Represents a fake directory copied from the real file system.
565+
566+
The contents of the directory are read on demand only.
567+
New in pyfakefs 3.2.
568+
"""
569+
570+
def __init__(self, dir_path, filesystem, read_only):
571+
"""init.
572+
573+
Args:
574+
dir_path: full directory path
575+
filesystem: the fake filesystem where the directory is created
576+
read_only: if set, all files under the directory are treated as read-only,
577+
e.g. a write access raises an exception;
578+
otherwise, writing to the files changes the fake files only as usually.
579+
580+
Raises:
581+
OSError if the directory does not exist in the real file system
582+
"""
583+
real_stat = os.stat(dir_path)
584+
super(FakeDirectoryFromRealDirectory, self).__init__(
585+
name=os.path.split(dir_path)[1],
586+
perm_bits=real_stat.st_mode,
587+
filesystem=filesystem)
588+
589+
self.st_ctime = real_stat.st_ctime
590+
self.st_atime = real_stat.st_atime
591+
self.st_mtime = real_stat.st_mtime
592+
self.st_gid = real_stat.st_gid
593+
self.st_uid = real_stat.st_uid
594+
self.dir_path = dir_path
595+
self.read_only = read_only
596+
self.contents_read = False
597+
598+
@property
599+
def contents(self):
600+
"""Return the list of contained directory entries, loading them if not already loaded."""
601+
if not self.contents_read:
602+
self.contents_read = True
603+
self.filesystem.add_real_paths(
604+
[os.path.join(self.dir_path, entry) for entry in os.listdir(self.dir_path)],
605+
read_only=self.read_only)
606+
return self.byte_contents
607+
608+
def GetSize(self):
609+
# we cannot get the size until the contents are loaded
610+
if not self.contents_read:
611+
return 0
612+
return super(FakeDirectoryFromRealDirectory, self).GetSize()
613+
614+
523615
class FakeFilesystem(object):
524616
"""Provides the appearance of a real directory tree for unit testing."""
525617

@@ -1167,6 +1259,7 @@ def _DirectoryContent(self, directory, component):
11671259
if subdir.lower() == component.lower()]
11681260
if matching_content:
11691261
return matching_content[0]
1262+
11701263
return None, None
11711264

11721265
def Exists(self, file_path):
@@ -1642,13 +1735,11 @@ def add_real_file(self, file_path, read_only=True):
16421735
OSError: if the file does not exist in the real file system.
16431736
IOError: if the file already exists in the fake file system.
16441737
"""
1645-
real_stat = os.stat(file_path)
1646-
# for read-only mode, remove the write/executable permission bits
1647-
mode = real_stat.st_mode & 0o777444 if read_only else real_stat.st_mode
1648-
return self._CreateFile(file_path, contents=None, read_from_real_fs=True,
1649-
st_mode=mode, real_stat=real_stat)
1738+
return self._CreateFile(file_path,
1739+
read_from_real_fs=True,
1740+
read_only=read_only)
16501741

1651-
def add_real_directory(self, dir_path, read_only=True):
1742+
def add_real_directory(self, dir_path, read_only=True, lazy_read=True):
16521743
"""Create fake directory for the existing directory at path, and entries for all contained
16531744
files in the real file system.
16541745
New in pyfakefs 3.2.
@@ -1658,6 +1749,11 @@ def add_real_directory(self, dir_path, read_only=True):
16581749
read_only: if set, all files under the directory are treated as read-only,
16591750
e.g. a write access raises an exception;
16601751
otherwise, writing to the files changes the fake files only as usually.
1752+
lazy_read: if set (default), directory contents are only read when accessed,
1753+
and only until the needed subdirectory level
1754+
Note: this means that the file system size is only updated at the time
1755+
the directory contents are read; set this to False only if you
1756+
are dependent on accurate file system size in your test
16611757
16621758
Returns:
16631759
the newly created FakeDirectory object.
@@ -1668,12 +1764,24 @@ def add_real_directory(self, dir_path, read_only=True):
16681764
"""
16691765
if not os.path.exists(dir_path):
16701766
raise IOError(errno.ENOENT, 'No such directory', dir_path)
1671-
self.CreateDirectory(dir_path)
1672-
for base, _, files in os.walk(dir_path):
1673-
for fileEntry in files:
1674-
self.add_real_file(os.path.join(base, fileEntry), read_only)
1767+
if lazy_read:
1768+
parent_path = os.path.split(dir_path)[0]
1769+
if self.Exists(parent_path):
1770+
parent_dir = self.GetObject(parent_path)
1771+
else:
1772+
parent_dir = self.CreateDirectory(parent_path)
1773+
new_dir = FakeDirectoryFromRealDirectory(dir_path, filesystem=self, read_only=read_only)
1774+
parent_dir.AddEntry(new_dir)
1775+
self.last_ino += 1
1776+
new_dir.SetIno(self.last_ino)
1777+
else:
1778+
new_dir = self.CreateDirectory(dir_path)
1779+
for base, _, files in os.walk(dir_path):
1780+
for fileEntry in files:
1781+
self.add_real_file(os.path.join(base, fileEntry), read_only)
1782+
return new_dir
16751783

1676-
def add_real_paths(self, path_list, read_only=True):
1784+
def add_real_paths(self, path_list, read_only=True, lazy_dir_read=True):
16771785
"""Convenience method to add several files and directories from the real file system
16781786
in the fake file system. See `add_real_file()` and `add_real_directory()`.
16791787
New in pyfakefs 3.2.
@@ -1683,22 +1791,24 @@ def add_real_paths(self, path_list, read_only=True):
16831791
read_only: if set, all files and files under under the directories are treated as read-only,
16841792
e.g. a write access raises an exception;
16851793
otherwise, writing to the files changes the fake files only as usually.
1794+
lazy_dir_read: uses lazy reading of directory contents if set
1795+
(see `add_real_directory`)
16861796
16871797
Raises:
16881798
OSError: if any of the files and directories in the list does not exist in the real file system.
16891799
OSError: if any of the files and directories in the list already exists in the fake file system.
16901800
"""
16911801
for path in path_list:
16921802
if os.path.isdir(path):
1693-
self.add_real_directory(path, read_only)
1803+
self.add_real_directory(path, read_only, lazy_dir_read)
16941804
else:
16951805
self.add_real_file(path, read_only)
16961806

16971807
def _CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE,
16981808
contents='', st_size=None, create_missing_dirs=True,
16991809
apply_umask=False, encoding=None, errors=None,
1700-
read_from_real_fs=False, real_stat=None):
1701-
"""Create file_path, including all the parent directories along the way.
1810+
read_from_real_fs=False, read_only=True):
1811+
"""Internal fake file creation, supports both normal fake files and fake files from real files.
17021812
17031813
Args:
17041814
file_path: path to the file to create.
@@ -1708,12 +1818,10 @@ def _CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE,
17081818
create_missing_dirs: if True, auto create missing directories.
17091819
apply_umask: whether or not the current umask must be applied on st_mode.
17101820
encoding: if contents is a unicode string, the encoding used for serialization.
1711-
New in pyfakefs 2.9.
17121821
errors: the error mode used for encoding/decoding errors
1713-
New in pyfakefs 3.2.
17141822
read_from_real_fs: if True, the contents are reaf from the real file system on demand.
1715-
New in pyfakefs 3.2.
1716-
real_stat: used in combination with read_from_real_fs; stat result of the real file
1823+
read_only: if set, the file is treated as read-only, e.g. a write access raises an exception;
1824+
otherwise, writing to the file changes the fake file only as usually.
17171825
"""
17181826
file_path = self.NormalizePath(file_path)
17191827
if self.Exists(file_path):
@@ -1732,22 +1840,16 @@ def _CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE,
17321840
parent_directory = self.NormalizeCase(parent_directory)
17331841
if apply_umask:
17341842
st_mode &= ~self.umask
1735-
file_object = FakeFile(new_file, st_mode, filesystem=self, encoding=encoding, errors=errors)
17361843
if read_from_real_fs:
1737-
file_object.st_ctime = real_stat.st_ctime
1738-
file_object.st_atime = real_stat.st_atime
1739-
file_object.st_mtime = real_stat.st_mtime
1740-
file_object.st_gid = real_stat.st_gid
1741-
file_object.st_uid = real_stat.st_uid
1742-
file_object.st_size = real_stat.st_size
1743-
file_object.read_from_real_fs = True
1744-
file_object.file_path = file_path
1844+
file_object = FakeFileFromRealFile(file_path, filesystem=self, read_only=read_only)
1845+
else:
1846+
file_object = FakeFile(new_file, st_mode, filesystem=self, encoding=encoding, errors=errors)
17451847

17461848
self.last_ino += 1
17471849
file_object.SetIno(self.last_ino)
17481850
self.AddObject(parent_directory, file_object)
17491851

1750-
if contents is not None or st_size is not None:
1852+
if not read_from_real_fs and (contents is not None or st_size is not None):
17511853
try:
17521854
if st_size is not None:
17531855
file_object.SetLargeFileSize(st_size)

0 commit comments

Comments
 (0)