]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-73991: Support copying directory symlinks on older Windows (#120807)
authorBarney Gale <barney.gale@gmail.com>
Wed, 3 Jul 2024 03:30:29 +0000 (04:30 +0100)
committerGitHub <noreply@github.com>
Wed, 3 Jul 2024 03:30:29 +0000 (04:30 +0100)
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.

Doc/library/pathlib.rst
Lib/pathlib/__init__.py
Lib/pathlib/_abc.py
Lib/pathlib/_local.py
Lib/pathlib/_os.py
Lib/test/test_pathlib/test_pathlib_abc.py

index 0918bbb47e9ea6001a237448c8ba7a3db346682b..d7fd56f4c4ff7fac3cabb1317f7a58c669b76926 100644 (file)
@@ -1554,11 +1554,6 @@ Copying, renaming and deleting
       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.
-
    .. versionadded:: 3.14
 
 
index 4b3edf535a61aafc35cb7f7d4ac149719a7fa8ac..2298a2495294600db41d5eefbaae212de8b0e9be 100644 (file)
@@ -5,8 +5,8 @@ paths with operations that have semantics appropriate for different
 operating systems.
 """
 
-from ._abc import *
+from ._os import *
 from ._local import *
 
-__all__ = (_abc.__all__ +
+__all__ = (_os.__all__ +
            _local.__all__)
index 7197391392116964a26b7ab766e1330145a7d720..b5f903ec1f03ce606508495837348dbc07200a0d 100644 (file)
@@ -16,10 +16,7 @@ import operator
 import posixpath
 from glob import _GlobberBase, _no_recurse_symlinks
 from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
-from ._os import copyfileobj
-
-
-__all__ = ["UnsupportedOperation"]
+from ._os import UnsupportedOperation, copyfileobj
 
 
 @functools.cache
@@ -27,12 +24,6 @@ def _is_case_sensitive(parser):
     return parser.normcase('Aa') == 'Aa'
 
 
-class UnsupportedOperation(NotImplementedError):
-    """An exception that is raised when an unsupported operation is called on
-    a path object.
-    """
-    pass
-
 
 class ParserBase:
     """Base class for path parsers, which do low-level path manipulation.
index 0105ea3042422e5453eb07938e56fea6552eaf5f..acb57214b81865128c9c910538c6065b44750b25 100644 (file)
@@ -17,8 +17,8 @@ try:
 except ImportError:
     grp = None
 
-from ._abc import UnsupportedOperation, PurePathBase, PathBase
-from ._os import copyfile
+from ._os import UnsupportedOperation, copyfile
+from ._abc import PurePathBase, PathBase
 
 
 __all__ = [
@@ -791,12 +791,15 @@ class Path(PathBase, PurePath):
             try:
                 target = os.fspath(target)
             except TypeError:
-                if isinstance(target, PathBase):
-                    # Target is an instance of PathBase but not os.PathLike.
-                    # Use generic implementation from PathBase.
-                    return PathBase.copy(self, target, follow_symlinks=follow_symlinks)
-                raise
-            copyfile(os.fspath(self), target, follow_symlinks)
+                if not isinstance(target, PathBase):
+                    raise
+            else:
+                try:
+                    copyfile(os.fspath(self), target, follow_symlinks)
+                    return
+                except UnsupportedOperation:
+                    pass  # Fall through to generic code.
+            PathBase.copy(self, target, follow_symlinks=follow_symlinks)
 
     def chmod(self, mode, *, follow_symlinks=True):
         """
index bbb019b65345035a7314568baf85937a1c752bf3..61923b5e410b5caee2d632c8bf364bea1373e816 100644 (file)
@@ -20,6 +20,15 @@ except ImportError:
     _winapi = None
 
 
+__all__ = ["UnsupportedOperation"]
+
+
+class UnsupportedOperation(NotImplementedError):
+    """An exception that is raised when an unsupported operation is attempted.
+    """
+    pass
+
+
 def get_copy_blocksize(infd):
     """Determine blocksize for fastcopying on Linux.
     Hopefully the whole file will be copied in a single call.
@@ -106,18 +115,30 @@ if _winapi and hasattr(_winapi, 'CopyFile2') and hasattr(os.stat_result, 'st_fil
         Copy from one file to another using CopyFile2 (Windows only).
         """
         if follow_symlinks:
-            flags = 0
+            _winapi.CopyFile2(source, target, 0)
         else:
+            # Use COPY_FILE_COPY_SYMLINK to copy a file symlink.
             flags = _winapi.COPY_FILE_COPY_SYMLINK
             try:
                 _winapi.CopyFile2(source, target, flags)
                 return
             except OSError as err:
                 # Check for ERROR_ACCESS_DENIED
-                if err.winerror != 5 or not _is_dirlink(source):
+                if err.winerror == 5 and _is_dirlink(source):
+                    pass
+                else:
                     raise
+
+            # Add COPY_FILE_DIRECTORY to copy a directory symlink.
             flags |= _winapi.COPY_FILE_DIRECTORY
-        _winapi.CopyFile2(source, target, flags)
+            try:
+                _winapi.CopyFile2(source, target, flags)
+            except OSError as err:
+                # Check for ERROR_INVALID_PARAMETER
+                if err.winerror == 87:
+                    raise UnsupportedOperation(err) from None
+                else:
+                    raise
 else:
     copyfile = None
 
index ad692e872ede0bb618d198568eb34da20d419f24..28c9664cc90fe1970fd99ec87413c7680d21f119 100644 (file)
@@ -5,7 +5,8 @@ import errno
 import stat
 import unittest
 
-from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase
+from pathlib._os import UnsupportedOperation
+from pathlib._abc import ParserBase, PurePathBase, PathBase
 import posixpath
 
 from test.support import is_wasi