2323
2424:Usage:
2525
26- * With Patcher:
27- The fake implementation is automatically involved if using
28- `fake_filesystem_unittest.TestCase`, pytest fs fixture,
29- 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-
26+ The fake implementation is automatically involved if using
27+ `fake_filesystem_unittest.TestCase`, pytest fs fixture,
28+ or directly `Patcher`.
4229"""
4330
44- import contextlib
31+ import functools
4532import os
4633import shutil
4734import sys
35+ from threading import Lock
36+ from typing import Callable
4837
4938
5039class FakeShutilModule :
@@ -53,24 +42,19 @@ class FakeShutilModule:
5342
5443 Automatically created if using `fake_filesystem_unittest.TestCase`,
5544 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")
6645 """
6746
47+ module_lock = Lock ()
48+
6849 use_copy_file_range = (
6950 hasattr (shutil , "_USE_CP_COPY_FILE_RANGE" ) and shutil ._USE_CP_COPY_FILE_RANGE # type: ignore[attr-defined]
7051 )
7152 has_fcopy_file = hasattr (shutil , "_HAS_FCOPYFILE" ) and shutil ._HAS_FCOPYFILE # type: ignore[attr-defined]
7253 use_sendfile = hasattr (shutil , "_USE_CP_SENDFILE" ) and shutil ._USE_CP_SENDFILE # type: ignore[attr-defined]
7354 use_fd_functions = shutil ._use_fd_functions # type: ignore[attr-defined]
55+ functions_to_patch = ["copy" , "copyfile" , "rmtree" ]
56+ if sys .version_info < (3 , 12 ) or sys .platform != "win32" :
57+ functions_to_patch .extend (["copy2" , "copytree" , "move" ])
7458
7559 @staticmethod
7660 def dir ():
@@ -89,12 +73,12 @@ def __init__(self, filesystem):
8973 self .shutil_module = shutil
9074 self ._in_get_attribute = False
9175
92- def start_patching_global_vars (self ):
93- if self .__class__ . has_fcopy_file :
76+ def _start_patching_global_vars (self ):
77+ if self .has_fcopy_file :
9478 self .shutil_module ._HAS_FCOPYFILE = False
95- if self .__class__ . use_copy_file_range :
79+ if self .use_copy_file_range :
9680 self .shutil_module ._USE_CP_COPY_FILE_RANGE = False
97- if self .__class__ . use_sendfile :
81+ if self .use_sendfile :
9882 self .shutil_module ._USE_CP_SENDFILE = False
9983 if self .use_fd_functions :
10084 if sys .version_info >= (3 , 14 ):
@@ -104,28 +88,36 @@ def start_patching_global_vars(self):
10488 else :
10589 self .shutil_module ._use_fd_functions = False
10690
107- def stop_patching_global_vars (self ):
108- if self .__class__ . has_fcopy_file :
91+ def _stop_patching_global_vars (self ):
92+ if self .has_fcopy_file :
10993 self .shutil_module ._HAS_FCOPYFILE = True
110- if self .__class__ . use_copy_file_range :
94+ if self .use_copy_file_range :
11195 self .shutil_module ._USE_CP_COPY_FILE_RANGE = True
112- if self .__class__ . use_sendfile :
96+ if self .use_sendfile :
11397 self .shutil_module ._USE_CP_SENDFILE = True
114- if self .__class__ . use_fd_functions :
98+ if self .use_fd_functions :
11599 if sys .version_info >= (3 , 14 ):
116- self .__class__ . shutil_module ._rmtree_impl = (
100+ self .shutil_module ._rmtree_impl = (
117101 self .shutil_module ._rmtree_safe_fd # type: ignore[attr-defined]
118102 )
119103 else :
120104 self .shutil_module ._use_fd_functions = True
121105
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 ()
106+ def with_patched_globals (self , f : Callable ) -> Callable :
107+ """Function wrapper that patches global variables during function execution.
108+ Can be used in multi-threading code.
109+ """
110+
111+ @functools .wraps (f )
112+ def wrapped (* args , ** kwargs ):
113+ with self .module_lock :
114+ self ._start_patching_global_vars ()
115+ try :
116+ return f (* args , ** kwargs )
117+ finally :
118+ self ._stop_patching_global_vars ()
119+
120+ return wrapped
129121
130122 def disk_usage (self , path ):
131123 """Return the total, used and free disk space in bytes as named tuple
@@ -136,32 +128,6 @@ def disk_usage(self, path):
136128 """
137129 return self .filesystem .get_disk_usage (path )
138130
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-
165131 if sys .version_info >= (3 , 12 ) and sys .platform == "win32" :
166132
167133 def copy2 (self , src , dst , * , follow_symlinks = True ):
@@ -207,4 +173,6 @@ def move(self, src, dst, copy_function=shutil.copy2):
207173
208174 def __getattr__ (self , name ):
209175 """Forwards any non-faked calls to the standard shutil module."""
176+ if name in self .functions_to_patch :
177+ return self .with_patched_globals (getattr (self .shutil_module , name ))
210178 return getattr (self .shutil_module , name )
0 commit comments