Skip to content

Commit 07e3611

Browse files
committed
Ignore permissions in FakeFilesystem.get_object()
- restores the behavior before version 5.4.0 - fix permission error on reading file type - fix os.access() behavior for root user - fix os.scandir(None) behavior - see #1122
1 parent ba6d191 commit 07e3611

File tree

6 files changed

+121
-41
lines changed

6 files changed

+121
-41
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ The released versions correspond to PyPI releases.
1616

1717
### Changes
1818
* added some preliminary support for Python 3.14
19+
* change behavior of `FakeFilesystem.get_object()` to ignore permissions as it has been
20+
before version 5.4.0 (see [#1122](../../issues/1122))
1921

2022
### Fixes
2123
* fixed a problem with flushing if writing over the buffer end
2224
(see [#1120](../../issues/1120))
2325
* fixed a regression that could break tests under Posix in Python 3.12
2426
(see [#1126](../../issues/1126))
2527
* fixed behavior for `os.access` for symlinks under Windows
28+
* fixes permission problem on querying file properties (see [#1122](../../issues/1122))
2629

2730
## [Version 5.7.4](https://pypi.python.org/pypi/pyfakefs/5.7.4) (2025-01-14)
2831
Minor bugfix release.

pyfakefs/fake_filesystem.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ def stat(self, entry_path: AnyStr, follow_symlinks: bool = True):
745745
# make sure stat raises if a parent dir is not readable
746746
parent_dir = file_object.parent_dir
747747
if parent_dir:
748-
self.get_object(parent_dir.path, check_read_perm=False) # type: ignore[arg-type]
748+
self._get_object(parent_dir.path, check_read_perm=False) # type: ignore[arg-type]
749749

750750
self.raise_for_filepath_ending_with_separator(
751751
entry_path, file_object, follow_symlinks
@@ -1775,15 +1775,22 @@ def _can_read(target, check_read_perm, check_exe_perm, owner_can_read):
17751775
return True
17761776
return target.has_permission(permission)
17771777

1778-
def get_object(self, file_path: AnyPath, check_read_perm: bool = True) -> FakeFile:
1778+
def _get_object(
1779+
self,
1780+
file_path: AnyPath,
1781+
check_read_perm: bool = True,
1782+
check_exe_perm: bool = True,
1783+
) -> FakeFile:
17791784
"""Search for the specified filesystem object within the fake
1780-
filesystem.
1785+
filesystem. By default, consider read and execute permissions.
17811786
17821787
Args:
17831788
file_path: Specifies the target
17841789
:py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object to retrieve.
17851790
check_read_perm: If True, raises OSError if a parent directory
17861791
does not have read permission
1792+
check_exe_perm: If True, raises OSError if a parent directory
1793+
does not have execute (e.g. search) permission
17871794
17881795
Returns:
17891796
The :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object corresponding
@@ -1794,7 +1801,24 @@ def get_object(self, file_path: AnyPath, check_read_perm: bool = True) -> FakeFi
17941801
"""
17951802
path = make_string_path(file_path)
17961803
path = self.absnormpath(self._original_path(path))
1797-
return self.get_object_from_normpath(path, check_read_perm)
1804+
return self.get_object_from_normpath(path, check_read_perm, check_exe_perm)
1805+
1806+
def get_object(self, file_path: AnyPath) -> FakeFile:
1807+
"""Search for the specified filesystem object within the fake
1808+
filesystem and return it. Ignore any read permissions.
1809+
1810+
Args:
1811+
file_path: Specifies the target
1812+
:py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object to retrieve.
1813+
1814+
Returns:
1815+
The :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object corresponding
1816+
to `file_path`.
1817+
1818+
Raises:
1819+
OSError: if the object is not found.
1820+
"""
1821+
return self._get_object(file_path, check_read_perm=False, check_exe_perm=False)
17981822

17991823
def resolve(
18001824
self,
@@ -2051,7 +2075,7 @@ def _rename_to_existing_path(
20512075
old_object: FakeFile,
20522076
ends_with_sep: bool,
20532077
) -> Optional[AnyStr]:
2054-
new_object = self.get_object(new_file_path)
2078+
new_object = self._get_object(new_file_path)
20552079
if old_file_path == new_file_path:
20562080
if not S_ISLNK(new_object.st_mode) and ends_with_sep:
20572081
error = errno.EINVAL if self.is_windows_fs else errno.ENOTDIR
@@ -2418,7 +2442,7 @@ def add_real_directory(
24182442
)
24192443
else:
24202444
self._create_fake_from_real_dir(source_path_str, target_path_str, read_only)
2421-
return cast(FakeDirectory, self.get_object(target_path_str))
2445+
return cast(FakeDirectory, self._get_object(target_path_str))
24222446

24232447
def _create_fake_from_real_dir(self, source_path_str, target_path_str, read_only):
24242448
if not self.exists(target_path_str):
@@ -2456,11 +2480,11 @@ def _create_fake_from_real_dir_lazily(
24562480
self.add_real_symlink(src_entry_path, target_entry_path)
24572481
elif os.path.isfile(src_entry_path):
24582482
self.add_real_file(src_entry_path, read_only, target_entry_path)
2459-
return self.get_object(target_path_str)
2483+
return self._get_object(target_path_str)
24602484

24612485
parent_path = os.path.split(target_path_str)[0]
24622486
if self.exists(parent_path):
2463-
parent_dir = self.get_object(parent_path)
2487+
parent_dir = self._get_object(parent_path)
24642488
else:
24652489
parent_dir = self.create_dir(parent_path)
24662490
new_dir = FakeDirectoryFromRealDirectory(
@@ -2913,7 +2937,6 @@ def _is_of_type(
29132937
path: AnyPath,
29142938
st_flag: int,
29152939
follow_symlinks: bool = True,
2916-
check_read_perm: bool = True,
29172940
) -> bool:
29182941
"""Helper function to implement isdir(), islink(), etc.
29192942
@@ -2922,8 +2945,8 @@ def _is_of_type(
29222945
Args:
29232946
path: Path to file to stat and test
29242947
st_flag: The stat.S_I* flag checked for the file's st_mode
2925-
check_read_perm: If True (default) False is returned for
2926-
existing but unreadable file paths.
2948+
follow_symlinks: If `False` and path points to a symlink,
2949+
the link itself is checked instead of the linked object.
29272950
29282951
Returns:
29292952
(boolean) `True` if the st_flag is set in path's st_mode.
@@ -2935,9 +2958,7 @@ def _is_of_type(
29352958
raise TypeError
29362959
file_path = make_string_path(path)
29372960
try:
2938-
obj = self.resolve(
2939-
file_path, follow_symlinks, check_read_perm=check_read_perm
2940-
)
2961+
obj = self.resolve(file_path, follow_symlinks, check_read_perm=False)
29412962
if obj:
29422963
self.raise_for_filepath_ending_with_separator(
29432964
file_path, obj, macos_handling=not follow_symlinks
@@ -2952,6 +2973,8 @@ def isdir(self, path: AnyPath, follow_symlinks: bool = True) -> bool:
29522973
29532974
Args:
29542975
path: Path to filesystem object.
2976+
follow_symlinks: If `False` and path points to a symlink,
2977+
the link itself is checked instead of the linked object.
29552978
29562979
Returns:
29572980
`True` if path points to a directory (following symlinks).
@@ -2966,14 +2989,16 @@ def isfile(self, path: AnyPath, follow_symlinks: bool = True) -> bool:
29662989
29672990
Args:
29682991
path: Path to filesystem object.
2992+
follow_symlinks: If `False` and path points to a symlink,
2993+
the link itself is checked instead of the linked object.
29692994
29702995
Returns:
29712996
`True` if path points to a regular file (following symlinks).
29722997
29732998
Raises:
29742999
TypeError: if path is None.
29753000
"""
2976-
return self._is_of_type(path, S_IFREG, follow_symlinks, check_read_perm=False)
3001+
return self._is_of_type(path, S_IFREG, follow_symlinks)
29773002

29783003
def islink(self, path: AnyPath) -> bool:
29793004
"""Determine if path identifies a symbolic link.
@@ -3121,6 +3146,8 @@ def listdir(self, target_directory: AnyStr) -> List[AnyStr]:
31213146
Raises:
31223147
OSError: if the target is not a directory.
31233148
"""
3149+
if target_directory is None:
3150+
return []
31243151
target_directory = self.resolve_path(target_directory, allow_fd=True)
31253152
directory = self.confirmdir(target_directory, check_exe_perm=False)
31263153
directory_contents = list(directory.entries.keys())

pyfakefs/fake_os.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,20 @@ def chdir(self, path: AnyStr) -> None:
453453
except OSError as exc:
454454
if self.filesystem.is_macos and exc.errno == errno.EBADF:
455455
raise OSError(errno.ENOTDIR, "Not a directory: " + str(path))
456+
elif (
457+
self.filesystem.is_windows_fs
458+
and exc.errno == errno.ENOENT
459+
and path == ""
460+
):
461+
raise OSError(errno.EINVAL, "Invalid argument: + str(path)")
462+
raise
463+
try:
464+
self.filesystem.confirmdir(path)
465+
except OSError as exc:
466+
if exc.errno == errno.EACCES:
467+
# no access rights to the parent directory - do nothing
468+
return
456469
raise
457-
self.filesystem.confirmdir(path)
458470
directory = self.filesystem.resolve(path)
459471
# A full implementation would check permissions all the way
460472
# up the tree.
@@ -1058,7 +1070,9 @@ def access(
10581070
return False
10591071
raise
10601072
if is_root():
1061-
mode &= ~os.W_OK
1073+
if mode & os.X_OK:
1074+
return stat_result.st_mode & 0o111 != 0
1075+
return True
10621076
return (mode & ((stat_result.st_mode >> 6) & 7)) == mode
10631077

10641078
def fchmod(

pyfakefs/fake_scandir.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,10 @@ def __init__(self, filesystem, path):
133133
self.filesystem.get_open_file(path).get_object().path
134134
)
135135
self.path = ""
136+
self.entry_iter = iter(tuple())
136137
else:
138+
if path is None:
139+
path = "."
137140
path = make_string_path(path)
138141
self.abspath = self.filesystem.absnormpath(path)
139142
self.path = to_string(path)

pyfakefs/tests/fake_filesystem_vs_real_test.py

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,10 @@ def _get_errno(raised_error):
4141
pass
4242

4343

44-
class TestCase(unittest.TestCase):
44+
class FakeFilesystemVsRealTest(unittest.TestCase):
4545
is_windows = sys.platform.startswith("win")
46-
_FAKE_FS_BASE = "C:\\fakefs" if is_windows else "/fakefs"
46+
fake_base = "C:\\fakefs" if is_windows else "/fakefs"
4747

48-
49-
class FakeFilesystemVsRealTest(TestCase):
5048
def _paths(self, path):
5149
"""For a given path, return paths in the real and fake filesystems."""
5250
if not path:
@@ -106,7 +104,6 @@ def setUp(self):
106104
if os.path.isdir(self.real_base):
107105
shutil.rmtree(self.real_base)
108106
os.mkdir(self.real_base)
109-
self.fake_base = self._FAKE_FS_BASE
110107

111108
# Make sure we can write to the physical testing temp directory.
112109
self.assertTrue(os.access(self.real_base, os.W_OK))
@@ -167,7 +164,7 @@ def _compare_behaviors(
167164
to method.
168165
real: built-in system library or method from the built-in system
169166
library which takes a path as an arg and returns some value.
170-
fake: fake_filsystem object or method from a fake_filesystem class
167+
fake: fake_filesystem object or method from a fake_filesystem class
171168
which takes a path as an arg and returns some value.
172169
method_returns_path: True if the method returns a path, and thus we
173170
must compensate for expected difference between real and fake.
@@ -302,7 +299,7 @@ def assertOsMethodBehaviorMatches(
302299
def diff_open_method_behavior(
303300
self, method_name, path, mode, data, method_returns_data=True
304301
):
305-
"""Invoke an open method in both real and fkae contexts and compare.
302+
"""Invoke an open method in both real and fake contexts and compare.
306303
307304
Args:
308305
method_name: Name of method being tested.
@@ -335,7 +332,7 @@ def diff_os_path_method_behavior(
335332
336333
For a given method name (from the os.path module) and a path, compare
337334
the behavior of the system provided module against the
338-
fake_filesytem module.
335+
fake_filesystem module.
339336
We expect results and/or Exceptions raised to be identical.
340337
341338
Args:
@@ -374,24 +371,35 @@ def assertOsPathMethodBehaviorMatches(
374371
if diff:
375372
self.fail(diff)
376373

377-
def assertAllOsBehaviorsMatch(self, path):
374+
def assertAllOsBehaviorsMatch(self, path, excludes=None):
375+
excludes = excludes or []
378376
path = sep(path)
379377
os_method_names = ["readlink"]
380378
os_method_names_no_args = ["getcwd"]
381-
os_path_method_names = [
379+
os_path_method_names = {
382380
"isabs",
383381
"isdir",
384382
"islink",
385383
"lexists",
386384
"isfile",
387385
"exists",
388-
]
386+
} - set(excludes)
389387

390388
wrapped_methods = [
391389
["access", self._access_real, self._access_fake],
392-
["stat.size", self._stat_size_real, self._stat_size_fake],
390+
[
391+
"stat.size",
392+
lambda p: self._stat_result_real(p, "st_size", False),
393+
lambda p: self._stat_result_fake(p, "st_size", False),
394+
],
395+
[
396+
"stat.mode",
397+
lambda p: self._stat_result_real(p, "st_mode"),
398+
lambda p: self._stat_result_fake(p, "st_mode"),
399+
],
393400
["lstat.size", self._lstat_size_real, self._lstat_size_fake],
394401
]
402+
wrapped_methods = [m for m in wrapped_methods if m[0] not in excludes]
395403

396404
differences = []
397405
for method_name in os_method_names:
@@ -492,19 +500,18 @@ def _access_real(path):
492500
def _access_fake(self, path):
493501
return self.fake_os.access(path, os.R_OK)
494502

495-
def _stat_size_real(self, path):
503+
def _stat_result_real(self, path, prop, support_dir=True):
496504
real_path, unused_fake_path = self._paths(path)
497505
# fake_filesystem.py does not implement stat().st_size for directories
498-
if os.path.isdir(real_path):
506+
if not support_dir and os.path.isdir(real_path):
499507
return None
500-
return os.stat(real_path).st_size
508+
return getattr(os.stat(real_path), prop)
501509

502-
def _stat_size_fake(self, path):
503-
unused_real_path, fake_path = self._paths(path)
504-
# fake_filesystem.py does not implement stat().st_size for directories
505-
if self.fake_os.path.isdir(fake_path):
510+
def _stat_result_fake(self, path, prop, support_dir=True):
511+
_, fake_path = self._paths(path)
512+
if not support_dir and self.fake_os.path.isdir(fake_path):
506513
return None
507-
return self.fake_os.stat(fake_path).st_size
514+
return getattr(self.fake_os.stat(fake_path), prop)
508515

509516
def _lstat_size_real(self, path):
510517
real_path, unused_fake_path = self._paths(path)
@@ -544,9 +551,9 @@ def test_empty_path(self):
544551
self.assertAllOsBehaviorsMatch("")
545552

546553
def test_root_path(self):
547-
self.assertAllOsBehaviorsMatch("/")
554+
self.assertAllOsBehaviorsMatch("/", excludes=["stat.mode"])
548555

549-
def test_non_existant_file(self):
556+
def test_non_existent_file(self):
550557
self.assertAllOsBehaviorsMatch("foo")
551558

552559
def test_empty_file(self):
@@ -690,7 +697,7 @@ def test_bad_relative_path(self):
690697
self._create_test_file("f", "a/sibling_of_b/target", "contents")
691698
self.assertAllOsBehaviorsMatch("a/b/../broken/../target")
692699

693-
def test_getmtime_nonexistant_path(self):
700+
def test_getmtime_nonexistent_path(self):
694701
self.assertOsPathMethodBehaviorMatches("getmtime", "no/such/path")
695702

696703
def test_builtin_open_modes(self):
@@ -724,6 +731,27 @@ def test_builtin_open_modes(self):
724731
self.assertFileHandleOpenBehaviorsMatch("write", "w", encoding="utf-8")
725732
self.assertFileHandleOpenBehaviorsMatch("append", "a", encoding="utf-8")
726733

734+
def test_directory_permissions(self):
735+
self._create_test_file("d", "a")
736+
self._create_test_file("f", "a/write")
737+
self._create_test_file("d", "b")
738+
self._create_test_file("f", "b/write")
739+
self._create_test_file("d", "b/stat_dir")
740+
for path, mode in [("b", 0o555), ("b/stat_dir", 0o111)]:
741+
real_path, fake_path = self._paths(path)
742+
os.chmod(real_path, mode)
743+
self.fake_os.chmod(fake_path, mode)
744+
745+
try:
746+
self.assertFileHandleOpenBehaviorsMatch("a/write", "w")
747+
self.assertFileHandleOpenBehaviorsMatch("b/write", "w")
748+
self.assertAllOsBehaviorsMatch("b/stat_dir")
749+
finally:
750+
for path in ["b", "b/stat_dir"]:
751+
real_path, fake_path = self._paths(path)
752+
os.chmod(real_path, 0o777)
753+
self.fake_os.chmod(fake_path, 0o777)
754+
727755

728756
def main(_):
729757
unittest.main()

0 commit comments

Comments
 (0)