23
23
24
24
:Usage:
25
25
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`.
42
29
"""
43
30
44
- import contextlib
31
+ import functools
45
32
import os
46
33
import shutil
47
34
import sys
35
+ from threading import Lock
36
+ from typing import Callable
48
37
49
38
50
39
class FakeShutilModule :
@@ -53,24 +42,19 @@ class FakeShutilModule:
53
42
54
43
Automatically created if using `fake_filesystem_unittest.TestCase`,
55
44
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")
66
45
"""
67
46
47
+ module_lock = Lock ()
48
+
68
49
use_copy_file_range = (
69
50
hasattr (shutil , "_USE_CP_COPY_FILE_RANGE" ) and shutil ._USE_CP_COPY_FILE_RANGE # type: ignore[attr-defined]
70
51
)
71
52
has_fcopy_file = hasattr (shutil , "_HAS_FCOPYFILE" ) and shutil ._HAS_FCOPYFILE # type: ignore[attr-defined]
72
53
use_sendfile = hasattr (shutil , "_USE_CP_SENDFILE" ) and shutil ._USE_CP_SENDFILE # type: ignore[attr-defined]
73
54
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" ])
74
58
75
59
@staticmethod
76
60
def dir ():
@@ -89,12 +73,12 @@ def __init__(self, filesystem):
89
73
self .shutil_module = shutil
90
74
self ._in_get_attribute = False
91
75
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 :
94
78
self .shutil_module ._HAS_FCOPYFILE = False
95
- if self .__class__ . use_copy_file_range :
79
+ if self .use_copy_file_range :
96
80
self .shutil_module ._USE_CP_COPY_FILE_RANGE = False
97
- if self .__class__ . use_sendfile :
81
+ if self .use_sendfile :
98
82
self .shutil_module ._USE_CP_SENDFILE = False
99
83
if self .use_fd_functions :
100
84
if sys .version_info >= (3 , 14 ):
@@ -104,28 +88,36 @@ def start_patching_global_vars(self):
104
88
else :
105
89
self .shutil_module ._use_fd_functions = False
106
90
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 :
109
93
self .shutil_module ._HAS_FCOPYFILE = True
110
- if self .__class__ . use_copy_file_range :
94
+ if self .use_copy_file_range :
111
95
self .shutil_module ._USE_CP_COPY_FILE_RANGE = True
112
- if self .__class__ . use_sendfile :
96
+ if self .use_sendfile :
113
97
self .shutil_module ._USE_CP_SENDFILE = True
114
- if self .__class__ . use_fd_functions :
98
+ if self .use_fd_functions :
115
99
if sys .version_info >= (3 , 14 ):
116
- self .__class__ . shutil_module ._rmtree_impl = (
100
+ self .shutil_module ._rmtree_impl = (
117
101
self .shutil_module ._rmtree_safe_fd # type: ignore[attr-defined]
118
102
)
119
103
else :
120
104
self .shutil_module ._use_fd_functions = True
121
105
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
129
121
130
122
def disk_usage (self , path ):
131
123
"""Return the total, used and free disk space in bytes as named tuple
@@ -136,32 +128,6 @@ def disk_usage(self, path):
136
128
"""
137
129
return self .filesystem .get_disk_usage (path )
138
130
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
-
165
131
if sys .version_info >= (3 , 12 ) and sys .platform == "win32" :
166
132
167
133
def copy2 (self , src , dst , * , follow_symlinks = True ):
@@ -207,4 +173,6 @@ def move(self, src, dst, copy_function=shutil.copy2):
207
173
208
174
def __getattr__ (self , name ):
209
175
"""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 ))
210
178
return getattr (self .shutil_module , name )
0 commit comments