diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 1527deb..bac55ab 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -74,6 +74,11 @@ Directory and files operations Raise :exc:`SameFileError` instead of :exc:`Error`. Since the former is a subclass of the latter, this change is backward compatible. + .. versionchanged:: 3.8 + Use platform-specific zero-copy syscalls on Linux, OSX and Windows in + order to copy the file more efficiently. + If the zero-copy operation fails and no data was written in *dst* + silently fallback on using less efficient :func:`copyfileobj` internally. .. exception:: SameFileError @@ -163,6 +168,12 @@ Directory and files operations Added *follow_symlinks* argument. Now returns path to the newly created file. + .. versionchanged:: 3.8 + Use platform-specific zero-copy syscalls on Linux, OSX and Windows in + order to copy the file more efficiently. + If the zero-copy operation fails and no data was written in *dst* + silently fallback on using less efficient :func:`copyfileobj` internally. + .. function:: copy2(src, dst, *, follow_symlinks=True) Identical to :func:`~shutil.copy` except that :func:`copy2` @@ -185,6 +196,12 @@ Directory and files operations file system attributes too (currently Linux only). Now returns path to the newly created file. + .. versionchanged:: 3.8 + Use platform-specific zero-copy syscalls on Linux, OSX and Windows in + order to copy the file more efficiently. + If the zero-copy operation fails and no data was written in *dst* + silently fallback on using less efficient :func:`copyfileobj` internally. + .. function:: ignore_patterns(\*patterns) This factory function creates a function that can be used as a callable for @@ -241,6 +258,11 @@ Directory and files operations Added the *ignore_dangling_symlinks* argument to silent dangling symlinks errors when *symlinks* is false. + .. versionchanged:: 3.8 + Use platform-specific zero-copy syscalls on Linux, OSX and Windows in + order to copy the file more efficiently. + If the zero-copy operation fails and no data was written in *dst* + silently fallback on using less efficient :func:`copyfileobj` internally. .. function:: rmtree(path, ignore_errors=False, onerror=None) @@ -314,6 +336,12 @@ Directory and files operations .. versionchanged:: 3.5 Added the *copy_function* keyword argument. + .. versionchanged:: 3.8 + Use platform-specific zero-copy syscalls on Linux, OSX and Windows in + order to copy the file more efficiently. + If the zero-copy operation fails and no data was written in *dst* + silently fallback on using less efficient :func:`copyfileobj` internally. + .. function:: disk_usage(path) Return disk usage statistics about the given path as a :term:`named tuple` @@ -656,4 +684,3 @@ Querying the size of the output terminal .. _`Other Environment Variables`: http://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html#tag_002_003 - diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 9aad908..8239643 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -90,10 +90,15 @@ New Modules Improved Modules ================ - Optimizations ============= +* :func:`shutil.copyfile`, :func:`shutil.copy`, :func:`shutil.copy2`, + :func:`shutil.copytree` and :func:`shutil.move` use platform specific + zero-copy syscalls on Linux, OSX and Windows in order to copy the file more + efficiently. The average speedup for copying a 512MB file is +24% on Linux, + +50% on OSX and +48% on Windows. + * The default protocol in the :mod:`pickle` module is now Protocol 4, first introduced in Python 3.4. It offers better performance and smaller size compared to Protocol 3 available since Python 3.0. diff --git a/Lib/shutil.py b/Lib/shutil.py index 3c02776..19026e5 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -42,6 +42,19 @@ try: except ImportError: getgrnam = None +try: + import posix +except ImportError: + posix = None + +try: + import nt +except ImportError: + nt = None + +_HAS_SENDFILE = hasattr(os, "sendfile") +_HAS_FCOPYFILE = hasattr(posix, "_fcopyfile") + __all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", "copytree", "move", "rmtree", "Error", "SpecialFileError", "ExecError", "make_archive", "get_archive_formats", @@ -72,6 +85,10 @@ class RegistryError(Exception): """Raised when a registry operation with the archiving and unpacking registries fails""" +class _GiveupOnZeroCopy(Exception): + """Raised as a signal to fallback on using raw file copy + when zero-copy functions fail to do so. + """ def copyfileobj(fsrc, fdst, length=16*1024): """copy data from file-like object fsrc to file-like object fdst""" @@ -81,6 +98,104 @@ def copyfileobj(fsrc, fdst, length=16*1024): break fdst.write(buf) +def _zerocopy_osx(fsrc, fdst): + """Copy 2 regular mmap-like files by using high-performance + fcopyfile() syscall (OSX only). + """ + try: + infd = fsrc.fileno() + outfd = fdst.fileno() + except Exception as err: + raise _GiveupOnZeroCopy(err) # not a regular file + + try: + posix._fcopyfile(infd, outfd) + except OSError as err: + if err.errno in {errno.EINVAL, errno.ENOTSUP}: + raise _GiveupOnZeroCopy(err) + else: + raise err from None + +def _zerocopy_win(fsrc, fdst): + """Copy 2 files by using high-performance CopyFileW (Windows only).""" + nt._win32copyfile(fsrc, fdst) + +def _zerocopy_sendfile(fsrc, fdst): + """Copy data from one regular mmap-like fd to another by using + high-performance sendfile() method. + This should work on Linux >= 2.6.33 and Solaris only. + """ + global _HAS_SENDFILE + try: + infd = fsrc.fileno() + outfd = fdst.fileno() + except Exception as err: + raise _GiveupOnZeroCopy(err) # not a regular file + + # Hopefully the whole file will be copied in a single call. + # sendfile() is called in a loop 'till EOF is reached (0 return) + # so a bufsize smaller or bigger than the actual file size + # should not make any difference, also in case the file content + # changes while being copied. + try: + blocksize = max(os.fstat(infd).st_size, 2 ** 23) # min 8MB + except Exception: + blocksize = 2 ** 27 # 128MB + + offset = 0 + while True: + try: + sent = os.sendfile(outfd, infd, offset, blocksize) + except OSError as err: + if err.errno == errno.ENOTSOCK: + # sendfile() on this platform (probably Linux < 2.6.33) + # does not support copies between regular files (only + # sockets). + _HAS_SENDFILE = False + raise _GiveupOnZeroCopy(err) + + if err.errno == errno.ENOSPC: # filesystem is full + raise err from None + + # Give up on first call and if no data was copied. + if offset == 0 and os.lseek(outfd, 0, os.SEEK_CUR) == 0: + raise _GiveupOnZeroCopy(err) + + raise err from None + else: + if sent == 0: + break # EOF + offset += sent + +def _copyfileobj2(fsrc, fdst): + """Copy 2 regular mmap-like fds by using zero-copy sendfile(2) + (Linux) and fcopyfile(2) (OSX) syscalls. + In case of error fallback on using plain read()/write() if no + data was copied. + """ + # Note: copyfileobj() is left alone in order to not introduce any + # unexpected breakage. Possible risks by using zero-copy calls + # in copyfileobj() are: + # - fdst cannot be open in "a"(ppend) mode + # - fsrc and fdst may be open in "t"(ext) mode + # - fsrc may be a BufferedReader (which hides unread data in a buffer), + # GzipFile (which decompresses data), HTTPResponse (which decodes + # chunks). + # - possibly others + if _HAS_SENDFILE: + try: + return _zerocopy_sendfile(fsrc, fdst) + except _GiveupOnZeroCopy: + pass + + if _HAS_FCOPYFILE: + try: + return _zerocopy_osx(fsrc, fdst) + except _GiveupOnZeroCopy: + pass + + return copyfileobj(fsrc, fdst) + def _samefile(src, dst): # Macintosh, Unix. if hasattr(os.path, 'samefile'): @@ -117,9 +232,13 @@ def copyfile(src, dst, *, follow_symlinks=True): if not follow_symlinks and os.path.islink(src): os.symlink(os.readlink(src), dst) else: + if os.name == 'nt': + _zerocopy_win(src, dst) + return dst + with open(src, 'rb') as fsrc: with open(dst, 'wb') as fdst: - copyfileobj(fsrc, fdst) + _copyfileobj2(fsrc, fdst) return dst def copymode(src, dst, *, follow_symlinks=True): diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 2cb2f14..0e0cf2d 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -12,19 +12,28 @@ import errno import functools import pathlib import subprocess +import random +import string +import contextlib +import io from shutil import (make_archive, register_archive_format, unregister_archive_format, get_archive_formats, Error, unpack_archive, register_unpack_format, RegistryError, unregister_unpack_format, get_unpack_formats, - SameFileError) + SameFileError, _GiveupOnZeroCopy) import tarfile import zipfile +try: + import posix +except ImportError: + posix = None from test import support from test.support import TESTFN, FakePath TESTFN2 = TESTFN + "2" +HAS_OSX_ZEROCOPY = hasattr(posix, "_fcopyfile") try: import grp @@ -60,6 +69,24 @@ def write_file(path, content, binary=False): with open(path, 'wb' if binary else 'w') as fp: fp.write(content) +def write_test_file(path, size): + """Create a test file with an arbitrary size and random text content.""" + def chunks(total, step): + assert total >= step + while total > step: + yield step + total -= step + if total: + yield total + + bufsize = min(size, 8192) + chunk = b"".join([random.choice(string.ascii_letters).encode() + for i in range(bufsize)]) + with open(path, 'wb') as f: + for csize in chunks(size, bufsize): + f.write(chunk) + assert os.path.getsize(path) == size + def read_file(path, binary=False): """Return contents from a file located at *path*. @@ -84,6 +111,37 @@ def rlistdir(path): res.append(name) return res +def supports_file2file_sendfile(): + # ...apparently Linux is the only one. + if not hasattr(os, "sendfile"): + return False + srcname = None + dstname = None + try: + with tempfile.NamedTemporaryFile("wb", delete=False) as f: + srcname = f.name + f.write(b"0123456789") + + with open(srcname, "rb") as src: + with tempfile.NamedTemporaryFile("wb", delete=False) as dst: + dstname = f.name + infd = src.fileno() + outfd = dst.fileno() + try: + os.sendfile(outfd, infd, 0, 2) + except OSError: + return False + else: + return True + finally: + if srcname is not None: + support.unlink(srcname) + if dstname is not None: + support.unlink(dstname) + + +SUPPORTS_SENDFILE = supports_file2file_sendfile() + class TestShutil(unittest.TestCase): @@ -1749,6 +1807,7 @@ class TestCopyFile(unittest.TestCase): self.assertRaises(OSError, shutil.copyfile, 'srcfile', 'destfile') + @unittest.skipIf(os.name == 'nt', "not POSIX") def test_w_dest_open_fails(self): srcfile = self.Faux() @@ -1768,6 +1827,7 @@ class TestCopyFile(unittest.TestCase): self.assertEqual(srcfile._exited_with[1].args, ('Cannot open "destfile"',)) + @unittest.skipIf(os.name == 'nt', "not POSIX") def test_w_dest_close_fails(self): srcfile = self.Faux() @@ -1790,6 +1850,7 @@ class TestCopyFile(unittest.TestCase): self.assertEqual(srcfile._exited_with[1].args, ('Cannot close',)) + @unittest.skipIf(os.name == 'nt', "not POSIX") def test_w_source_close_fails(self): srcfile = self.Faux(True) @@ -1829,6 +1890,205 @@ class TestCopyFile(unittest.TestCase): finally: os.rmdir(dst_dir) + +class _ZeroCopyFileTest(object): + """Tests common to all zero-copy APIs.""" + FILESIZE = (10 * 1024 * 1024) # 10 MiB + FILEDATA = b"" + PATCHPOINT = "" + + @classmethod + def setUpClass(cls): + write_test_file(TESTFN, cls.FILESIZE) + with open(TESTFN, 'rb') as f: + cls.FILEDATA = f.read() + assert len(cls.FILEDATA) == cls.FILESIZE + + @classmethod + def tearDownClass(cls): + support.unlink(TESTFN) + + def tearDown(self): + support.unlink(TESTFN2) + + @contextlib.contextmanager + def get_files(self): + with open(TESTFN, "rb") as src: + with open(TESTFN2, "wb") as dst: + yield (src, dst) + + def zerocopy_fun(self, *args, **kwargs): + raise NotImplementedError("must be implemented in subclass") + + # --- + + def test_regular_copy(self): + with self.get_files() as (src, dst): + self.zerocopy_fun(src, dst) + self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) + + @unittest.skipIf(os.name == 'nt', 'POSIX only') + def test_non_regular_file_src(self): + with io.BytesIO(self.FILEDATA) as src: + with open(TESTFN2, "wb") as dst: + with self.assertRaises(_GiveupOnZeroCopy): + self.zerocopy_fun(src, dst) + shutil.copyfileobj(src, dst) + + self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) + + @unittest.skipIf(os.name == 'nt', 'POSIX only') + def test_non_regular_file_dst(self): + with open(TESTFN, "rb") as src: + with io.BytesIO() as dst: + with self.assertRaises(_GiveupOnZeroCopy): + self.zerocopy_fun(src, dst) + shutil.copyfileobj(src, dst) + dst.seek(0) + self.assertEqual(dst.read(), self.FILEDATA) + + def test_non_existent_src(self): + name = tempfile.mktemp() + with self.assertRaises(FileNotFoundError) as cm: + shutil.copyfile(name, "new") + self.assertEqual(cm.exception.filename, name) + + def test_empty_file(self): + srcname = TESTFN + 'src' + dstname = TESTFN + 'dst' + self.addCleanup(lambda: support.unlink(srcname)) + self.addCleanup(lambda: support.unlink(dstname)) + with open(srcname, "wb"): + pass + + with open(srcname, "rb") as src: + with open(dstname, "wb") as dst: + self.zerocopy_fun(src, dst) + + self.assertEqual(read_file(dstname, binary=True), b"") + + def test_unhandled_exception(self): + with unittest.mock.patch(self.PATCHPOINT, + side_effect=ZeroDivisionError): + self.assertRaises(ZeroDivisionError, + shutil.copyfile, TESTFN, TESTFN2) + + @unittest.skipIf(os.name == 'nt', 'POSIX only') + def test_exception_on_first_call(self): + # Emulate a case where the first call to the zero-copy + # function raises an exception in which case the function is + # supposed to give up immediately. + with unittest.mock.patch(self.PATCHPOINT, + side_effect=OSError(errno.EINVAL, "yo")): + with self.get_files() as (src, dst): + with self.assertRaises(_GiveupOnZeroCopy): + self.zerocopy_fun(src, dst) + + def test_filesystem_full(self): + # Emulate a case where filesystem is full and sendfile() fails + # on first call. + with unittest.mock.patch(self.PATCHPOINT, + side_effect=OSError(errno.ENOSPC, "yo")): + with self.get_files() as (src, dst): + self.assertRaises(OSError, self.zerocopy_fun, src, dst) + + +@unittest.skipIf(not SUPPORTS_SENDFILE, 'os.sendfile() not supported') +class TestZeroCopySendfile(_ZeroCopyFileTest, unittest.TestCase): + PATCHPOINT = "os.sendfile" + + def zerocopy_fun(self, *args, **kwargs): + return shutil._zerocopy_sendfile(*args, **kwargs) + + def test_exception_on_second_call(self): + def sendfile(*args, **kwargs): + if not flag: + flag.append(None) + return orig_sendfile(*args, **kwargs) + else: + raise OSError(errno.EBADF, "yo") + + flag = [] + orig_sendfile = os.sendfile + with unittest.mock.patch('os.sendfile', create=True, + side_effect=sendfile): + with self.get_files() as (src, dst): + with self.assertRaises(OSError) as cm: + shutil._zerocopy_sendfile(src, dst) + assert flag + self.assertEqual(cm.exception.errno, errno.EBADF) + + def test_cant_get_size(self): + # Emulate a case where src file size cannot be determined. + # Internally bufsize will be set to a small value and + # sendfile() will be called repeatedly. + with unittest.mock.patch('os.fstat', side_effect=OSError) as m: + with self.get_files() as (src, dst): + shutil._zerocopy_sendfile(src, dst) + assert m.called + self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) + + def test_small_chunks(self): + # Force internal file size detection to be smaller than the + # actual file size. We want to force sendfile() to be called + # multiple times, also in order to emulate a src fd which gets + # bigger while it is being copied. + mock = unittest.mock.Mock() + mock.st_size = 65536 + 1 + with unittest.mock.patch('os.fstat', return_value=mock) as m: + with self.get_files() as (src, dst): + shutil._zerocopy_sendfile(src, dst) + assert m.called + self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) + + def test_big_chunk(self): + # Force internal file size detection to be +100MB bigger than + # the actual file size. Make sure sendfile() does not rely on + # file size value except for (maybe) a better throughput / + # performance. + mock = unittest.mock.Mock() + mock.st_size = self.FILESIZE + (100 * 1024 * 1024) + with unittest.mock.patch('os.fstat', return_value=mock) as m: + with self.get_files() as (src, dst): + shutil._zerocopy_sendfile(src, dst) + assert m.called + self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA) + + def test_blocksize_arg(self): + with unittest.mock.patch('os.sendfile', + side_effect=ZeroDivisionError) as m: + self.assertRaises(ZeroDivisionError, + shutil.copyfile, TESTFN, TESTFN2) + blocksize = m.call_args[0][3] + # Make sure file size and the block size arg passed to + # sendfile() are the same. + self.assertEqual(blocksize, os.path.getsize(TESTFN)) + # ...unless we're dealing with a small file. + support.unlink(TESTFN2) + write_file(TESTFN2, b"hello", binary=True) + self.addCleanup(support.unlink, TESTFN2 + '3') + self.assertRaises(ZeroDivisionError, + shutil.copyfile, TESTFN2, TESTFN2 + '3') + blocksize = m.call_args[0][3] + self.assertEqual(blocksize, 2 ** 23) + + +@unittest.skipIf(not HAS_OSX_ZEROCOPY, 'OSX only') +class TestZeroCopyOSX(_ZeroCopyFileTest, unittest.TestCase): + PATCHPOINT = "posix._fcopyfile" + + def zerocopy_fun(self, *args, **kwargs): + return shutil._zerocopy_osx(*args, **kwargs) + + +@unittest.skipIf(not os.name == 'nt', 'Windows only') +class TestZeroCopyWindows(_ZeroCopyFileTest, unittest.TestCase): + PATCHPOINT = "nt._win32copyfile" + + def zerocopy_fun(self, src, dst): + return shutil._zerocopy_win(src.name, dst.name) + + class TermsizeTests(unittest.TestCase): def test_does_not_crash(self): """Check if get_terminal_size() returns a meaningful value. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index e4bbd08..6cef185 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -3853,6 +3853,77 @@ exit: return return_value; } +#if defined(__APPLE__) + +PyDoc_STRVAR(os__fcopyfile__doc__, +"_fcopyfile($module, infd, outfd, /)\n" +"--\n" +"\n" +"Efficiently copy the content of 2 file descriptors (OSX only)."); + +#define OS__FCOPYFILE_METHODDEF \ + {"_fcopyfile", (PyCFunction)os__fcopyfile, METH_FASTCALL, os__fcopyfile__doc__}, + +static PyObject * +os__fcopyfile_impl(PyObject *module, int infd, int outfd); + +static PyObject * +os__fcopyfile(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int infd; + int outfd; + + if (!_PyArg_ParseStack(args, nargs, "ii:_fcopyfile", + &infd, &outfd)) { + goto exit; + } + return_value = os__fcopyfile_impl(module, infd, outfd); + +exit: + return return_value; +} + +#endif /* defined(__APPLE__) */ + +#if (defined MS_WINDOWS) + +PyDoc_STRVAR(os__win32copyfile__doc__, +"_win32copyfile($module, src, dst, /)\n" +"--\n" +"\n" +"Efficiently copy 2 files (Windows only)."); + +#define OS__WIN32COPYFILE_METHODDEF \ + {"_win32copyfile", (PyCFunction)os__win32copyfile, METH_FASTCALL, os__win32copyfile__doc__}, + +static PyObject * +os__win32copyfile_impl(PyObject *module, path_t *src, path_t *dst); + +static PyObject * +os__win32copyfile(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + path_t src = PATH_T_INITIALIZE("_win32copyfile", "src", 0, 0); + path_t dst = PATH_T_INITIALIZE("_win32copyfile", "dst", 0, 0); + + if (!_PyArg_ParseStack(args, nargs, "O&O&:_win32copyfile", + path_converter, &src, path_converter, &dst)) { + goto exit; + } + return_value = os__win32copyfile_impl(module, &src, &dst); + +exit: + /* Cleanup for src */ + path_cleanup(&src); + /* Cleanup for dst */ + path_cleanup(&dst); + + return return_value; +} + +#endif /* (defined MS_WINDOWS) */ + PyDoc_STRVAR(os_fstat__doc__, "fstat($module, /, fd)\n" "--\n" @@ -6414,6 +6485,14 @@ exit: #define OS_PREADV_METHODDEF #endif /* !defined(OS_PREADV_METHODDEF) */ +#ifndef OS__FCOPYFILE_METHODDEF + #define OS__FCOPYFILE_METHODDEF +#endif /* !defined(OS__FCOPYFILE_METHODDEF) */ + +#ifndef OS__WIN32COPYFILE_METHODDEF + #define OS__WIN32COPYFILE_METHODDEF +#endif /* !defined(OS__WIN32COPYFILE_METHODDEF) */ + #ifndef OS_PIPE_METHODDEF #define OS_PIPE_METHODDEF #endif /* !defined(OS_PIPE_METHODDEF) */ @@ -6589,4 +6668,4 @@ exit: #ifndef OS_GETRANDOM_METHODDEF #define OS_GETRANDOM_METHODDEF #endif /* !defined(OS_GETRANDOM_METHODDEF) */ -/*[clinic end generated code: output=8d3d9dddf254c3c2 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=6e1b458924d141ed input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index a9b3917..83580f1 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -97,6 +97,10 @@ corresponding Unix manual entries for more information on calls."); #include #endif +#if defined(__APPLE__) +#include +#endif + #ifdef HAVE_SCHED_H #include #endif @@ -8745,6 +8749,62 @@ done: #endif /* HAVE_SENDFILE */ +#if defined(__APPLE__) +/*[clinic input] +os._fcopyfile + + infd: int + outfd: int + / + +Efficiently copy the content of 2 file descriptors (OSX only). +[clinic start generated code]*/ + +static PyObject * +os__fcopyfile_impl(PyObject *module, int infd, int outfd) +/*[clinic end generated code: output=3e629d5c50b33d04 input=ef4f7667f63d3e42]*/ +{ + // copyfile() source code: + // https://opensource.apple.com/source/copyfile/copyfile-42/copyfile.c + int ret; + + Py_BEGIN_ALLOW_THREADS + ret = fcopyfile(infd, outfd, NULL, COPYFILE_DATA); + Py_END_ALLOW_THREADS + if (ret < 0) + return PyErr_SetFromErrno(PyExc_OSError); + Py_RETURN_NONE; +} +#endif + + +#if defined MS_WINDOWS +/*[clinic input] +os._win32copyfile + + src: path_t + dst: path_t + / + +Efficiently copy 2 files (Windows only). +[clinic start generated code]*/ + +static PyObject * +os__win32copyfile_impl(PyObject *module, path_t *src, path_t *dst) +/*[clinic end generated code: output=9df245926c468843 input=00817871f5770bdc]*/ +{ + int ret; + + Py_BEGIN_ALLOW_THREADS + ret = CopyFileW(src->wide, dst->wide, FALSE); + Py_END_ALLOW_THREADS + if (ret == 0) + return win32_error_object("_win32copyfile", src->object); + Py_RETURN_NONE; +} +#endif + + /*[clinic input] os.fstat @@ -12921,6 +12981,8 @@ static PyMethodDef posix_methods[] = { OS_UTIME_METHODDEF OS_TIMES_METHODDEF OS__EXIT_METHODDEF + OS__FCOPYFILE_METHODDEF + OS__WIN32COPYFILE_METHODDEF OS_EXECV_METHODDEF OS_EXECVE_METHODDEF OS_SPAWNV_METHODDEF