Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 34k
GH-73991: Add follow_symlinks argument to pathlib.Path.copy()#120519
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
34a336538bf7910717792de5a230File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1432,17 +1432,26 @@ Creating files and directories | ||
| Copying, renaming and deleting | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| .. method:: Path.copy(target) | ||
| .. method:: Path.copy(target, *, follow_symlinks=True) | ||
| Copy the contents of this file to the *target* file. If *target* specifies | ||
| a file that already exists, it will be replaced. | ||
| If *follow_symlinks* is false, and this file is a symbolic link, *target* | ||
| will be created as a symbolic link. If *follow_symlinks* is true and this | ||
| file is a symbolic link, *target* will be a copy of the symlink target. | ||
| .. note:: | ||
| This method uses operating system functionality to copy file content | ||
| efficiently. The OS might also copy some metadata, such as file | ||
| permissions. After the copy is complete, users may wish to call | ||
| :meth:`Path.chmod` to set the permissions of the target file. | ||
| .. warning:: | ||
| On old builds of Windows (before Windows 10 build 19041), this method | ||
| raises :exc:`OSError` when a symlink to a directory is encountered and | ||
| *follow_symlinks* is false. | ||
Comment on lines +1450 to +1453 Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Failing to copy a symlink that targets a directory is disappointing. A symlink is never reported as a directory in Python, so code that copies a tree that contains directory symlinks is going to fail on Windows Server 2016 and Windows Server 2019 systems, with no obvious reason that a Python programmer would reasonably expect. A fallback path is needed to copy the link via ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should have waited for your feedback Eryk, my bad. I'll fix this in a follow-up PR. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As always, I defer to what you and Steve think is best. I'm of a mixed mind regarding Python's PEP 11 support policy that requires new versions of Python to support Server releases that still have extended support from Microsoft, which sets the bar for all versions of Windows. The Server editions are released in the LTSC channel and get 10 years of support. Server 2016 (build 14393) is supported until January 2027, and Server 2019 (build 17763) is supported until January 2029. Part of me wishes we just grouped the Server releases with corresponding releases in the general availability channel (e.g. Windows 10 1607 and Windows 10 1809) because it's more work to provide and maintain fallback paths for older versions of Windows. However, it's also great for users that they can use the latest version of Python on older systems. | ||
| .. versionadded:: 3.14 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1712,7 +1712,7 @@ def test_copy_directory(self): | ||
| source.copy(target) | ||
| @needs_symlinks | ||
| def test_copy_symlink(self): | ||
| def test_copy_symlink_follow_symlinks_true(self): | ||
| base = self.cls(self.base) | ||
| source = base / 'linkA' | ||
| target = base / 'copyA' | ||
| @@ -1721,6 +1721,26 @@ def test_copy_symlink(self): | ||
| self.assertFalse(target.is_symlink()) | ||
| self.assertEqual(source.read_text(), target.read_text()) | ||
| @needs_symlinks | ||
| def test_copy_symlink_follow_symlinks_false(self): | ||
| base = self.cls(self.base) | ||
| source = base / 'linkA' | ||
| target = base / 'copyA' | ||
| source.copy(target, follow_symlinks=False) | ||
| self.assertTrue(target.exists()) | ||
| self.assertTrue(target.is_symlink()) | ||
| self.assertEqual(source.readlink(), target.readlink()) | ||
barneygale marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| @needs_symlinks | ||
| def test_copy_directory_symlink_follow_symlinks_false(self): | ||
| base = self.cls(self.base) | ||
| source = base / 'linkB' | ||
| target = base / 'copyA' | ||
| source.copy(target, follow_symlinks=False) | ||
| self.assertTrue(target.exists()) | ||
| self.assertTrue(target.is_symlink()) | ||
| self.assertEqual(source.readlink(), target.readlink()) | ||
| def test_copy_to_existing_file(self): | ||
| base = self.cls(self.base) | ||
| source = base / 'fileA' | ||
| @@ -1749,6 +1769,19 @@ def test_copy_to_existing_symlink(self): | ||
| self.assertFalse(real_target.is_symlink()) | ||
| self.assertEqual(source.read_text(), real_target.read_text()) | ||
| @needs_symlinks | ||
| def test_copy_to_existing_symlink_follow_symlinks_false(self): | ||
| base = self.cls(self.base) | ||
| source = base / 'dirB' / 'fileB' | ||
| target = base / 'linkA' | ||
| real_target = base / 'fileA' | ||
| source.copy(target, follow_symlinks=False) | ||
| self.assertTrue(target.exists()) | ||
| self.assertTrue(target.is_symlink()) | ||
| self.assertTrue(real_target.exists()) | ||
| self.assertFalse(real_target.is_symlink()) | ||
| self.assertEqual(source.read_text(), real_target.read_text()) | ||
| def test_copy_empty(self): | ||
| base = self.cls(self.base) | ||
| source = base / 'empty' | ||
Uh oh!
There was an error while loading. Please reload this page.