Skip to content

Commit f09d184

Browse files
authored
GH-73991: Support copying directory symlinks on older Windows (#120807)
Check for `ERROR_INVALID_PARAMETER` when calling `_winapi.CopyFile2()` and raise `UnsupportedOperation`. In `Path.copy()`, handle this exception and fall back to the `PathBase.copy()` implementation.
1 parent 0898354 commit f09d184

File tree

6 files changed

+40
-29
lines changed

6 files changed

+40
-29
lines changed

Doc/library/pathlib.rst

-5
Original file line numberDiff line numberDiff line change
@@ -1554,11 +1554,6 @@ Copying, renaming and deleting
15541554
permissions. After the copy is complete, users may wish to call
15551555
:meth:`Path.chmod` to set the permissions of the target file.
15561556

1557-
.. warning::
1558-
On old builds of Windows (before Windows 10 build 19041), this method
1559-
raises :exc:`OSError` when a symlink to a directory is encountered and
1560-
*follow_symlinks* is false.
1561-
15621557
.. versionadded:: 3.14
15631558

15641559

Lib/pathlib/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
operating systems.
66
"""
77

8-
from ._abc import *
8+
from ._os import *
99
from ._local import *
1010

11-
__all__ = (_abc.__all__ +
11+
__all__ = (_os.__all__ +
1212
_local.__all__)

Lib/pathlib/_abc.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,14 @@
1616
import posixpath
1717
from glob import _GlobberBase, _no_recurse_symlinks
1818
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
19-
from ._os import copyfileobj
20-
21-
22-
__all__ = ["UnsupportedOperation"]
19+
from ._os import UnsupportedOperation, copyfileobj
2320

2421

2522
@functools.cache
2623
def _is_case_sensitive(parser):
2724
return parser.normcase('Aa') == 'Aa'
2825

2926

30-
class UnsupportedOperation(NotImplementedError):
31-
"""An exception that is raised when an unsupported operation is called on
32-
a path object.
33-
"""
34-
pass
35-
3627

3728
class ParserBase:
3829
"""Base class for path parsers, which do low-level path manipulation.

Lib/pathlib/_local.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
except ImportError:
1818
grp = None
1919

20-
from ._abc import UnsupportedOperation, PurePathBase, PathBase
21-
from ._os import copyfile
20+
from ._os import UnsupportedOperation, copyfile
21+
from ._abc import PurePathBase, PathBase
2222

2323

2424
__all__ = [
@@ -791,12 +791,15 @@ def copy(self, target, follow_symlinks=True):
791791
try:
792792
target = os.fspath(target)
793793
except TypeError:
794-
if isinstance(target, PathBase):
795-
# Target is an instance of PathBase but not os.PathLike.
796-
# Use generic implementation from PathBase.
797-
return PathBase.copy(self, target, follow_symlinks=follow_symlinks)
798-
raise
799-
copyfile(os.fspath(self), target, follow_symlinks)
794+
if not isinstance(target, PathBase):
795+
raise
796+
else:
797+
try:
798+
copyfile(os.fspath(self), target, follow_symlinks)
799+
return
800+
except UnsupportedOperation:
801+
pass # Fall through to generic code.
802+
PathBase.copy(self, target, follow_symlinks=follow_symlinks)
800803

801804
def chmod(self, mode, *, follow_symlinks=True):
802805
"""

Lib/pathlib/_os.py

+24-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
_winapi = None
2121

2222

23+
__all__ = ["UnsupportedOperation"]
24+
25+
26+
class UnsupportedOperation(NotImplementedError):
27+
"""An exception that is raised when an unsupported operation is attempted.
28+
"""
29+
pass
30+
31+
2332
def get_copy_blocksize(infd):
2433
"""Determine blocksize for fastcopying on Linux.
2534
Hopefully the whole file will be copied in a single call.
@@ -106,18 +115,30 @@ def copyfile(source, target, follow_symlinks):
106115
Copy from one file to another using CopyFile2 (Windows only).
107116
"""
108117
if follow_symlinks:
109-
flags = 0
118+
_winapi.CopyFile2(source, target, 0)
110119
else:
120+
# Use COPY_FILE_COPY_SYMLINK to copy a file symlink.
111121
flags = _winapi.COPY_FILE_COPY_SYMLINK
112122
try:
113123
_winapi.CopyFile2(source, target, flags)
114124
return
115125
except OSError as err:
116126
# Check for ERROR_ACCESS_DENIED
117-
if err.winerror != 5 or not _is_dirlink(source):
127+
if err.winerror == 5 and _is_dirlink(source):
128+
pass
129+
else:
118130
raise
131+
132+
# Add COPY_FILE_DIRECTORY to copy a directory symlink.
119133
flags |= _winapi.COPY_FILE_DIRECTORY
120-
_winapi.CopyFile2(source, target, flags)
134+
try:
135+
_winapi.CopyFile2(source, target, flags)
136+
except OSError as err:
137+
# Check for ERROR_INVALID_PARAMETER
138+
if err.winerror == 87:
139+
raise UnsupportedOperation(err) from None
140+
else:
141+
raise
121142
else:
122143
copyfile = None
123144

Lib/test/test_pathlib/test_pathlib_abc.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import stat
66
import unittest
77

8-
from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase
8+
from pathlib._os import UnsupportedOperation
9+
from pathlib._abc import ParserBase, PurePathBase, PathBase
910
import posixpath
1011

1112
from test.support import is_wasi

0 commit comments

Comments
 (0)