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
3045import os
3146import shutil
3247import sys
3550class 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 )
0 commit comments