Skip to content

Commit 5a62c25

Browse files
committed
Fix deadlock in shutil.copytree
- support recursive entry into patch functions - happened if using shutil's own function as copy_function argument
1 parent 6002085 commit 5a62c25

File tree

3 files changed

+25
-3
lines changed

3 files changed

+25
-3
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ The released versions correspond to PyPI releases.
1717
* changed the default for `FakeFilesystem.shuffle_listdir_results` to `True` to reflect
1818
the real filesystem behavior
1919

20+
### Fixes
21+
* fixed a deadlock in `shutil.copytree` if copying using an `shutil` function as
22+
`copy_function` argument (see [#1235](../../issues/1235))
23+
2024
## [Version 5.10.0](https://pypi.python.org/pypi/pyfakefs/5.10.0) (2025-10-11)
2125
Adds official support for Python 3.14. Last minor version before the 6.0 release.
2226

pyfakefs/fake_filesystem_shutil.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import os
3333
import shutil
3434
import sys
35-
from threading import Lock
35+
from threading import RLock
3636
from collections.abc import Callable
3737

3838

@@ -44,7 +44,7 @@ class FakeShutilModule:
4444
the `fs` fixture, the `patchfs` decorator, or directly the `Patcher`.
4545
"""
4646

47-
module_lock = Lock()
47+
module_lock = RLock()
4848

4949
use_copy_file_range = (
5050
hasattr(shutil, "_USE_CP_COPY_FILE_RANGE") and shutil._USE_CP_COPY_FILE_RANGE # type: ignore[attr-defined]
@@ -71,9 +71,12 @@ def __init__(self, filesystem):
7171
"""
7272
self.filesystem = filesystem
7373
self.shutil_module = shutil
74-
self._in_get_attribute = False
74+
self._patch_level = 0
7575

7676
def _start_patching_global_vars(self):
77+
self._patch_level += 1
78+
if self._patch_level > 1:
79+
return # nested call - already patched
7780
if self.has_fcopy_file:
7881
self.shutil_module._HAS_FCOPYFILE = False
7982
if self.use_copy_file_range:
@@ -89,6 +92,9 @@ def _start_patching_global_vars(self):
8992
self.shutil_module._use_fd_functions = False
9093

9194
def _stop_patching_global_vars(self):
95+
self._patch_level -= 1
96+
if self._patch_level > 0:
97+
return # nested call - remains patched
9298
if self.has_fcopy_file:
9399
self.shutil_module._HAS_FCOPYFILE = True
94100
if self.use_copy_file_range:

pyfakefs/tests/fake_filesystem_shutil_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@ def test_copy(self):
212212
self.assertTrue(os.path.exists(dst_file))
213213
self.assertEqual(os.stat(src_file).st_mode, os.stat(dst_file).st_mode)
214214

215+
def test_copytree_with_copy_function(self):
216+
# regression test for #1235 (deadlock)
217+
source_dir = Path(self.make_path("source_dir"))
218+
target_dir = Path(self.make_path("target_dir"))
219+
test_contents = "Test contents"
220+
source_file = source_dir / "test.txt"
221+
target_file = target_dir / "test.txt"
222+
self.create_file(source_file, contents=test_contents)
223+
224+
shutil.copytree(source_dir, target_dir, copy_function=shutil.copy2)
225+
assert target_file.read_text() == test_contents
226+
215227
def test_permission_error_message(self):
216228
self.check_posix_only()
217229
dst_dir = Path(self.make_path("home1"))

0 commit comments

Comments
 (0)