Skip to content

Commit bcd91d1

Browse files
committed
Move shutil patches to fake shutil module
- allows to use FakeShutilModule outside of Patcher
1 parent edd6a6c commit bcd91d1

File tree

4 files changed

+124
-50
lines changed

4 files changed

+124
-50
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The released versions correspond to PyPI releases.
1717
### Changes
1818
* the message from an `OSError` raised in the fake filesystem has no longer the postfix
1919
_"in the fake filesystem"_ (see [#1159](../../discussions/1159))
20+
* changed implementation of `FakeShutilModule` to be able to be used without the patcher
21+
(see [#1171](../../discussions/1171))
2022

2123
### Enhancements
2224
* added convenience function `add_package_metadata` to add the metadata of a given

pyfakefs/fake_filesystem_shutil.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,26 @@
2222
shutil module.
2323
2424
:Usage:
25+
26+
* With Patcher:
2527
The fake implementation is automatically involved if using
2628
`fake_filesystem_unittest.TestCase`, pytest fs fixture,
2729
or directly `Patcher`.
30+
31+
* Stand-alone with FakeFilesystem:
32+
To patch it independently of these, you also need to patch `os`, e.g:
33+
34+
filesystem = fake_filesystem.FakeFilesystem()
35+
fake_os = fake_os.FakeOsModule(filesystem)
36+
fake_shutil = fake_filesystem_shutil.FakeShutilModule(filesystem)
37+
38+
with patch("os", fake_os):
39+
with patch("shutil.os", shutil_mock):
40+
shutil.rmtree("path/in/fakefs")
41+
2842
"""
2943

44+
import contextlib
3045
import os
3146
import shutil
3247
import sys
@@ -35,8 +50,28 @@
3550
class FakeShutilModule:
3651
"""Uses a FakeFilesystem to provide a fake replacement
3752
for shutil module.
53+
54+
Automatically created if using `fake_filesystem_unittest.TestCase`,
55+
the `fs` fixture, the `patchfs` decorator, or directly the `Patcher`.
56+
57+
To patch it separately, you also need to patch `os`::
58+
59+
filesystem = fake_filesystem.FakeFilesystem()
60+
fake_os = fake_os.FakeOsModule(filesystem)
61+
fake_shutil = fake_filesystem_shutil.FakeShutilModule(filesystem)
62+
63+
with patch("os", fake_os):
64+
with patch("shutil.os", shutil_mock):
65+
shutil.rmtree("path/in/fakefs")
3866
"""
3967

68+
use_copy_file_range = (
69+
hasattr(shutil, "_USE_CP_COPY_FILE_RANGE") and shutil._USE_CP_COPY_FILE_RANGE # type: ignore[attr-defined]
70+
)
71+
has_fcopy_file = hasattr(shutil, "_HAS_FCOPYFILE") and shutil._HAS_FCOPYFILE # type: ignore[attr-defined]
72+
use_sendfile = hasattr(shutil, "_USE_CP_SENDFILE") and shutil._USE_CP_SENDFILE # type: ignore[attr-defined]
73+
use_fd_functions = shutil._use_fd_functions # type: ignore[attr-defined]
74+
4075
@staticmethod
4176
def dir():
4277
"""Return the list of patched function names. Used for patching
@@ -51,7 +86,46 @@ def __init__(self, filesystem):
5186
filesystem: FakeFilesystem used to provide file system information
5287
"""
5388
self.filesystem = filesystem
54-
self._shutil_module = shutil
89+
self.shutil_module = shutil
90+
self._in_get_attribute = False
91+
92+
def start_patching_global_vars(self):
93+
if self.__class__.has_fcopy_file:
94+
self.shutil_module._HAS_FCOPYFILE = False
95+
if self.__class__.use_copy_file_range:
96+
self.shutil_module._USE_CP_COPY_FILE_RANGE = False
97+
if self.__class__.use_sendfile:
98+
self.shutil_module._USE_CP_SENDFILE = False
99+
if self.use_fd_functions:
100+
if sys.version_info >= (3, 14):
101+
self.shutil_module._rmtree_impl = (
102+
self.shutil_module._rmtree_unsafe # type: ignore[attr-defined]
103+
)
104+
else:
105+
self.shutil_module._use_fd_functions = False
106+
107+
def stop_patching_global_vars(self):
108+
if self.__class__.has_fcopy_file:
109+
self.shutil_module._HAS_FCOPYFILE = True
110+
if self.__class__.use_copy_file_range:
111+
self.shutil_module._USE_CP_COPY_FILE_RANGE = True
112+
if self.__class__.use_sendfile:
113+
self.shutil_module._USE_CP_SENDFILE = True
114+
if self.__class__.use_fd_functions:
115+
if sys.version_info >= (3, 14):
116+
self.__class__.shutil_module._rmtree_impl = (
117+
self.shutil_module._rmtree_safe_fd # type: ignore[attr-defined]
118+
)
119+
else:
120+
self.shutil_module._use_fd_functions = True
121+
122+
@contextlib.contextmanager
123+
def patch_global_vars(self):
124+
self.start_patching_global_vars()
125+
try:
126+
yield
127+
finally:
128+
self.start_patching_global_vars()
55129

56130
def disk_usage(self, path):
57131
"""Return the total, used and free disk space in bytes as named tuple
@@ -62,6 +136,32 @@ def disk_usage(self, path):
62136
"""
63137
return self.filesystem.get_disk_usage(path)
64138

139+
if sys.version_info < (3, 11):
140+
141+
def rmtree(self, path, ignore_errors=False, onerror=None):
142+
with self.patch_global_vars():
143+
self.shutil_module.rmtree(path, ignore_errors, onerror)
144+
145+
elif sys.version_info < (3, 12):
146+
147+
def rmtree(self, path, ignore_errors=False, onerror=None, *, dir_fd=None):
148+
with self.patch_global_vars():
149+
self.shutil_module.rmtree(path, ignore_errors, onerror, dir_fd=dir_fd)
150+
151+
else:
152+
153+
def rmtree(
154+
self, path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None
155+
):
156+
with self.patch_global_vars():
157+
self.shutil_module.rmtree(
158+
path, ignore_errors, onerror, onexc=onexc, dir_fd=dir_fd
159+
)
160+
161+
def copyfile(self, src, dst, *, follow_symlinks=True):
162+
with self.patch_global_vars():
163+
self.shutil_module.copyfile(src, dst, follow_symlinks=follow_symlinks)
164+
65165
if sys.version_info >= (3, 12) and sys.platform == "win32":
66166

67167
def copy2(self, src, dst, *, follow_symlinks=True):
@@ -89,7 +189,7 @@ def copytree(
89189
"""Make sure the default argument is patched."""
90190
if copy_function == shutil.copy2:
91191
copy_function = self.copy2
92-
return self._shutil_module.copytree(
192+
return self.shutil_module.copytree(
93193
src,
94194
dst,
95195
symlinks,
@@ -103,8 +203,8 @@ def move(self, src, dst, copy_function=shutil.copy2):
103203
"""Make sure the default argument is patched."""
104204
if copy_function == shutil.copy2:
105205
copy_function = self.copy2
106-
return self._shutil_module.move(src, dst, copy_function)
206+
return self.shutil_module.move(src, dst, copy_function)
107207

108208
def __getattr__(self, name):
109209
"""Forwards any non-faked calls to the standard shutil module."""
110-
return getattr(self._shutil_module, name)
210+
return getattr(self.shutil_module, name)

pyfakefs/fake_filesystem_unittest.py

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import io
4646
import linecache
4747
import os
48-
import shutil
4948
import sys
5049
import tempfile
5150
import tokenize
@@ -690,6 +689,20 @@ def __init__(
690689
self.use_dynamic_patch = use_dynamic_patch
691690
self.cleanup_handlers: Dict[str, Callable[[str], bool]] = {}
692691

692+
# Attributes set by _refresh()
693+
self._stubs: Optional[StubOutForTesting] = None
694+
self.fs: Optional[FakeFilesystem] = None
695+
self.fake_modules: Dict[str, Any] = {}
696+
self.unfaked_modules: Dict[str, Any] = {}
697+
698+
# _isStale is set by tearDown(), reset by _refresh()
699+
self._isStale = True
700+
self._dyn_patcher: Optional[DynamicPatcher] = None
701+
self._patching = False
702+
self._paused = False
703+
self.has_copy_file_range = False
704+
self.has_copy_file = False
705+
693706
if use_known_patches:
694707
from pyfakefs.patched_packages import (
695708
get_modules_to_patch,
@@ -727,20 +740,6 @@ def __init__(
727740
self._fake_module_functions: Dict[str, Dict] = {}
728741
self._init_fake_module_functions()
729742

730-
# Attributes set by _refresh()
731-
self._stubs: Optional[StubOutForTesting] = None
732-
self.fs: Optional[FakeFilesystem] = None
733-
self.fake_modules: Dict[str, Any] = {}
734-
self.unfaked_modules: Dict[str, Any] = {}
735-
736-
# _isStale is set by tearDown(), reset by _refresh()
737-
self._isStale = True
738-
self._dyn_patcher: Optional[DynamicPatcher] = None
739-
self._patching = False
740-
self._paused = False
741-
self.has_copy_file_range = False
742-
self.has_copy_file = False
743-
744743
@classmethod
745744
def clear_fs_cache(cls) -> None:
746745
"""Clear the module cache."""
@@ -1027,30 +1026,6 @@ def setUp(self, doctester: Any = None) -> None:
10271026
self.__class__.REF_COUNT += 1
10281027
if self.__class__.REF_COUNT > 1:
10291028
return
1030-
self.has_fcopy_file = (
1031-
sys.platform == "darwin"
1032-
and hasattr(shutil, "_HAS_FCOPYFILE")
1033-
and shutil._HAS_FCOPYFILE
1034-
)
1035-
if self.has_fcopy_file:
1036-
shutil._HAS_FCOPYFILE = False # type: ignore[attr-defined]
1037-
1038-
self.has_copy_file_range = (
1039-
sys.platform == "linux"
1040-
and hasattr(shutil, "_USE_CP_COPY_FILE_RANGE")
1041-
and shutil._USE_CP_COPY_FILE_RANGE
1042-
)
1043-
if self.has_copy_file_range:
1044-
shutil._USE_CP_COPY_FILE_RANGE = False # type: ignore[attr-defined]
1045-
1046-
# do not use the fd functions, as they may not be available in the target OS
1047-
if hasattr(shutil, "_use_fd_functions"):
1048-
shutil._use_fd_functions = False # type: ignore[module-attr]
1049-
# in Python 3.14, _rmtree_impl is set at load time based on _use_fd_functions
1050-
# the safe version cannot be used at the moment as it used asserts of type
1051-
# 'assert func is os.rmtree', which do not work with the fake versions
1052-
if hasattr(shutil, "_rmtree_impl"):
1053-
shutil._rmtree_impl = shutil._rmtree_unsafe # type: ignore[attr-defined]
10541029

10551030
with warnings.catch_warnings():
10561031
# ignore warnings, see #542 and #614
@@ -1169,10 +1144,6 @@ def tearDown(self, doctester: Any = None):
11691144
if self.__class__.REF_COUNT > 0:
11701145
return
11711146
self.stop_patching()
1172-
if self.has_fcopy_file:
1173-
shutil._HAS_FCOPYFILE = True # type: ignore[attr-defined]
1174-
if self.has_copy_file_range:
1175-
shutil._USE_CP_COPY_FILE_RANGE = True # type: ignore[attr-defined]
11761147

11771148
reset_ids()
11781149
if self.is_doc_test:

pyfakefs/fake_pathlib.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
1616
Usage:
1717
18-
* With fake_filesystem_unittest:
19-
If using fake_filesystem_unittest.TestCase, pathlib gets replaced
18+
* With Patcher:
19+
If using `fake_filesystem_unittest.TestCase`, pytest fs fixture,
20+
or directly `Patcher`, pathlib gets replaced
2021
by fake_pathlib together with other file system related modules.
2122
2223
* Stand-alone with FakeFilesystem:
@@ -865,7 +866,7 @@ class FakePathlibModule:
865866
"""Uses FakeFilesystem to provide a fake pathlib module replacement.
866867
867868
Automatically created if using `fake_filesystem_unittest.TestCase`,
868-
`fs` fixture, or the `patchfs` decorator.
869+
the `fs` fixture, the `patchfs` decorator, or directly the `Patcher`.
869870
870871
For creating it separately, a `fake_filesystem` instance is needed::
871872

0 commit comments

Comments
 (0)