Skip to content

GH-130614: pathlib ABCs: parametrize test suite for path copying #131168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Lib/pathlib/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def copy_file(source, target, follow_symlinks=True, preserve_metadata=False):
"""
info = source.info
if not follow_symlinks and info.is_symlink():
target.symlink_to(source.readlink(), info.is_dir())
target.symlink_to(str(source.readlink()), info.is_dir())
if preserve_metadata:
target._write_info(info, follow_symlinks=False)
elif info.is_dir():
Expand Down
172 changes: 172 additions & 0 deletions Lib/test/test_pathlib/test_copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""
Tests for copying from pathlib.types._ReadablePath to _WritablePath.
"""

import contextlib
import unittest

from pathlib import Path

from test.test_pathlib.support.local_path import LocalPathGround, WritableLocalPath
from test.test_pathlib.support.zip_path import ZipPathGround, ReadableZipPath, WritableZipPath


class CopyTestBase:
def setUp(self):
self.source_root = self.source_ground.setup()
self.source_ground.create_hierarchy(self.source_root)
self.target_root = self.target_ground.setup(local_suffix="_target")

def tearDown(self):
self.source_ground.teardown(self.source_root)
self.target_ground.teardown(self.target_root)

def test_copy_file(self):
source = self.source_root / 'fileA'
target = self.target_root / 'copyA'
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isfile(target))
self.assertEqual(self.source_ground.readbytes(source),
self.target_ground.readbytes(result))

def test_copy_file_empty(self):
source = self.source_root / 'empty'
target = self.target_root / 'copyA'
self.source_ground.create_file(source, b'')
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isfile(target))
self.assertEqual(self.target_ground.readbytes(result), b'')

def test_copy_file_to_existing_file(self):
source = self.source_root / 'fileA'
target = self.target_root / 'copyA'
self.target_ground.create_file(target, b'this is a copy\n')
with contextlib.ExitStack() as stack:
if isinstance(target, WritableZipPath):
stack.enter_context(self.assertWarns(UserWarning))
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isfile(target))
self.assertEqual(self.source_ground.readbytes(source),
self.target_ground.readbytes(result))

def test_copy_file_to_directory(self):
if not isinstance(self.target_root, WritableLocalPath):
self.skipTest('needs local target')
source = self.source_root / 'fileA'
target = self.target_root / 'copyA'
self.target_ground.create_dir(target)
self.assertRaises(OSError, source.copy, target)

def test_copy_file_to_itself(self):
source = self.source_root / 'fileA'
self.assertRaises(OSError, source.copy, source)
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)

def test_copy_dir(self):
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isdir(target))
self.assertTrue(self.target_ground.isfile(target / 'fileC'))
self.assertEqual(self.target_ground.readtext(target / 'fileC'), 'this is file C\n')
self.assertTrue(self.target_ground.isdir(target / 'dirD'))
self.assertTrue(self.target_ground.isfile(target / 'dirD' / 'fileD'))
self.assertEqual(self.target_ground.readtext(target / 'dirD' / 'fileD'), 'this is file D\n')

def test_copy_dir_follow_symlinks_true(self):
if not self.source_ground.can_symlink:
self.skipTest('needs symlink support on source')
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
self.source_ground.create_symlink(source / 'linkC', 'fileC')
self.source_ground.create_symlink(source / 'linkD', 'dirD')
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isdir(target))
self.assertFalse(self.target_ground.islink(target / 'linkC'))
self.assertTrue(self.target_ground.isfile(target / 'linkC'))
self.assertEqual(self.target_ground.readtext(target / 'linkC'), 'this is file C\n')
self.assertFalse(self.target_ground.islink(target / 'linkD'))
self.assertTrue(self.target_ground.isdir(target / 'linkD'))
self.assertTrue(self.target_ground.isfile(target / 'linkD' / 'fileD'))
self.assertEqual(self.target_ground.readtext(target / 'linkD' / 'fileD'), 'this is file D\n')

def test_copy_dir_follow_symlinks_false(self):
if not self.source_ground.can_symlink:
self.skipTest('needs symlink support on source')
if not self.target_ground.can_symlink:
self.skipTest('needs symlink support on target')
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
self.source_ground.create_symlink(source / 'linkC', 'fileC')
self.source_ground.create_symlink(source / 'linkD', 'dirD')
result = source.copy(target, follow_symlinks=False)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isdir(target))
self.assertTrue(self.target_ground.islink(target / 'linkC'))
self.assertEqual(self.target_ground.readlink(target / 'linkC'), 'fileC')
self.assertTrue(self.target_ground.islink(target / 'linkD'))
self.assertEqual(self.target_ground.readlink(target / 'linkD'), 'dirD')

def test_copy_dir_to_existing_directory(self):
if not isinstance(self.target_root, WritableLocalPath):
self.skipTest('needs local target')
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
self.target_ground.create_dir(target)
self.assertRaises(FileExistsError, source.copy, target)

def test_copy_dir_to_itself(self):
source = self.source_root / 'dirC'
self.assertRaises(OSError, source.copy, source)
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)

def test_copy_dir_into_itself(self):
source = self.source_root / 'dirC'
target = self.source_root / 'dirC' / 'dirD' / 'copyC'
self.assertRaises(OSError, source.copy, target)
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)

def test_copy_into(self):
source = self.source_root / 'fileA'
target_dir = self.target_root / 'dirA'
self.target_ground.create_dir(target_dir)
result = source.copy_into(target_dir)
self.assertEqual(result, target_dir / 'fileA')
self.assertTrue(self.target_ground.isfile(result))
self.assertEqual(self.source_ground.readbytes(source),
self.target_ground.readbytes(result))

def test_copy_into_empty_name(self):
source = self.source_root.with_segments()
target_dir = self.target_root / 'dirA'
self.target_ground.create_dir(target_dir)
self.assertRaises(ValueError, source.copy_into, target_dir)


class ZipToZipPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = ZipPathGround(ReadableZipPath)
target_ground = ZipPathGround(WritableZipPath)


class ZipToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = ZipPathGround(ReadableZipPath)
target_ground = LocalPathGround(Path)


class LocalToZipPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = ZipPathGround(WritableZipPath)


class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = LocalPathGround(Path)


if __name__ == "__main__":
unittest.main()
138 changes: 0 additions & 138 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,144 +343,6 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
cls = DummyRWPath
can_symlink = False

def test_copy_file(self):
base = self.cls(self.base)
source = base / 'fileA'
target = base / 'copyA'
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(result.info.exists())
self.assertEqual(source.read_text(), result.read_text())

def test_copy_file_to_existing_file(self):
base = self.cls(self.base)
source = base / 'fileA'
target = base / 'dirB' / 'fileB'
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(result.info.exists())
self.assertEqual(source.read_text(), result.read_text())

def test_copy_file_to_existing_directory(self):
base = self.cls(self.base)
source = base / 'fileA'
target = base / 'dirA'
self.assertRaises(OSError, source.copy, target)

def test_copy_file_empty(self):
base = self.cls(self.base)
source = base / 'empty'
target = base / 'copyA'
source.write_bytes(b'')
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(result.info.exists())
self.assertEqual(result.read_bytes(), b'')

def test_copy_file_to_itself(self):
base = self.cls(self.base)
source = base / 'empty'
source.write_bytes(b'')
self.assertRaises(OSError, source.copy, source)
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)

def test_copy_dir_simple(self):
base = self.cls(self.base)
source = base / 'dirC'
target = base / 'copyC'
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(result.info.is_dir())
self.assertTrue(result.joinpath('dirD').info.is_dir())
self.assertTrue(result.joinpath('dirD', 'fileD').info.is_file())
self.assertEqual(result.joinpath('dirD', 'fileD').read_text(),
"this is file D\n")
self.assertTrue(result.joinpath('fileC').info.is_file())
self.assertTrue(result.joinpath('fileC').read_text(),
"this is file C\n")

def test_copy_dir_complex(self, follow_symlinks=True):
def ordered_walk(path):
for dirpath, dirnames, filenames in path.walk(follow_symlinks=follow_symlinks):
dirnames.sort()
filenames.sort()
yield dirpath, dirnames, filenames
base = self.cls(self.base)
source = base / 'dirC'

if self.can_symlink:
# Add some symlinks
source.joinpath('linkC').symlink_to('fileC')
source.joinpath('linkD').symlink_to('dirD', target_is_directory=True)

# Perform the copy
target = base / 'copyC'
result = source.copy(target, follow_symlinks=follow_symlinks)
self.assertEqual(result, target)

# Compare the source and target trees
source_walk = ordered_walk(source)
target_walk = ordered_walk(result)
for source_item, target_item in zip(source_walk, target_walk, strict=True):
self.assertEqual(source_item[0].parts[len(source.parts):],
target_item[0].parts[len(target.parts):]) # dirpath
self.assertEqual(source_item[1], target_item[1]) # dirnames
self.assertEqual(source_item[2], target_item[2]) # filenames
# Compare files and symlinks
for filename in source_item[2]:
source_file = source_item[0].joinpath(filename)
target_file = target_item[0].joinpath(filename)
if follow_symlinks or not source_file.info.is_symlink():
# Regular file.
self.assertEqual(source_file.read_bytes(), target_file.read_bytes())
elif source_file.info.is_dir():
# Symlink to directory.
self.assertTrue(target_file.info.is_dir())
self.assertEqual(source_file.readlink(), target_file.readlink())
else:
# Symlink to file.
self.assertEqual(source_file.read_bytes(), target_file.read_bytes())
self.assertEqual(source_file.readlink(), target_file.readlink())

def test_copy_dir_complex_follow_symlinks_false(self):
self.test_copy_dir_complex(follow_symlinks=False)

def test_copy_dir_to_existing_directory(self):
base = self.cls(self.base)
source = base / 'dirC'
target = base / 'copyC'
target.mkdir()
target.joinpath('dirD').mkdir()
self.assertRaises(FileExistsError, source.copy, target)

def test_copy_dir_to_itself(self):
base = self.cls(self.base)
source = base / 'dirC'
self.assertRaises(OSError, source.copy, source)
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)

def test_copy_dir_into_itself(self):
base = self.cls(self.base)
source = base / 'dirC'
target = base / 'dirC' / 'dirD' / 'copyC'
self.assertRaises(OSError, source.copy, target)
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
self.assertFalse(target.info.exists())

def test_copy_into(self):
base = self.cls(self.base)
source = base / 'fileA'
target_dir = base / 'dirA'
result = source.copy_into(target_dir)
self.assertEqual(result, target_dir / 'fileA')
self.assertTrue(result.info.exists())
self.assertEqual(source.read_text(), result.read_text())

def test_copy_into_empty_name(self):
source = self.cls('')
target_dir = self.base
self.assertRaises(ValueError, source.copy_into, target_dir)


class ReadablePathWalkTest(unittest.TestCase):
cls = DummyReadablePath
Expand Down
Loading