Skip to content

Commit a80d441

Browse files
committed
Add thread lock for global patches
- remove incorrect documentation about stand-alone usage - fix patching of global variables
1 parent f1c86aa commit a80d441

File tree

4 files changed

+43
-86
lines changed

4 files changed

+43
-86
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ 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
20+
* changed implementation of `FakeShutilModule` to prepare it for usage without the patcher
2121
(see [#1171](../../discussions/1171))
2222

2323
### Enhancements

pyfakefs/fake_filesystem_shutil.py

Lines changed: 38 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,17 @@
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
4532
import os
4633
import shutil
4734
import sys
35+
from threading import Lock
36+
from typing import Callable
4837

4938

5039
class 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)

pyfakefs/fake_pathlib.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,9 @@
1515
1616
Usage:
1717
18-
* With Patcher:
19-
If using `fake_filesystem_unittest.TestCase`, pytest fs fixture,
20-
or directly `Patcher`, pathlib gets replaced
21-
by fake_pathlib together with other file system related modules.
22-
23-
* Stand-alone with FakeFilesystem:
24-
`filesystem = fake_filesystem.FakeFilesystem()`
25-
`fake_pathlib_module = fake_pathlib.FakePathlibModule(filesystem)`
26-
`path = fake_pathlib_module.Path('/foo/bar')`
18+
If using `fake_filesystem_unittest.TestCase`, pytest fs fixture,
19+
or directly `Patcher`, pathlib gets replaced
20+
by fake_pathlib together with other file system related modules.
2721
2822
Note: as the implementation is based on FakeFilesystem, all faked classes
2923
(including PurePosixPath, PosixPath, PureWindowsPath and WindowsPath)
@@ -867,11 +861,6 @@ class FakePathlibModule:
867861
868862
Automatically created if using `fake_filesystem_unittest.TestCase`,
869863
the `fs` fixture, the `patchfs` decorator, or directly the `Patcher`.
870-
871-
For creating it separately, a `fake_filesystem` instance is needed::
872-
873-
filesystem = fake_filesystem.FakeFilesystem()
874-
fake_pathlib_module = fake_pathlib.FakePathlibModule(filesystem)
875864
"""
876865

877866
def __init__(self, filesystem, from_patcher=False):

pyfakefs/tests/fake_filesystem_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,7 @@ def test_realpath_allow_missing(self):
10451045
root_dir = self.filesystem.root_dir_name
10461046
self.filesystem.cwd = f"{root_dir}foo"
10471047
self.assertEqual(
1048-
"!foo!baz",
1048+
f"{root_dir}foo!baz",
10491049
self.os.path.realpath("baz", strict=os.path.ALLOW_MISSING), # type: ignore[attr-defined]
10501050
)
10511051
if not is_root():

0 commit comments

Comments
 (0)