Skip to content

Commit 1249ae6

Browse files
committed
Add support for Python 3.14 pathlib copy methods
- support pathlib.copy and pathlib.copy_into - add tests for pathlib.move and pathlib move_into
1 parent 0ee3516 commit 1249ae6

File tree

3 files changed

+133
-2
lines changed

3 files changed

+133
-2
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The released versions correspond to PyPI releases.
2222

2323
### Enhancements
2424
* added support for `os.readinto` in Python 3.14
25+
* added support for `pathlib.copy` and `pathlib.copy_into` in Python 3.14
2526

2627
### Fixes
2728
* fixes patching of Debian-specific `tempfile` in Python 3.13 (see [#1214](../../issues/1214))

pyfakefs/fake_pathlib.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,35 @@ def touch(self, mode=0o666, exist_ok=True):
845845
fake_file.close()
846846
self.chmod(mode)
847847

848+
if sys.version_info >= (3, 14):
849+
850+
def _copy_from_file(self, source, preserve_metadata=False):
851+
if sys.platform == "win32":
852+
# do not use the optimized version that uses OS functions in Windows
853+
if hasattr(self, "_copy_from_file_fallback"):
854+
return self._copy_from_file_fallback(source, preserve_metadata)
855+
else:
856+
return super()._copy_from_file(source, preserve_metadata) # type: ignore[attr-defined]
857+
else:
858+
# make pathlib think that no system calls are available so that it
859+
# will fall back to pure Python calls
860+
pathlib_os = pathlib.pathlib_module._os # type: ignore[attr-defined]
861+
old_fcopyfile = pathlib_os._fcopyfile
862+
pathlib_os._fcopyfile = None
863+
old_copy_file_range = pathlib_os._copy_file_range # type: ignore[attr-defined]
864+
pathlib_os._copy_file_range = None
865+
old_ficlone = pathlib_os._ficlone # type: ignore[attr-defined]
866+
pathlib_os._ficlone = None
867+
old_sendfile = pathlib_os._sendfile # type: ignore[attr-defined]
868+
pathlib_os._sendfile = None
869+
try:
870+
return super()._copy_from_file(source, preserve_metadata) # type: ignore[attr-defined]
871+
finally:
872+
pathlib_os._fcopyfile = old_fcopyfile
873+
pathlib_os._copy_file_range = old_copy_file_range
874+
pathlib_os._ficlone = old_ficlone
875+
pathlib_os._sendfile = old_sendfile
876+
848877

849878
def _warn_is_reserved_deprecated():
850879
if sys.version_info >= (3, 13):
@@ -875,7 +904,7 @@ def __init__(self, filesystem, from_patcher=False):
875904
Will be set to `True` if instantiated from `Patcher`.
876905
"""
877906
init_module(filesystem)
878-
self._pathlib_module = pathlib
907+
self.pathlib_module = pathlib
879908
self._os = None
880909
self._os_patcher = None
881910
if not from_patcher:
@@ -998,7 +1027,7 @@ def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
9981027

9991028
def __getattr__(self, name):
10001029
"""Forwards any unfaked calls to the standard pathlib module."""
1001-
return getattr(self._pathlib_module, name)
1030+
return getattr(self.pathlib_module, name)
10021031

10031032

10041033
class FakePathlibPathModule:

pyfakefs/tests/fake_pathlib_test.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,6 +1376,107 @@ def test_owner_and_group_windows(self):
13761376
with self.assertRaises(NotImplementedError):
13771377
self.path(path).group()
13781378

1379+
@unittest.skipIf(sys.version_info < (3, 14), "copy() is new in Python 3.14")
1380+
def test_copy_new_file(self):
1381+
source_path = self.path(self.make_path("some_file"))
1382+
target_path = self.make_path("some_other_file")
1383+
self.create_file(source_path, contents="test")
1384+
target = source_path.copy(target_path)
1385+
self.assertEqual(target_path, str(target))
1386+
self.assertTrue(target.exists())
1387+
self.assertEqual("test", target.read_text())
1388+
1389+
@unittest.skipIf(sys.version_info < (3, 14), "copy() is new in Python 3.14")
1390+
def test_copy_to_existing_file(self):
1391+
source_path = self.path(self.make_path("some_file"))
1392+
target_path = self.make_path("some_other_file")
1393+
self.create_file(source_path, contents="foo")
1394+
self.create_file(target_path, contents="bar")
1395+
target = source_path.copy(target_path)
1396+
self.assertEqual(target_path, str(target))
1397+
self.assertTrue(source_path.exists())
1398+
self.assertTrue(target.exists())
1399+
self.assertEqual("foo", target.read_text())
1400+
1401+
@unittest.skipIf(sys.version_info < (3, 14), "copy() is new in Python 3.14")
1402+
def test_copy_directory(self):
1403+
source_path = self.path(self.make_path("some_dir"))
1404+
self.create_file(source_path / "foo", contents="foo")
1405+
self.create_file(source_path / "bar", contents="bar")
1406+
self.create_dir(source_path / "dir")
1407+
target_path = self.make_path("new_dir")
1408+
target = source_path.copy(target_path)
1409+
self.assertEqual(target_path, str(target))
1410+
self.assertTrue(source_path.exists())
1411+
self.assertTrue(target.exists())
1412+
self.assertTrue((target / "foo").exists())
1413+
self.assertEqual("foo", (target / "foo").read_text())
1414+
self.assertEqual("bar", (target / "bar").read_text())
1415+
1416+
@unittest.skipIf(sys.version_info < (3, 14), "copy_into() is new in Python 3.14")
1417+
def test_copy_into(self):
1418+
source_dir = self.path(self.make_path("some_dir"))
1419+
source_path = source_dir / "foo"
1420+
self.create_file(source_path, contents="foo")
1421+
target_path = self.path(self.make_path("new_dir"))
1422+
self.create_dir(target_path)
1423+
target = source_path.copy_into(target_path)
1424+
self.assertTrue(source_path.exists())
1425+
self.assertTrue(target.exists())
1426+
self.assertEqual(str(target_path / "foo"), str(target))
1427+
self.assertEqual("foo", target.read_text())
1428+
1429+
@unittest.skipIf(sys.version_info < (3, 14), "move() is new in Python 3.14")
1430+
def test_move_file(self):
1431+
source_path = self.path(self.make_path("some_file"))
1432+
target_path = self.make_path("some_other_file")
1433+
self.create_file(source_path, contents="test")
1434+
target = source_path.move(target_path)
1435+
self.assertEqual(target_path, str(target))
1436+
self.assertFalse(source_path.exists())
1437+
self.assertTrue(target.exists())
1438+
self.assertEqual("test", target.read_text())
1439+
1440+
@unittest.skipIf(sys.version_info < (3, 14), "move() is new in Python 3.14")
1441+
def test_move_to_existing_file(self):
1442+
source_path = self.path(self.make_path("some_file"))
1443+
target_path = self.make_path("some_other_file")
1444+
self.create_file(source_path, contents="foo")
1445+
self.create_file(target_path, contents="bar")
1446+
target = source_path.move(target_path)
1447+
self.assertEqual(target_path, str(target))
1448+
self.assertFalse(source_path.exists())
1449+
self.assertTrue(target.exists())
1450+
self.assertEqual("foo", target.read_text())
1451+
1452+
@unittest.skipIf(sys.version_info < (3, 14), "move() is new in Python 3.14")
1453+
def test_move_directory(self):
1454+
source_path = self.path(self.make_path("some_dir"))
1455+
self.create_file(source_path / "foo", contents="foo")
1456+
self.create_file(source_path / "bar", contents="bar")
1457+
self.create_dir(source_path / "dir")
1458+
target_path = self.make_path("new_dir")
1459+
target = source_path.move(target_path)
1460+
self.assertEqual(target_path, str(target))
1461+
self.assertFalse(source_path.exists())
1462+
self.assertTrue(target.exists())
1463+
self.assertTrue((target / "foo").exists())
1464+
self.assertEqual("foo", (target / "foo").read_text())
1465+
self.assertEqual("bar", (target / "bar").read_text())
1466+
1467+
@unittest.skipIf(sys.version_info < (3, 14), "move_into() is new in Python 3.14")
1468+
def test_move_into(self):
1469+
source_dir = self.path(self.make_path("some_dir"))
1470+
source_path = source_dir / "foo"
1471+
self.create_file(source_path, contents="foo")
1472+
target_path = self.path(self.make_path("new_dir"))
1473+
self.create_dir(target_path)
1474+
target = source_path.move_into(target_path)
1475+
self.assertFalse(source_path.exists())
1476+
self.assertTrue(target.exists())
1477+
self.assertEqual(str(target_path / "foo"), str(target))
1478+
self.assertEqual("foo", target.read_text())
1479+
13791480
def test_walk(self):
13801481
"""Regression test for #915 - walk results shall be strings."""
13811482
base_dir = self.make_path("foo")

0 commit comments

Comments
 (0)